using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.IO; using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Scanner.Analyzers.Lang; using StellaOps.Scanner.Analyzers.Lang.Plugin; using StellaOps.Scanner.Analyzers.OS.Abstractions; using StellaOps.Scanner.Analyzers.OS.Plugin; using StellaOps.Scanner.Core.Contracts; using StellaOps.Scanner.Worker.Processing; using Xunit; using WorkerOptions = StellaOps.Scanner.Worker.Options.ScannerWorkerOptions; namespace StellaOps.Scanner.Worker.Tests; public sealed class CompositeScanAnalyzerDispatcherTests { [Fact] public async Task ExecuteAsync_RunsLanguageAnalyzers_StoresResults() { using var workspace = new TempDirectory(); var metadata = new Dictionary(StringComparer.Ordinal) { { ScanMetadataKeys.RootFilesystemPath, workspace.Path }, { ScanMetadataKeys.WorkspacePath, workspace.Path }, }; var osCatalog = new FakeOsCatalog(); var languageCatalog = new FakeLanguageCatalog(new FakeLanguageAnalyzer()); var services = new ServiceCollection() .AddLogging(builder => builder.SetMinimumLevel(LogLevel.Debug)) .BuildServiceProvider(); var scopeFactory = services.GetRequiredService(); var loggerFactory = services.GetRequiredService(); var options = Microsoft.Extensions.Options.Options.Create(new WorkerOptions()); var dispatcher = new CompositeScanAnalyzerDispatcher( scopeFactory, osCatalog, languageCatalog, options, loggerFactory.CreateLogger()); var lease = new TestJobLease(metadata); var context = new ScanJobContext(lease, TimeProvider.System, TimeProvider.System.GetUtcNow(), CancellationToken.None); await dispatcher.ExecuteAsync(context, CancellationToken.None); Assert.True(context.Analysis.TryGet>(ScanAnalysisKeys.LanguageAnalyzerResults, out var results)); Assert.Single(results); Assert.True(context.Analysis.TryGet>(ScanAnalysisKeys.LanguageComponentFragments, out var fragments)); Assert.False(fragments.IsDefaultOrEmpty); Assert.True(context.Analysis.GetLayerFragments().Any(fragment => fragment.Components.Any(component => component.Identity.Name == "demo-package"))); } private sealed class FakeOsCatalog : IOSAnalyzerPluginCatalog { public IReadOnlyCollection Plugins => Array.Empty(); public void LoadFromDirectory(string directory, bool seal = true) { } public IReadOnlyList CreateAnalyzers(IServiceProvider services) => Array.Empty(); } private sealed class FakeLanguageCatalog : ILanguageAnalyzerPluginCatalog { private readonly IReadOnlyList _analyzers; public FakeLanguageCatalog(params ILanguageAnalyzer[] analyzers) { _analyzers = analyzers ?? Array.Empty(); } public IReadOnlyCollection Plugins => Array.Empty(); public void LoadFromDirectory(string directory, bool seal = true) { } public IReadOnlyList CreateAnalyzers(IServiceProvider services) => _analyzers; } private sealed class FakeLanguageAnalyzer : ILanguageAnalyzer { public string Id => "lang.fake"; public string DisplayName => "Fake Language Analyzer"; public ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken) { writer.AddFromPurl( analyzerId: Id, purl: "pkg:npm/demo-package@1.0.0", name: "demo-package", version: "1.0.0", type: "npm"); return ValueTask.CompletedTask; } } private sealed class TestJobLease : IScanJobLease { private readonly Dictionary _metadata; public TestJobLease(Dictionary metadata) { _metadata = metadata; JobId = Guid.NewGuid().ToString("n"); ScanId = $"scan-{Guid.NewGuid():n}"; Attempt = 1; EnqueuedAtUtc = DateTimeOffset.UtcNow.AddSeconds(-1); LeasedAtUtc = DateTimeOffset.UtcNow; LeaseDuration = TimeSpan.FromMinutes(5); } public string JobId { get; } public string ScanId { get; } public int Attempt { get; } public DateTimeOffset EnqueuedAtUtc { get; } public DateTimeOffset LeasedAtUtc { get; } public TimeSpan LeaseDuration { get; } public IReadOnlyDictionary Metadata => _metadata; public ValueTask RenewAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask; public ValueTask CompleteAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask; public ValueTask AbandonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask; public ValueTask PoisonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask; public ValueTask DisposeAsync() => ValueTask.CompletedTask; } private sealed class TempDirectory : IDisposable { public TempDirectory() { Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"stellaops-test-{Guid.NewGuid():n}"); Directory.CreateDirectory(Path); } public string Path { get; } public void Dispose() { try { if (Directory.Exists(Path)) { Directory.Delete(Path, recursive: true); } } catch { } } } }