tests fixes and sprints work

This commit is contained in:
master
2026-01-22 19:08:46 +02:00
parent c32fff8f86
commit 726d70dc7f
881 changed files with 134434 additions and 6228 deletions

View File

@@ -0,0 +1,253 @@
// -----------------------------------------------------------------------------
// CorpusMirrorFreshnessCheckTests.cs
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
// Task: GCB-004 - Doctor checks for ground-truth corpus health
// Description: Unit tests for CorpusMirrorFreshnessCheck
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugin.BinaryAnalysis.Checks;
using StellaOps.Doctor.Plugins;
using Xunit;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Tests.Checks;
[Trait("Category", "Unit")]
public class CorpusMirrorFreshnessCheckTests : IDisposable
{
private readonly string _tempDir;
private readonly CorpusMirrorFreshnessCheck _check = new();
private readonly FakeTimeProvider _timeProvider;
public CorpusMirrorFreshnessCheckTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"corpus-mirror-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 22, 12, 0, 0, TimeSpan.Zero));
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
[Fact]
public void CheckId_ReturnsExpectedValue()
{
// Assert
_check.CheckId.Should().Be("check.binaryanalysis.corpus.mirror.freshness");
}
[Fact]
public void Name_ReturnsCorpusMirrorFreshness()
{
// Assert
_check.Name.Should().Be("Corpus Mirror Freshness");
}
[Fact]
public void DefaultSeverity_IsWarn()
{
// Assert
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public void Tags_ContainsExpectedValues()
{
// Assert
_check.Tags.Should().Contain("binaryanalysis");
_check.Tags.Should().Contain("corpus");
_check.Tags.Should().Contain("mirrors");
_check.Tags.Should().Contain("groundtruth");
}
[Fact]
public void CanRun_ReturnsTrue_Always()
{
// Arrange
var context = CreateContext(_tempDir);
// Act & Assert
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_ReturnsWarn_WhenMirrorsDirectoryDoesNotExist()
{
// Arrange
var nonExistentDir = Path.Combine(_tempDir, "nonexistent");
var context = CreateContext(nonExistentDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("does not exist");
}
[Fact]
public async Task RunAsync_ReturnsFail_WhenNoMirrorsExist()
{
// Arrange - directory exists but no mirrors
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("No corpus mirrors found");
}
[Fact]
public async Task RunAsync_ReturnsPass_WhenAllMirrorsAreFresh()
{
// Arrange - create fresh mirrors
CreateMirrorWithFiles(_tempDir, "debian/archive", daysOld: 1);
CreateMirrorWithFiles(_tempDir, "debian/snapshot", daysOld: 2);
CreateMirrorWithFiles(_tempDir, "ubuntu/usn-index", daysOld: 3);
CreateMirrorWithFiles(_tempDir, "alpine/secdb", daysOld: 4);
CreateMirrorWithFiles(_tempDir, "osv", daysOld: 5);
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("fresh");
}
[Fact]
public async Task RunAsync_ReturnsFail_WhenAllMirrorsAreStale()
{
// Arrange - create stale mirrors (> 7 days old by default)
CreateMirrorWithFiles(_tempDir, "debian/archive", daysOld: 10);
CreateMirrorWithFiles(_tempDir, "debian/snapshot", daysOld: 15);
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("stale");
}
[Fact]
public async Task RunAsync_ReturnsWarn_WhenSomeMirrorsAreStale()
{
// Arrange - mix of fresh and stale mirrors
CreateMirrorWithFiles(_tempDir, "debian/archive", daysOld: 2); // Fresh
CreateMirrorWithFiles(_tempDir, "debian/snapshot", daysOld: 10); // Stale
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("stale");
}
[Fact]
public async Task RunAsync_RespectsConfiguredThreshold()
{
// Arrange - create mirror that is 5 days old
CreateMirrorWithFiles(_tempDir, "debian/archive", daysOld: 5);
// Configure threshold to 3 days (so 5 days is stale)
var context = CreateContext(_tempDir, staleThresholdDays: 3);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("stale");
}
[Fact]
public async Task RunAsync_IncludesVerificationCommand()
{
// Arrange
CreateMirrorWithFiles(_tempDir, "debian/archive", daysOld: 2);
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.VerificationCommand.Should().NotBeNullOrEmpty();
result.VerificationCommand.Should().Contain("stella doctor --check");
result.VerificationCommand.Should().Contain(_check.CheckId);
}
[Fact]
public async Task RunAsync_IncludesRemediationSteps_OnFailure()
{
// Arrange - no mirrors
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Remediation.Should().NotBeNull();
result.Remediation!.Steps.Should().NotBeEmpty();
}
private DoctorPluginContext CreateContext(string mirrorsRoot, int? staleThresholdDays = null)
{
var configValues = new Dictionary<string, string?>
{
["BinaryAnalysis:Corpus:MirrorsDirectory"] = mirrorsRoot
};
if (staleThresholdDays.HasValue)
{
configValues["BinaryAnalysis:Corpus:StalenessThresholdDays"] = staleThresholdDays.Value.ToString();
}
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = _timeProvider,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
private void CreateMirrorWithFiles(string root, string subdir, int daysOld)
{
var mirrorPath = Path.Combine(root, subdir);
Directory.CreateDirectory(mirrorPath);
// Create some test files
var testFile = Path.Combine(mirrorPath, "test-data.json");
File.WriteAllText(testFile, "{}");
// Set modification time
var modTime = _timeProvider.GetUtcNow().AddDays(-daysOld).DateTime;
File.SetLastWriteTimeUtc(testFile, modTime);
}
}

View File

@@ -0,0 +1,340 @@
// -----------------------------------------------------------------------------
// KpiBaselineExistsCheckTests.cs
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
// Task: GCB-004 - Doctor checks for ground-truth corpus health
// Description: Unit tests for KpiBaselineExistsCheck
// -----------------------------------------------------------------------------
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugin.BinaryAnalysis.Checks;
using StellaOps.Doctor.Plugins;
using Xunit;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Tests.Checks;
[Trait("Category", "Unit")]
public class KpiBaselineExistsCheckTests : IDisposable
{
private readonly string _tempDir;
private readonly KpiBaselineExistsCheck _check = new();
private readonly FakeTimeProvider _timeProvider;
public KpiBaselineExistsCheckTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"kpi-baseline-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 22, 12, 0, 0, TimeSpan.Zero));
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
[Fact]
public void CheckId_ReturnsExpectedValue()
{
// Assert
_check.CheckId.Should().Be("check.binaryanalysis.corpus.kpi.baseline");
}
[Fact]
public void Name_ReturnsKpiBaselineConfiguration()
{
// Assert
_check.Name.Should().Be("KPI Baseline Configuration");
}
[Fact]
public void DefaultSeverity_IsWarn()
{
// Assert
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public void Tags_ContainsExpectedValues()
{
// Assert
_check.Tags.Should().Contain("binaryanalysis");
_check.Tags.Should().Contain("corpus");
_check.Tags.Should().Contain("kpi");
_check.Tags.Should().Contain("baseline");
_check.Tags.Should().Contain("groundtruth");
}
[Fact]
public void CanRun_ReturnsTrue_Always()
{
// Arrange
var context = CreateContext(_tempDir);
// Act & Assert
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_ReturnsWarn_WhenBaselineDirectoryDoesNotExist()
{
// Arrange
var nonExistentDir = Path.Combine(_tempDir, "nonexistent");
var context = CreateContext(nonExistentDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("does not exist");
}
[Fact]
public async Task RunAsync_ReturnsWarn_WhenBaselineFileDoesNotExist()
{
// Arrange - directory exists but no baseline file
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("No KPI baseline file found");
}
[Fact]
public async Task RunAsync_ReturnsWarn_WhenOtherBaselineFilesExist()
{
// Arrange - create a different baseline file
var otherBaseline = Path.Combine(_tempDir, "baseline-20260120.json");
File.WriteAllText(otherBaseline, CreateValidBaselineJson());
var context = CreateContext(_tempDir); // Will look for current.json
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("other baseline file");
}
[Fact]
public async Task RunAsync_ReturnsFail_WhenBaselineFileIsEmpty()
{
// Arrange
var baselinePath = Path.Combine(_tempDir, "current.json");
File.WriteAllText(baselinePath, "");
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("invalid");
}
[Fact]
public async Task RunAsync_ReturnsFail_WhenBaselineFileHasInvalidJson()
{
// Arrange
var baselinePath = Path.Combine(_tempDir, "current.json");
File.WriteAllText(baselinePath, "{ invalid json }");
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("invalid");
result.Diagnosis.Should().Contain("JSON");
}
[Fact]
public async Task RunAsync_ReturnsFail_WhenBaselineFileHasNoKpiFields()
{
// Arrange
var baselinePath = Path.Combine(_tempDir, "current.json");
File.WriteAllText(baselinePath, "{ \"unrelated\": \"data\" }");
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("invalid");
}
[Fact]
public async Task RunAsync_ReturnsWarn_WhenSomeKpiFieldsAreMissing()
{
// Arrange - partial baseline with only some fields
var baselinePath = Path.Combine(_tempDir, "current.json");
var partialBaseline = new
{
precision = 0.95,
recall = 0.92
// Missing: falseNegativeRate, deterministicReplayRate, ttfrpP95Ms
};
File.WriteAllText(baselinePath, JsonSerializer.Serialize(partialBaseline));
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("missing");
}
[Fact]
public async Task RunAsync_ReturnsPass_WhenBaselineIsValid()
{
// Arrange
var baselinePath = Path.Combine(_tempDir, "current.json");
File.WriteAllText(baselinePath, CreateValidBaselineJson());
// Set file modification time to 2 days ago
File.SetLastWriteTimeUtc(baselinePath, _timeProvider.GetUtcNow().AddDays(-2).DateTime);
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("valid");
result.Diagnosis.Should().Contain("2 days ago");
}
[Fact]
public async Task RunAsync_AcceptsCamelCaseKpiFields()
{
// Arrange
var baselinePath = Path.Combine(_tempDir, "current.json");
var baseline = new
{
precision = 0.95,
recall = 0.92,
falseNegativeRate = 0.08,
deterministicReplayRate = 1.0,
ttfrpP95Ms = 150
};
File.WriteAllText(baselinePath, JsonSerializer.Serialize(baseline));
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
}
[Fact]
public async Task RunAsync_IncludesVerificationCommand()
{
// Arrange
var baselinePath = Path.Combine(_tempDir, "current.json");
File.WriteAllText(baselinePath, CreateValidBaselineJson());
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.VerificationCommand.Should().NotBeNullOrEmpty();
result.VerificationCommand.Should().Contain("stella doctor --check");
result.VerificationCommand.Should().Contain(_check.CheckId);
}
[Fact]
public async Task RunAsync_IncludesRemediationSteps_OnFailure()
{
// Arrange - no baseline
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Remediation.Should().NotBeNull();
result.Remediation!.Steps.Should().NotBeEmpty();
}
[Fact]
public async Task RunAsync_RespectsCustomFilename()
{
// Arrange
var customFilename = "my-baseline.json";
var baselinePath = Path.Combine(_tempDir, customFilename);
File.WriteAllText(baselinePath, CreateValidBaselineJson());
var context = CreateContext(_tempDir, customFilename);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
}
private DoctorPluginContext CreateContext(string baselineDir, string? baselineFilename = null)
{
var configValues = new Dictionary<string, string?>
{
["BinaryAnalysis:Corpus:BaselineDirectory"] = baselineDir
};
if (baselineFilename != null)
{
configValues["BinaryAnalysis:Corpus:BaselineFilename"] = baselineFilename;
}
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = _timeProvider,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
private static string CreateValidBaselineJson()
{
var baseline = new
{
precision = 0.95,
recall = 0.92,
falseNegativeRate = 0.08,
deterministicReplayRate = 1.0,
ttfrpP95Ms = 150,
timestamp = "2026-01-20T10:00:00Z"
};
return JsonSerializer.Serialize(baseline);
}
}

View File

@@ -0,0 +1,523 @@
// -----------------------------------------------------------------------------
// CorpusHealthChecksIntegrationTests.cs
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
// Task: GCB-004 - Integration test with mock services
// Description: Integration tests for corpus health Doctor checks with mock services
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using StellaOps.Doctor.Plugin.BinaryAnalysis.Checks;
using StellaOps.Doctor.Plugin.BinaryAnalysis.Options;
using Xunit;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Tests.Integration;
/// <summary>
/// Integration tests for ground-truth corpus Doctor checks.
/// These tests verify the health checks work correctly with mock services
/// simulating various infrastructure states.
/// </summary>
public sealed class CorpusHealthChecksIntegrationTests : IDisposable
{
private readonly string _testOutputDir;
private readonly IServiceProvider _serviceProvider;
public CorpusHealthChecksIntegrationTests()
{
_testOutputDir = Path.Combine(Path.GetTempPath(), $"corpus-health-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_testOutputDir);
_serviceProvider = BuildServiceProvider();
}
#region DebuginfodAvailabilityCheck Tests
[Fact]
public async Task DebuginfodAvailabilityCheck_AllUrlsReachable_ReturnsPass()
{
// Arrange
var httpClient = CreateMockHttpClient(HttpStatusCode.OK);
var options = CreateDebuginfodOptions(["https://debuginfod.fedora.org", "https://debuginfod.ubuntu.com"]);
var check = new DebuginfodAvailabilityCheck(httpClient, options, NullLogger<DebuginfodAvailabilityCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Pass);
result.Message.Should().Contain("reachable");
}
[Fact]
public async Task DebuginfodAvailabilityCheck_SomeUrlsUnreachable_ReturnsWarn()
{
// Arrange
var httpClient = CreateMockHttpClient(statusByUrl: new Dictionary<string, HttpStatusCode>
{
["https://debuginfod.fedora.org"] = HttpStatusCode.OK,
["https://debuginfod.ubuntu.com"] = HttpStatusCode.ServiceUnavailable
});
var options = CreateDebuginfodOptions(["https://debuginfod.fedora.org", "https://debuginfod.ubuntu.com"]);
var check = new DebuginfodAvailabilityCheck(httpClient, options, NullLogger<DebuginfodAvailabilityCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Warn);
result.Message.Should().Contain("unreachable");
}
[Fact]
public async Task DebuginfodAvailabilityCheck_AllUrlsUnreachable_ReturnsFail()
{
// Arrange
var httpClient = CreateMockHttpClient(HttpStatusCode.ServiceUnavailable);
var options = CreateDebuginfodOptions(["https://debuginfod.fedora.org"]);
var check = new DebuginfodAvailabilityCheck(httpClient, options, NullLogger<DebuginfodAvailabilityCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Fail);
}
#endregion
#region CorpusMirrorFreshnessCheck Tests
[Fact]
public async Task CorpusMirrorFreshnessCheck_FreshMirrors_ReturnsPass()
{
// Arrange
SetupFreshMirrors();
var options = CreateMirrorOptions(_testOutputDir);
var check = new CorpusMirrorFreshnessCheck(options, NullLogger<CorpusMirrorFreshnessCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Pass);
result.Message.Should().Contain("fresh");
}
[Fact]
public async Task CorpusMirrorFreshnessCheck_StaleMirrors_ReturnsWarn()
{
// Arrange
SetupStaleMirrors(daysOld: 5); // Within warning threshold
var options = CreateMirrorOptions(_testOutputDir, staleDays: 7, warnDays: 3);
var check = new CorpusMirrorFreshnessCheck(options, NullLogger<CorpusMirrorFreshnessCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Warn);
result.Message.Should().Contain("stale");
}
[Fact]
public async Task CorpusMirrorFreshnessCheck_VeryOldMirrors_ReturnsFail()
{
// Arrange
SetupStaleMirrors(daysOld: 30); // Beyond stale threshold
var options = CreateMirrorOptions(_testOutputDir, staleDays: 7);
var check = new CorpusMirrorFreshnessCheck(options, NullLogger<CorpusMirrorFreshnessCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Fail);
}
[Fact]
public async Task CorpusMirrorFreshnessCheck_MissingLastSync_ReturnsFail()
{
// Arrange - no .last-sync files
Directory.CreateDirectory(Path.Combine(_testOutputDir, "mirrors", "debian"));
var options = CreateMirrorOptions(_testOutputDir);
var check = new CorpusMirrorFreshnessCheck(options, NullLogger<CorpusMirrorFreshnessCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Fail);
result.Message.Should().Contain("never synced");
}
#endregion
#region KpiBaselineExistsCheck Tests
[Fact]
public async Task KpiBaselineExistsCheck_BaselineExists_ReturnsPass()
{
// Arrange
SetupValidBaseline();
var options = CreateKpiOptions(_testOutputDir);
var check = new KpiBaselineExistsCheck(options, NullLogger<KpiBaselineExistsCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Pass);
result.Message.Should().Contain("baseline");
}
[Fact]
public async Task KpiBaselineExistsCheck_BaselineMissing_ReturnsFail()
{
// Arrange - no baseline file
var options = CreateKpiOptions(_testOutputDir);
var check = new KpiBaselineExistsCheck(options, NullLogger<KpiBaselineExistsCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Fail);
result.Message.Should().Contain("not found");
}
[Fact]
public async Task KpiBaselineExistsCheck_OldBaseline_ReturnsWarn()
{
// Arrange
SetupOldBaseline(daysOld: 45); // Older than recommended refresh interval
var options = CreateKpiOptions(_testOutputDir, maxBaselineAgeDays: 30);
var check = new KpiBaselineExistsCheck(options, NullLogger<KpiBaselineExistsCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Warn);
result.Message.Should().Contain("old");
}
#endregion
#region BuildinfoCacheAccessibleCheck Tests
[Fact]
public async Task BuildinfoCacheAccessibleCheck_CacheAccessible_ReturnsPass()
{
// Arrange
var httpClient = CreateMockHttpClient(HttpStatusCode.OK);
var options = CreateBuildinfoOptions("https://buildinfos.debian.net");
var check = new BuildinfoCacheAccessibleCheck(httpClient, options, NullLogger<BuildinfoCacheAccessibleCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Pass);
}
[Fact]
public async Task BuildinfoCacheAccessibleCheck_CacheUnreachable_ReturnsFail()
{
// Arrange
var httpClient = CreateMockHttpClient(HttpStatusCode.ServiceUnavailable);
var options = CreateBuildinfoOptions("https://buildinfos.debian.net");
var check = new BuildinfoCacheAccessibleCheck(httpClient, options, NullLogger<BuildinfoCacheAccessibleCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Fail);
}
#endregion
#region SymbolRecoveryFallbackCheck Tests
[Fact]
public async Task SymbolRecoveryFallbackCheck_FallbackConfigured_ReturnsPass()
{
// Arrange
SetupFallbackSymbols();
var options = CreateFallbackOptions(_testOutputDir, fallbackEnabled: true);
var check = new SymbolRecoveryFallbackCheck(options, NullLogger<SymbolRecoveryFallbackCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Pass);
result.Message.Should().Contain("fallback");
}
[Fact]
public async Task SymbolRecoveryFallbackCheck_FallbackDisabled_ReturnsWarn()
{
// Arrange
var options = CreateFallbackOptions(_testOutputDir, fallbackEnabled: false);
var check = new SymbolRecoveryFallbackCheck(options, NullLogger<SymbolRecoveryFallbackCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Warn);
}
#endregion
#region Plugin Integration Tests
[Fact]
public void BinaryAnalysisDoctorPlugin_RegistersAllCorpusChecks()
{
// Arrange
var plugin = new BinaryAnalysisDoctorPlugin();
// Act
var checks = plugin.GetChecks(_serviceProvider).ToList();
// Assert
checks.Should().Contain(c => c.Name.Contains("debuginfod", StringComparison.OrdinalIgnoreCase));
checks.Should().Contain(c => c.Name.Contains("mirror", StringComparison.OrdinalIgnoreCase));
checks.Should().Contain(c => c.Name.Contains("baseline", StringComparison.OrdinalIgnoreCase));
checks.Should().Contain(c => c.Name.Contains("buildinfo", StringComparison.OrdinalIgnoreCase));
checks.Should().Contain(c => c.Name.Contains("fallback", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public async Task AllCorpusChecks_RunWithoutException()
{
// Arrange
SetupFreshMirrors();
SetupValidBaseline();
SetupFallbackSymbols();
var plugin = new BinaryAnalysisDoctorPlugin();
var checks = plugin.GetChecks(_serviceProvider).ToList();
// Act & Assert
foreach (var check in checks.Where(c => c.Category == "corpus"))
{
var act = async () => await check.RunAsync(CancellationToken.None);
await act.Should().NotThrowAsync();
}
}
#endregion
#region Helper Methods
private IServiceProvider BuildServiceProvider()
{
var services = new ServiceCollection();
services.AddSingleton(CreateMockHttpClient(HttpStatusCode.OK));
services.Configure<DebuginfodOptions>(o =>
{
o.Urls = ["https://debuginfod.fedora.org"];
});
services.Configure<CorpusMirrorOptions>(o =>
{
o.MirrorsPath = Path.Combine(_testOutputDir, "mirrors");
});
services.Configure<KpiOptions>(o =>
{
o.BaselinePath = Path.Combine(_testOutputDir, "baselines", "current.json");
});
services.Configure<BuildinfoOptions>(o =>
{
o.CacheUrl = "https://buildinfos.debian.net";
});
services.Configure<SymbolFallbackOptions>(o =>
{
o.FallbackPath = Path.Combine(_testOutputDir, "fallback");
o.Enabled = true;
});
services.AddLogging();
return services.BuildServiceProvider();
}
private static HttpClient CreateMockHttpClient(HttpStatusCode defaultStatus)
{
var handler = new MockHttpMessageHandler(defaultStatus);
return new HttpClient(handler);
}
private static HttpClient CreateMockHttpClient(Dictionary<string, HttpStatusCode> statusByUrl)
{
var handler = new MockHttpMessageHandler(statusByUrl);
return new HttpClient(handler);
}
private static IOptions<DebuginfodOptions> CreateDebuginfodOptions(string[] urls)
{
return Options.Create(new DebuginfodOptions { Urls = urls });
}
private IOptions<CorpusMirrorOptions> CreateMirrorOptions(string basePath, int staleDays = 7, int warnDays = 3)
{
return Options.Create(new CorpusMirrorOptions
{
MirrorsPath = Path.Combine(basePath, "mirrors"),
StaleDaysThreshold = staleDays,
WarnDaysThreshold = warnDays
});
}
private IOptions<KpiOptions> CreateKpiOptions(string basePath, int maxBaselineAgeDays = 30)
{
return Options.Create(new KpiOptions
{
BaselinePath = Path.Combine(basePath, "baselines", "current.json"),
MaxBaselineAgeDays = maxBaselineAgeDays
});
}
private static IOptions<BuildinfoOptions> CreateBuildinfoOptions(string url)
{
return Options.Create(new BuildinfoOptions { CacheUrl = url });
}
private IOptions<SymbolFallbackOptions> CreateFallbackOptions(string basePath, bool fallbackEnabled)
{
return Options.Create(new SymbolFallbackOptions
{
FallbackPath = Path.Combine(basePath, "fallback"),
Enabled = fallbackEnabled
});
}
private void SetupFreshMirrors()
{
var mirrorsPath = Path.Combine(_testOutputDir, "mirrors");
foreach (var mirror in new[] { "debian", "ubuntu", "alpine", "osv" })
{
var mirrorDir = Path.Combine(mirrorsPath, mirror);
Directory.CreateDirectory(mirrorDir);
File.WriteAllText(Path.Combine(mirrorDir, ".last-sync"), DateTime.UtcNow.ToString("O"));
}
}
private void SetupStaleMirrors(int daysOld)
{
var mirrorsPath = Path.Combine(_testOutputDir, "mirrors");
foreach (var mirror in new[] { "debian", "ubuntu" })
{
var mirrorDir = Path.Combine(mirrorsPath, mirror);
Directory.CreateDirectory(mirrorDir);
File.WriteAllText(Path.Combine(mirrorDir, ".last-sync"),
DateTime.UtcNow.AddDays(-daysOld).ToString("O"));
}
}
private void SetupValidBaseline()
{
var baselineDir = Path.Combine(_testOutputDir, "baselines");
Directory.CreateDirectory(baselineDir);
var baseline = new
{
baselineId = Guid.NewGuid().ToString(),
createdAt = DateTime.UtcNow.ToString("O"),
precision = 0.95,
recall = 0.92
};
File.WriteAllText(Path.Combine(baselineDir, "current.json"),
System.Text.Json.JsonSerializer.Serialize(baseline));
}
private void SetupOldBaseline(int daysOld)
{
var baselineDir = Path.Combine(_testOutputDir, "baselines");
Directory.CreateDirectory(baselineDir);
var baseline = new
{
baselineId = Guid.NewGuid().ToString(),
createdAt = DateTime.UtcNow.AddDays(-daysOld).ToString("O"),
precision = 0.95,
recall = 0.92
};
File.WriteAllText(Path.Combine(baselineDir, "current.json"),
System.Text.Json.JsonSerializer.Serialize(baseline));
}
private void SetupFallbackSymbols()
{
var fallbackDir = Path.Combine(_testOutputDir, "fallback");
Directory.CreateDirectory(fallbackDir);
File.WriteAllText(Path.Combine(fallbackDir, "fallback-config.json"), "{}");
}
public void Dispose()
{
if (Directory.Exists(_testOutputDir))
{
try
{
Directory.Delete(_testOutputDir, recursive: true);
}
catch
{
// Best effort cleanup
}
}
}
#endregion
#region Mock HTTP Handler
private sealed class MockHttpMessageHandler : HttpMessageHandler
{
private readonly HttpStatusCode _defaultStatus;
private readonly Dictionary<string, HttpStatusCode>? _statusByUrl;
public MockHttpMessageHandler(HttpStatusCode defaultStatus)
{
_defaultStatus = defaultStatus;
}
public MockHttpMessageHandler(Dictionary<string, HttpStatusCode> statusByUrl)
{
_statusByUrl = statusByUrl;
_defaultStatus = HttpStatusCode.OK;
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var url = request.RequestUri?.ToString() ?? "";
var status = _statusByUrl?.GetValueOrDefault(url, _defaultStatus) ?? _defaultStatus;
return Task.FromResult(new HttpResponseMessage(status));
}
}
#endregion
}
// Enum for HTTP status codes (if not already in scope)
file enum HttpStatusCode
{
OK = 200,
ServiceUnavailable = 503
}
// Check status enum (if not already defined in the Doctor plugin)
file enum CheckStatus
{
Pass,
Warn,
Fail
}

View File

@@ -10,8 +10,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="Moq" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>