using System.Diagnostics; using System.Text; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; using StellaOps.Scanner.Analyzers.Lang; using StellaOps.Scanner.Analyzers.Lang.Go; using StellaOps.Scanner.Analyzers.Lang.Java; using StellaOps.Scanner.Analyzers.Lang.Node; using StellaOps.Scanner.Analyzers.Lang.DotNet; using StellaOps.Scanner.Analyzers.Lang.Python; namespace StellaOps.Bench.ScannerAnalyzers.Scenarios; internal interface IScenarioRunner { Task ExecuteAsync(string rootPath, int iterations, CancellationToken cancellationToken); } internal sealed record ScenarioExecutionResult(double[] Durations, int SampleCount); internal static class ScenarioRunnerFactory { public static IScenarioRunner Create(BenchmarkScenarioConfig scenario) { if (scenario.HasAnalyzers) { return new LanguageAnalyzerScenarioRunner(scenario.Analyzers!); } if (string.IsNullOrWhiteSpace(scenario.Parser) || string.IsNullOrWhiteSpace(scenario.Matcher)) { throw new InvalidOperationException($"Scenario '{scenario.Id}' missing parser or matcher configuration."); } return new MetadataWalkScenarioRunner(scenario.Parser, scenario.Matcher); } } internal sealed class LanguageAnalyzerScenarioRunner : IScenarioRunner { private readonly IReadOnlyList> _analyzerFactories; public LanguageAnalyzerScenarioRunner(IEnumerable analyzerIds) { if (analyzerIds is null) { throw new ArgumentNullException(nameof(analyzerIds)); } _analyzerFactories = analyzerIds .Where(static id => !string.IsNullOrWhiteSpace(id)) .Select(CreateFactory) .ToArray(); if (_analyzerFactories.Count == 0) { throw new InvalidOperationException("At least one analyzer id must be provided."); } } public async Task ExecuteAsync(string rootPath, int iterations, CancellationToken cancellationToken) { if (iterations <= 0) { throw new ArgumentOutOfRangeException(nameof(iterations), iterations, "Iterations must be positive."); } var analyzers = _analyzerFactories.Select(factory => factory()).ToArray(); var engine = new LanguageAnalyzerEngine(analyzers); var durations = new double[iterations]; var componentCount = -1; for (var i = 0; i < iterations; i++) { cancellationToken.ThrowIfCancellationRequested(); var context = new LanguageAnalyzerContext(rootPath, TimeProvider.System); var stopwatch = Stopwatch.StartNew(); var result = await engine.AnalyzeAsync(context, cancellationToken).ConfigureAwait(false); stopwatch.Stop(); durations[i] = stopwatch.Elapsed.TotalMilliseconds; var currentCount = result.Components.Count; if (componentCount < 0) { componentCount = currentCount; } else if (componentCount != currentCount) { throw new InvalidOperationException($"Analyzer output count changed between iterations ({componentCount} vs {currentCount})."); } } if (componentCount < 0) { componentCount = 0; } return new ScenarioExecutionResult(durations, componentCount); } private static Func CreateFactory(string analyzerId) { var id = analyzerId.Trim().ToLowerInvariant(); return id switch { "java" => static () => new JavaLanguageAnalyzer(), "go" => static () => new GoLanguageAnalyzer(), "node" => static () => new NodeLanguageAnalyzer(), "dotnet" => static () => new DotNetLanguageAnalyzer(), "python" => static () => new PythonLanguageAnalyzer(), _ => throw new InvalidOperationException($"Unsupported analyzer '{analyzerId}'."), }; } } internal sealed class MetadataWalkScenarioRunner : IScenarioRunner { private readonly Regex _matcher; private readonly string _parserKind; public MetadataWalkScenarioRunner(string parserKind, string globPattern) { _parserKind = parserKind?.Trim().ToLowerInvariant() ?? throw new ArgumentNullException(nameof(parserKind)); _matcher = GlobToRegex(globPattern ?? throw new ArgumentNullException(nameof(globPattern))); } public async Task ExecuteAsync(string rootPath, int iterations, CancellationToken cancellationToken) { if (iterations <= 0) { throw new ArgumentOutOfRangeException(nameof(iterations), iterations, "Iterations must be positive."); } var durations = new double[iterations]; var sampleCount = -1; for (var i = 0; i < iterations; i++) { cancellationToken.ThrowIfCancellationRequested(); var stopwatch = Stopwatch.StartNew(); var files = EnumerateMatchingFiles(rootPath); if (files.Count == 0) { throw new InvalidOperationException($"Parser '{_parserKind}' matched zero files under '{rootPath}'."); } foreach (var file in files) { cancellationToken.ThrowIfCancellationRequested(); await ParseAsync(file).ConfigureAwait(false); } stopwatch.Stop(); durations[i] = stopwatch.Elapsed.TotalMilliseconds; if (sampleCount < 0) { sampleCount = files.Count; } else if (sampleCount != files.Count) { throw new InvalidOperationException($"File count changed between iterations ({sampleCount} vs {files.Count})."); } } if (sampleCount < 0) { sampleCount = 0; } return new ScenarioExecutionResult(durations, sampleCount); } private async ValueTask ParseAsync(string filePath) { switch (_parserKind) { case "node": { using var stream = File.OpenRead(filePath); using var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false); if (!document.RootElement.TryGetProperty("name", out var name) || name.ValueKind != JsonValueKind.String) { throw new InvalidOperationException($"package.json '{filePath}' missing name."); } if (!document.RootElement.TryGetProperty("version", out var version) || version.ValueKind != JsonValueKind.String) { throw new InvalidOperationException($"package.json '{filePath}' missing version."); } } break; case "python": { var (name, version) = await ParsePythonMetadataAsync(filePath).ConfigureAwait(false); if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(version)) { throw new InvalidOperationException($"METADATA '{filePath}' missing Name/Version."); } } break; default: throw new InvalidOperationException($"Unknown parser '{_parserKind}'."); } } private static async Task<(string? Name, string? Version)> ParsePythonMetadataAsync(string filePath) { using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete); using var reader = new StreamReader(stream); string? name = null; string? version = null; while (await reader.ReadLineAsync().ConfigureAwait(false) is { } line) { if (line.StartsWith("Name:", StringComparison.OrdinalIgnoreCase)) { name ??= line[5..].Trim(); } else if (line.StartsWith("Version:", StringComparison.OrdinalIgnoreCase)) { version ??= line[8..].Trim(); } if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(version)) { break; } } return (name, version); } private IReadOnlyList EnumerateMatchingFiles(string rootPath) { var files = new List(); var stack = new Stack(); stack.Push(rootPath); while (stack.Count > 0) { var current = stack.Pop(); foreach (var directory in Directory.EnumerateDirectories(current)) { stack.Push(directory); } foreach (var file in Directory.EnumerateFiles(current)) { var relative = Path.GetRelativePath(rootPath, file).Replace('\\', '/'); if (_matcher.IsMatch(relative)) { files.Add(file); } } } return files; } private static Regex GlobToRegex(string pattern) { if (string.IsNullOrWhiteSpace(pattern)) { throw new ArgumentException("Glob pattern is required.", nameof(pattern)); } var normalized = pattern.Replace("\\", "/"); normalized = normalized.Replace("**", "\u0001"); normalized = normalized.Replace("*", "\u0002"); var escaped = Regex.Escape(normalized); escaped = escaped.Replace("\u0001/", "(?:.*/)?", StringComparison.Ordinal); escaped = escaped.Replace("\u0001", ".*", StringComparison.Ordinal); escaped = escaped.Replace("\u0002", "[^/]*", StringComparison.Ordinal); return new Regex("^" + escaped + "$", RegexOptions.Compiled | RegexOptions.CultureInvariant); } }