test fixes and new product advisories work
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Environment;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for environment skew testing infrastructure.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class EnvironmentSkewTests
|
||||
{
|
||||
#region EnvironmentProfile Tests
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_Standard_HasCorrectDefaults()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profile = EnvironmentProfile.Standard;
|
||||
|
||||
// Assert
|
||||
profile.Name.Should().Be("Standard");
|
||||
profile.Cpu.Architecture.Should().Be(CpuArchitecture.X64);
|
||||
profile.Network.Latency.Should().Be(TimeSpan.Zero);
|
||||
profile.Runtime.Should().Be(ContainerRuntime.Docker);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_HighLatency_Has100msLatency()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profile = EnvironmentProfile.HighLatency;
|
||||
|
||||
// Assert
|
||||
profile.Name.Should().Be("HighLatency");
|
||||
profile.Network.Latency.Should().Be(TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_LowBandwidth_Has10MbpsLimit()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profile = EnvironmentProfile.LowBandwidth;
|
||||
|
||||
// Assert
|
||||
profile.Name.Should().Be("LowBandwidth");
|
||||
profile.Network.BandwidthMbps.Should().Be(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_PacketLoss_Has1PercentLoss()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profile = EnvironmentProfile.PacketLoss;
|
||||
|
||||
// Assert
|
||||
profile.Name.Should().Be("PacketLoss");
|
||||
profile.Network.PacketLossRate.Should().Be(0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_ArmCpu_HasArm64Architecture()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profile = EnvironmentProfile.ArmCpu;
|
||||
|
||||
// Assert
|
||||
profile.Name.Should().Be("ArmCpu");
|
||||
profile.Cpu.Architecture.Should().Be(CpuArchitecture.Arm64);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_ResourceConstrained_HasLimits()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profile = EnvironmentProfile.ResourceConstrained;
|
||||
|
||||
// Assert
|
||||
profile.Name.Should().Be("ResourceConstrained");
|
||||
profile.ResourceLimits.MemoryMb.Should().Be(256);
|
||||
profile.ResourceLimits.CpuCores.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_All_ContainsExpectedProfiles()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profiles = EnvironmentProfile.All;
|
||||
|
||||
// Assert
|
||||
profiles.Should().HaveCount(5);
|
||||
profiles.Should().Contain(p => p.Name == "Standard");
|
||||
profiles.Should().Contain(p => p.Name == "HighLatency");
|
||||
profiles.Should().Contain(p => p.Name == "LowBandwidth");
|
||||
profiles.Should().Contain(p => p.Name == "PacketLoss");
|
||||
profiles.Should().Contain(p => p.Name == "ResourceConstrained");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NetworkProfile_RequiresNetworkShaping_ReturnsTrueWhenConfigured()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
new NetworkProfile { Latency = TimeSpan.FromMilliseconds(50) }
|
||||
.RequiresNetworkShaping.Should().BeTrue();
|
||||
|
||||
new NetworkProfile { PacketLossRate = 0.01 }
|
||||
.RequiresNetworkShaping.Should().BeTrue();
|
||||
|
||||
new NetworkProfile { BandwidthMbps = 10 }
|
||||
.RequiresNetworkShaping.Should().BeTrue();
|
||||
|
||||
new NetworkProfile()
|
||||
.RequiresNetworkShaping.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SkewTestRunner Tests
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_RunAcrossProfiles_ExecutesTestForEachProfile()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var executedProfiles = new List<string>();
|
||||
|
||||
// Act
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () =>
|
||||
{
|
||||
executedProfiles.Add("executed");
|
||||
return Task.FromResult(new TestResult { Value = 1.0, DurationMs = 10 });
|
||||
},
|
||||
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
|
||||
|
||||
// Assert
|
||||
report.ProfileCount.Should().Be(2);
|
||||
report.Results.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_RunWithProfile_ExecutesMultipleIterations()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var executionCount = 0;
|
||||
|
||||
// Act
|
||||
var result = await runner.RunWithProfile(
|
||||
test: () =>
|
||||
{
|
||||
executionCount++;
|
||||
return Task.FromResult(new TestResult { Value = executionCount, DurationMs = 10 });
|
||||
},
|
||||
profile: EnvironmentProfile.Standard,
|
||||
iterations: 5);
|
||||
|
||||
// Assert
|
||||
executionCount.Should().Be(5);
|
||||
result.Results.Should().HaveCount(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_RunWithProfile_CalculatesAverages()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var values = new[] { 10.0, 20.0, 30.0 };
|
||||
var index = 0;
|
||||
|
||||
// Act
|
||||
var result = await runner.RunWithProfile(
|
||||
test: () => Task.FromResult(new TestResult
|
||||
{
|
||||
Value = values[index++],
|
||||
DurationMs = 100
|
||||
}),
|
||||
profile: EnvironmentProfile.Standard,
|
||||
iterations: 3);
|
||||
|
||||
// Assert
|
||||
result.AverageValue.Should().Be(20.0); // (10 + 20 + 30) / 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_RunWithProfile_HandlesErrors()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var iteration = 0;
|
||||
|
||||
// Act
|
||||
var result = await runner.RunWithProfile(
|
||||
test: () =>
|
||||
{
|
||||
iteration++;
|
||||
if (iteration == 2)
|
||||
{
|
||||
throw new InvalidOperationException("Test error");
|
||||
}
|
||||
return Task.FromResult(new TestResult { Value = 1.0, Success = true });
|
||||
},
|
||||
profile: EnvironmentProfile.Standard,
|
||||
iterations: 3);
|
||||
|
||||
// Assert
|
||||
result.Results.Should().HaveCount(3);
|
||||
result.SuccessRate.Should().BeApproximately(2.0 / 3.0, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_AssertEquivalence_PassesWhenResultsAreEquivalent()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestResult { Value = 100.0, DurationMs = 10 }),
|
||||
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_AssertEquivalence_FailsWhenSkewExceedsTolerance()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var values = new Queue<double>([100.0, 100.0, 100.0, 200.0, 200.0, 200.0]); // 100% difference
|
||||
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestResult { Value = values.Dequeue(), DurationMs = 10 }),
|
||||
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
|
||||
act.Should().Throw<SkewAssertException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_AssertEquivalence_IgnoresSingleProfile()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestResult { Value = 100.0, DurationMs = 10 }),
|
||||
profiles: [EnvironmentProfile.Standard]);
|
||||
|
||||
// Act & Assert - should not throw for single profile
|
||||
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SkewReport Tests
|
||||
|
||||
[Fact]
|
||||
public async Task SkewReport_ToJson_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestResult { Value = 1.0 }),
|
||||
profiles: [EnvironmentProfile.Standard]);
|
||||
|
||||
// Act
|
||||
var json = report.ToJson();
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"generatedAt\"");
|
||||
json.Should().Contain("\"profileCount\"");
|
||||
json.Should().Contain("\"hasSkew\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewReport_ToMarkdown_ProducesValidMarkdown()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestResult { Value = 1.0 }),
|
||||
profiles: [EnvironmentProfile.Standard]);
|
||||
|
||||
// Act
|
||||
var markdown = report.ToMarkdown();
|
||||
|
||||
// Assert
|
||||
markdown.Should().Contain("# Environment Skew Report");
|
||||
markdown.Should().Contain("| Profile |");
|
||||
markdown.Should().Contain("Standard");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TestResult Tests
|
||||
|
||||
[Fact]
|
||||
public void TestResult_Defaults_AreCorrect()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = new TestResult();
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.ProfileName.Should().BeEmpty();
|
||||
result.Metadata.Should().BeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SkewAssertException Tests
|
||||
|
||||
[Fact]
|
||||
public void SkewAssertException_SetsMessage()
|
||||
{
|
||||
// Arrange & Act
|
||||
var ex = new SkewAssertException("Test message");
|
||||
|
||||
// Assert
|
||||
ex.Message.Should().Be("Test message");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
using System.Reflection;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for evidence chain traceability infrastructure.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class EvidenceChainTests
|
||||
{
|
||||
#region RequirementAttribute Tests
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_Constructor_SetsRequirementId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var attr = new RequirementAttribute("REQ-TEST-001");
|
||||
|
||||
// Assert
|
||||
attr.RequirementId.Should().Be("REQ-TEST-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_Constructor_ThrowsOnNullRequirementId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var act = () => new RequirementAttribute(null!);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_OptionalProperties_DefaultToEmpty()
|
||||
{
|
||||
// Arrange & Act
|
||||
var attr = new RequirementAttribute("REQ-TEST-001");
|
||||
|
||||
// Assert
|
||||
attr.SprintTaskId.Should().BeEmpty();
|
||||
attr.ComplianceControl.Should().BeEmpty();
|
||||
attr.SourceDocument.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_GetTraits_ReturnsRequirementTrait()
|
||||
{
|
||||
// Arrange
|
||||
var attr = new RequirementAttribute("REQ-TEST-001");
|
||||
|
||||
// Act
|
||||
var traits = attr.GetTraits();
|
||||
|
||||
// Assert
|
||||
traits.Should().ContainSingle(t => t.Key == "Requirement" && t.Value == "REQ-TEST-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_GetTraits_IncludesSprintTaskWhenSet()
|
||||
{
|
||||
// Arrange
|
||||
var attr = new RequirementAttribute("REQ-TEST-001") { SprintTaskId = "SPRINT-001" };
|
||||
|
||||
// Act
|
||||
var traits = attr.GetTraits();
|
||||
|
||||
// Assert
|
||||
traits.Should().Contain(t => t.Key == "Requirement" && t.Value == "REQ-TEST-001");
|
||||
traits.Should().Contain(t => t.Key == "SprintTask" && t.Value == "SPRINT-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_GetTraits_IncludesComplianceControlWhenSet()
|
||||
{
|
||||
// Arrange
|
||||
var attr = new RequirementAttribute("REQ-TEST-001") { ComplianceControl = "SOC2-CC6.1" };
|
||||
|
||||
// Act
|
||||
var traits = attr.GetTraits();
|
||||
|
||||
// Assert
|
||||
traits.Should().Contain(t => t.Key == "ComplianceControl" && t.Value == "SOC2-CC6.1");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EvidenceChainAssert Tests
|
||||
|
||||
[Fact]
|
||||
public void ComputeSha256_ReturnsLowercaseHex()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test content";
|
||||
|
||||
// Act
|
||||
var hash = EvidenceChainAssert.ComputeSha256(content);
|
||||
|
||||
// Assert
|
||||
hash.Should().NotBeNullOrEmpty();
|
||||
hash.Should().MatchRegex("^[0-9a-f]{64}$");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeSha256_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var content = "deterministic test";
|
||||
|
||||
// Act
|
||||
var hash1 = EvidenceChainAssert.ComputeSha256(content);
|
||||
var hash2 = EvidenceChainAssert.ComputeSha256(content);
|
||||
|
||||
// Assert
|
||||
hash1.Should().Be(hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeSha256_Bytes_MatchesStringVersion()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test content";
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
|
||||
|
||||
// Act
|
||||
var hashFromString = EvidenceChainAssert.ComputeSha256(content);
|
||||
var hashFromBytes = EvidenceChainAssert.ComputeSha256(bytes);
|
||||
|
||||
// Assert
|
||||
hashFromString.Should().Be(hashFromBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactHashStable_PassesWithCorrectHash()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test artifact";
|
||||
var expectedHash = EvidenceChainAssert.ComputeSha256(content);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => EvidenceChainAssert.ArtifactHashStable(content, expectedHash);
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactHashStable_ThrowsWithIncorrectHash()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test artifact";
|
||||
var wrongHash = new string('0', 64);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => EvidenceChainAssert.ArtifactHashStable(content, wrongHash);
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Artifact hash mismatch*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactHashStable_ThrowsOnNullArtifact()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.ArtifactHashStable((byte[])null!, "hash");
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactImmutable_PassesWithDeterministicGenerator()
|
||||
{
|
||||
// Arrange
|
||||
var counter = 0;
|
||||
Func<string> generator = () =>
|
||||
{
|
||||
counter++;
|
||||
return "immutable content";
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var act = () => EvidenceChainAssert.ArtifactImmutable(generator, iterations: 5);
|
||||
act.Should().NotThrow();
|
||||
counter.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactImmutable_ThrowsWithNonDeterministicGenerator()
|
||||
{
|
||||
// Arrange
|
||||
var counter = 0;
|
||||
Func<string> generator = () =>
|
||||
{
|
||||
counter++;
|
||||
return $"non-deterministic content {counter}";
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var act = () => EvidenceChainAssert.ArtifactImmutable(generator, iterations: 5);
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Artifact not immutable*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactImmutable_ThrowsWithLessThanTwoIterations()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.ArtifactImmutable(() => "content", iterations: 1);
|
||||
act.Should().Throw<ArgumentOutOfRangeException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementLinked_PassesWithValidRequirementId()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.RequirementLinked("REQ-TEST-001");
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementLinked_ThrowsWithEmptyRequirementId()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.RequirementLinked("");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*cannot be empty*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementLinked_ThrowsWithWhitespaceRequirementId()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.RequirementLinked(" ");
|
||||
act.Should().Throw<EvidenceTraceabilityException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_PassesWithAllComponents()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete(
|
||||
"REQ-001",
|
||||
"MyTests.TestMethod",
|
||||
"sha256:abc123");
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_ThrowsWithMissingRequirement()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete(
|
||||
"",
|
||||
"MyTests.TestMethod",
|
||||
"sha256:abc123");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Requirement ID is missing*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_ThrowsWithMissingTestId()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete(
|
||||
"REQ-001",
|
||||
null!,
|
||||
"sha256:abc123");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Test ID is missing*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_ThrowsWithMissingArtifactId()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete(
|
||||
"REQ-001",
|
||||
"MyTests.TestMethod",
|
||||
" ");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Artifact ID is missing*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_ReportsAllMissingComponents()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete("", "", "");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Requirement ID is missing*")
|
||||
.WithMessage("*Test ID is missing*")
|
||||
.WithMessage("*Artifact ID is missing*");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EvidenceChainReporter Tests
|
||||
|
||||
[Fact]
|
||||
public void EvidenceChainReporter_GenerateReport_ReturnsEmptyReportForNoAssemblies()
|
||||
{
|
||||
// Arrange
|
||||
var reporter = new EvidenceChainReporter();
|
||||
|
||||
// Act
|
||||
var report = reporter.GenerateReport();
|
||||
|
||||
// Assert
|
||||
report.TotalRequirements.Should().Be(0);
|
||||
report.TotalTests.Should().Be(0);
|
||||
report.AssembliesScanned.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceChainReporter_GenerateReport_ScansAssemblyForRequirements()
|
||||
{
|
||||
// Arrange
|
||||
var reporter = new EvidenceChainReporter();
|
||||
reporter.AddAssembly(typeof(EvidenceChainTests).Assembly);
|
||||
|
||||
// Act
|
||||
var report = reporter.GenerateReport();
|
||||
|
||||
// Assert
|
||||
report.AssembliesScanned.Should().Contain("StellaOps.TestKit.Tests");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceChainReport_ToJson_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var reporter = new EvidenceChainReporter();
|
||||
var report = reporter.GenerateReport();
|
||||
|
||||
// Act
|
||||
var json = report.ToJson();
|
||||
|
||||
// Assert
|
||||
json.Should().NotBeNullOrEmpty();
|
||||
json.Should().Contain("\"totalRequirements\"");
|
||||
json.Should().Contain("\"totalTests\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceChainReport_ToMarkdown_ProducesValidMarkdown()
|
||||
{
|
||||
// Arrange
|
||||
var reporter = new EvidenceChainReporter();
|
||||
var report = reporter.GenerateReport();
|
||||
|
||||
// Act
|
||||
var markdown = report.ToMarkdown();
|
||||
|
||||
// Assert
|
||||
markdown.Should().Contain("# Evidence Chain Traceability Report");
|
||||
markdown.Should().Contain("## Traceability Matrix");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EvidenceTraceabilityException Tests
|
||||
|
||||
[Fact]
|
||||
public void EvidenceTraceabilityException_ConstructorWithMessage_SetsMessage()
|
||||
{
|
||||
// Arrange & Act
|
||||
var ex = new EvidenceTraceabilityException("Test error");
|
||||
|
||||
// Assert
|
||||
ex.Message.Should().Be("Test error");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceTraceabilityException_ConstructorWithInnerException_SetsInnerException()
|
||||
{
|
||||
// Arrange
|
||||
var inner = new InvalidOperationException("Inner");
|
||||
|
||||
// Act
|
||||
var ex = new EvidenceTraceabilityException("Outer", inner);
|
||||
|
||||
// Assert
|
||||
ex.Message.Should().Be("Outer");
|
||||
ex.InnerException.Should().Be(inner);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test class with [Requirement] attribute for reporter testing.
|
||||
/// </summary>
|
||||
[Requirement("REQ-REPORTER-TEST-001")]
|
||||
public sealed class RequirementTestFixture
|
||||
{
|
||||
[Fact]
|
||||
[Requirement("REQ-REPORTER-TEST-002", SprintTaskId = "TEST-001")]
|
||||
public void SampleTestWithRequirement()
|
||||
{
|
||||
// This test exists to verify the reporter can scan for [Requirement] attributes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Incident;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for post-incident test generation infrastructure.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class IncidentTestGeneratorTests
|
||||
{
|
||||
private static readonly string SampleManifestJson = """
|
||||
{
|
||||
"schemaVersion": "2.0",
|
||||
"scan": {
|
||||
"id": "scan-001",
|
||||
"time": "2026-01-15T10:30:00Z",
|
||||
"policyDigest": "sha256:abc123",
|
||||
"scorePolicyDigest": "sha256:def456"
|
||||
},
|
||||
"reachability": {
|
||||
"analysisId": "analysis-001",
|
||||
"graphs": [
|
||||
{
|
||||
"kind": "static",
|
||||
"hash": "sha256:graph123",
|
||||
"analyzer": "java-callgraph"
|
||||
}
|
||||
],
|
||||
"runtimeTraces": []
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private static readonly IncidentMetadata SampleMetadata = new()
|
||||
{
|
||||
IncidentId = "INC-2026-001",
|
||||
OccurredAt = DateTimeOffset.Parse("2026-01-15T10:30:00Z"),
|
||||
RootCause = "Race condition in concurrent writes",
|
||||
AffectedModules = ["EvidenceLocker", "Policy"],
|
||||
Severity = IncidentSeverity.P1,
|
||||
Title = "Evidence bundle duplication"
|
||||
};
|
||||
|
||||
#region IncidentMetadata Tests
|
||||
|
||||
[Fact]
|
||||
public void IncidentMetadata_RequiredProperties_AreSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
var metadata = new IncidentMetadata
|
||||
{
|
||||
IncidentId = "INC-001",
|
||||
OccurredAt = DateTimeOffset.UtcNow,
|
||||
RootCause = "Test cause",
|
||||
AffectedModules = ["Module1"],
|
||||
Severity = IncidentSeverity.P2
|
||||
};
|
||||
|
||||
// Assert
|
||||
metadata.IncidentId.Should().Be("INC-001");
|
||||
metadata.RootCause.Should().Be("Test cause");
|
||||
metadata.Severity.Should().Be(IncidentSeverity.P2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncidentMetadata_OptionalProperties_HaveDefaults()
|
||||
{
|
||||
// Arrange & Act
|
||||
var metadata = new IncidentMetadata
|
||||
{
|
||||
IncidentId = "INC-001",
|
||||
OccurredAt = DateTimeOffset.UtcNow,
|
||||
RootCause = "Test",
|
||||
AffectedModules = ["Module1"],
|
||||
Severity = IncidentSeverity.P3
|
||||
};
|
||||
|
||||
// Assert
|
||||
metadata.Title.Should().BeEmpty();
|
||||
metadata.ReportUrl.Should().BeEmpty();
|
||||
metadata.ResolvedAt.Should().BeNull();
|
||||
metadata.CorrelationIds.Should().BeEmpty();
|
||||
metadata.FixTaskId.Should().BeEmpty();
|
||||
metadata.Tags.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncidentSeverity_P1_HasCorrectValue()
|
||||
{
|
||||
// Assert
|
||||
((int)IncidentSeverity.P1).Should().Be(1);
|
||||
((int)IncidentSeverity.P2).Should().Be(2);
|
||||
((int)IncidentSeverity.P3).Should().Be(3);
|
||||
((int)IncidentSeverity.P4).Should().Be(4);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IncidentTestGenerator Tests
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_CreatesValidScaffold()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Assert
|
||||
scaffold.Should().NotBeNull();
|
||||
scaffold.Metadata.Should().Be(SampleMetadata);
|
||||
scaffold.TestClassName.Should().Contain("INC_2026_001");
|
||||
scaffold.TestMethodName.Should().Contain("Validates");
|
||||
scaffold.ReplayManifestHash.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_ExtractsInputFixtures()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Assert
|
||||
scaffold.InputFixtures.Should().ContainKey("scan");
|
||||
scaffold.InputFixtures.Should().ContainKey("reachabilityGraphs");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_ExtractsExpectedOutputs()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Assert
|
||||
scaffold.ExpectedOutputs.Should().ContainKey("policyDigest");
|
||||
scaffold.ExpectedOutputs["policyDigest"].Should().Be("sha256:abc123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_GeneratesImplementationNotes()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Assert
|
||||
scaffold.ImplementationNotes.Should().NotBeEmpty();
|
||||
scaffold.ImplementationNotes.Should().Contain(n => n.Contains("INC-2026-001"));
|
||||
scaffold.ImplementationNotes.Should().Contain(n => n.Contains("Race condition"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_SetsNamespaceFromModule()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Assert
|
||||
scaffold.Namespace.Should().Contain("EvidenceLocker");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_ThrowsOnNullManifest()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => generator.GenerateFromManifestJson(null!, SampleMetadata);
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_ThrowsOnNullMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => generator.GenerateFromManifestJson(SampleManifestJson, null!);
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RegisterIncidentTest Tests
|
||||
|
||||
[Fact]
|
||||
public void RegisterIncidentTest_AddsToRegistry()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Act
|
||||
generator.RegisterIncidentTest("INC-2026-001", scaffold);
|
||||
|
||||
// Assert
|
||||
generator.RegisteredTests.Should().ContainKey("INC-2026-001");
|
||||
generator.RegisteredTests["INC-2026-001"].Should().Be(scaffold);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterIncidentTest_OverwritesExisting()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold1 = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
var scaffold2 = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata with { Title = "Updated" });
|
||||
|
||||
// Act
|
||||
generator.RegisterIncidentTest("INC-2026-001", scaffold1);
|
||||
generator.RegisterIncidentTest("INC-2026-001", scaffold2);
|
||||
|
||||
// Assert
|
||||
generator.RegisteredTests["INC-2026-001"].Metadata.Title.Should().Be("Updated");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GenerateReport Tests
|
||||
|
||||
[Fact]
|
||||
public void GenerateReport_ReturnsEmptyForNoTests()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act
|
||||
var report = generator.GenerateReport();
|
||||
|
||||
// Assert
|
||||
report.TotalTests.Should().Be(0);
|
||||
report.Tests.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateReport_CountsBySeverity()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var p1Metadata = SampleMetadata with { IncidentId = "INC-001", Severity = IncidentSeverity.P1 };
|
||||
var p2Metadata = SampleMetadata with { IncidentId = "INC-002", Severity = IncidentSeverity.P2 };
|
||||
|
||||
generator.RegisterIncidentTest("INC-001", generator.GenerateFromManifestJson(SampleManifestJson, p1Metadata));
|
||||
generator.RegisterIncidentTest("INC-002", generator.GenerateFromManifestJson(SampleManifestJson, p2Metadata));
|
||||
|
||||
// Act
|
||||
var report = generator.GenerateReport();
|
||||
|
||||
// Assert
|
||||
report.TotalTests.Should().Be(2);
|
||||
report.BySeveority.Should().ContainKey(IncidentSeverity.P1);
|
||||
report.BySeveority.Should().ContainKey(IncidentSeverity.P2);
|
||||
report.BySeveority[IncidentSeverity.P1].Should().Be(1);
|
||||
report.BySeveority[IncidentSeverity.P2].Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateReport_CountsByModule()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
generator.RegisterIncidentTest("INC-001", generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata));
|
||||
|
||||
// Act
|
||||
var report = generator.GenerateReport();
|
||||
|
||||
// Assert
|
||||
report.ByModule.Should().ContainKey("EvidenceLocker");
|
||||
report.ByModule.Should().ContainKey("Policy");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TestScaffold Tests
|
||||
|
||||
[Fact]
|
||||
public void TestScaffold_GenerateTestCode_ProducesValidCSharp()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Act
|
||||
var code = scaffold.GenerateTestCode();
|
||||
|
||||
// Assert
|
||||
code.Should().Contain("namespace StellaOps.EvidenceLocker.Tests.PostIncident");
|
||||
code.Should().Contain($"public sealed class {scaffold.TestClassName}");
|
||||
code.Should().Contain("[Fact]");
|
||||
code.Should().Contain("[Trait(\"Category\", TestCategories.PostIncident)]");
|
||||
code.Should().Contain($"[Trait(\"Incident\", \"{SampleMetadata.IncidentId}\")]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestScaffold_GenerateTestCode_IncludesIncidentMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Act
|
||||
var code = scaffold.GenerateTestCode();
|
||||
|
||||
// Assert
|
||||
code.Should().Contain("INC-2026-001");
|
||||
code.Should().Contain("Race condition in concurrent writes");
|
||||
code.Should().Contain("IncidentSeverity.P1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestScaffold_ToJson_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Act
|
||||
var json = scaffold.ToJson();
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"incidentId\"");
|
||||
json.Should().Contain("\"testClassName\"");
|
||||
json.Should().Contain("\"inputFixtures\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestScaffold_FromJson_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var original = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
var json = original.ToJson();
|
||||
|
||||
// Act
|
||||
var deserialized = TestScaffold.FromJson(json);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.Metadata.IncidentId.Should().Be(original.Metadata.IncidentId);
|
||||
deserialized.TestClassName.Should().Be(original.TestClassName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Analysis;
|
||||
using StellaOps.TestKit.Traits;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="IntentCoverageReportGenerator"/> and <see cref="IntentCoverageReport"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class IntentCoverageReportTests
|
||||
{
|
||||
[Fact]
|
||||
public void TestIntents_All_ContainsAllCategories()
|
||||
{
|
||||
TestIntents.All.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
TestIntents.Regulatory,
|
||||
TestIntents.Safety,
|
||||
TestIntents.Performance,
|
||||
TestIntents.Competitive,
|
||||
TestIntents.Operational
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestIntents_IsValid_ValidatesKnownIntents()
|
||||
{
|
||||
TestIntents.IsValid("Regulatory").Should().BeTrue();
|
||||
TestIntents.IsValid("Safety").Should().BeTrue();
|
||||
TestIntents.IsValid("Performance").Should().BeTrue();
|
||||
TestIntents.IsValid("Competitive").Should().BeTrue();
|
||||
TestIntents.IsValid("Operational").Should().BeTrue();
|
||||
|
||||
// Case insensitive
|
||||
TestIntents.IsValid("regulatory").Should().BeTrue();
|
||||
TestIntents.IsValid("SAFETY").Should().BeTrue();
|
||||
|
||||
// Invalid
|
||||
TestIntents.IsValid("Unknown").Should().BeFalse();
|
||||
TestIntents.IsValid("").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentAttribute_CreatesTraits()
|
||||
{
|
||||
var attr = new IntentAttribute(TestIntents.Safety, "Security requirement");
|
||||
|
||||
attr.Intent.Should().Be(TestIntents.Safety);
|
||||
attr.Rationale.Should().Be("Security requirement");
|
||||
|
||||
var traits = attr.GetTraits();
|
||||
traits.Should().Contain(new KeyValuePair<string, string>("Intent", "Safety"));
|
||||
traits.Should().Contain(new KeyValuePair<string, string>("IntentRationale", "Security requirement"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentAttribute_WithoutRationale_OnlyIntentTrait()
|
||||
{
|
||||
var attr = new IntentAttribute(TestIntents.Operational);
|
||||
|
||||
attr.Intent.Should().Be(TestIntents.Operational);
|
||||
attr.Rationale.Should().BeEmpty();
|
||||
|
||||
var traits = attr.GetTraits();
|
||||
traits.Should().ContainSingle();
|
||||
traits.Should().Contain(new KeyValuePair<string, string>("Intent", "Operational"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentCoverageReportGenerator_EmptyAssemblies_ReturnsEmptyReport()
|
||||
{
|
||||
var generator = new IntentCoverageReportGenerator();
|
||||
|
||||
var report = generator.Generate();
|
||||
|
||||
report.TotalTests.Should().Be(0);
|
||||
report.TaggedTests.Should().Be(0);
|
||||
report.UntaggedTests.Should().Be(0);
|
||||
report.TagCoveragePercent.Should().Be(0);
|
||||
report.ModuleStats.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentCoverageReportGenerator_ScansSelfAssembly()
|
||||
{
|
||||
var generator = new IntentCoverageReportGenerator();
|
||||
generator.AddAssembly(typeof(IntentCoverageReportTests).Assembly);
|
||||
|
||||
var report = generator.Generate();
|
||||
|
||||
// This test class has tests, so we should find something
|
||||
report.TotalTests.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentCoverageReport_ToMarkdown_GeneratesValidOutput()
|
||||
{
|
||||
var report = new IntentCoverageReport
|
||||
{
|
||||
GeneratedAt = new DateTimeOffset(2026, 1, 27, 12, 0, 0, TimeSpan.Zero),
|
||||
TotalTests = 100,
|
||||
TaggedTests = 60,
|
||||
UntaggedTests = 40,
|
||||
TagCoveragePercent = 60.0,
|
||||
IntentDistribution = new Dictionary<string, int>
|
||||
{
|
||||
[TestIntents.Safety] = 20,
|
||||
[TestIntents.Regulatory] = 15,
|
||||
[TestIntents.Operational] = 25,
|
||||
[TestIntents.Performance] = 0,
|
||||
[TestIntents.Competitive] = 0
|
||||
},
|
||||
ModuleStats = new Dictionary<string, ModuleIntentStatsReadOnly>
|
||||
{
|
||||
["Policy"] = new ModuleIntentStatsReadOnly
|
||||
{
|
||||
ModuleName = "Policy",
|
||||
TotalTests = 50,
|
||||
TaggedTests = 30,
|
||||
TestsWithRationale = 10,
|
||||
TagCoveragePercent = 60.0,
|
||||
IntentCounts = new Dictionary<string, int>
|
||||
{
|
||||
[TestIntents.Safety] = 15,
|
||||
[TestIntents.Regulatory] = 15
|
||||
}
|
||||
}
|
||||
},
|
||||
Warnings = new List<string>
|
||||
{
|
||||
"Low intent coverage: only 60.0% of tests have intent tags"
|
||||
}
|
||||
};
|
||||
|
||||
var markdown = report.ToMarkdown();
|
||||
|
||||
markdown.Should().Contain("# Intent Coverage Report");
|
||||
markdown.Should().Contain("Total tests: 100");
|
||||
markdown.Should().Contain("Tagged: 60 (60.0%)");
|
||||
markdown.Should().Contain("## Intent Distribution");
|
||||
markdown.Should().Contain("| Safety |");
|
||||
markdown.Should().Contain("## Per-Module Coverage");
|
||||
markdown.Should().Contain("| Policy |");
|
||||
markdown.Should().Contain("## Warnings");
|
||||
markdown.Should().Contain("Low intent coverage");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentCoverageReport_GeneratesWarning_WhenSafetyMissing()
|
||||
{
|
||||
var generator = new IntentCoverageReportGenerator();
|
||||
// Empty assemblies means no Safety tests
|
||||
var report = generator.Generate();
|
||||
|
||||
report.Warnings.Should().Contain("No tests tagged with Safety intent");
|
||||
}
|
||||
}
|
||||
360
src/__Libraries/__Tests/StellaOps.TestKit.Tests/InteropTests.cs
Normal file
360
src/__Libraries/__Tests/StellaOps.TestKit.Tests/InteropTests.cs
Normal file
@@ -0,0 +1,360 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Interop;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for cross-version interoperability testing infrastructure.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class InteropTests
|
||||
{
|
||||
#region SchemaVersionMatrix Tests
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_AddVersion_StoresSchema()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
var schema = new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
};
|
||||
|
||||
// Act
|
||||
matrix.AddVersion("1.0", schema);
|
||||
|
||||
// Assert
|
||||
matrix.Versions.Should().Contain("1.0");
|
||||
matrix.GetVersion("1.0").Should().Be(schema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_IsBackwardCompatible_ReturnsTrueWhenNoFieldsRemoved()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name", "type"], // Added field, none removed
|
||||
OptionalFields = ["description"]
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
matrix.IsBackwardCompatible("1.0", "2.0").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_IsBackwardCompatible_ReturnsFalseWhenFieldsRemoved()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name", "oldField"]
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"] // oldField removed
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
matrix.IsBackwardCompatible("1.0", "2.0").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_IsForwardCompatible_ReturnsTrueWhenNewFieldsHaveDefaults()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name", "type"],
|
||||
FieldDefaults = new() { ["type"] = "default" }
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
matrix.IsForwardCompatible("1.0", "2.0").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_IsForwardCompatible_ReturnsFalseWhenNewRequiredFieldsHaveNoDefaults()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name", "type"] // No default for "type"
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
matrix.IsForwardCompatible("1.0", "2.0").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_Analyze_GeneratesReport()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
matrix.AddVersion("2.0", new SchemaDefinition { RequiredFields = ["id", "name"] });
|
||||
|
||||
// Act
|
||||
var report = matrix.Analyze();
|
||||
|
||||
// Assert
|
||||
report.Versions.Should().Contain(["1.0", "2.0"]);
|
||||
report.Pairs.Should().HaveCount(2); // 1.0->2.0 and 2.0->1.0
|
||||
report.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_Analyze_DetectsTypeChanges()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id"],
|
||||
FieldTypes = new() { ["id"] = "int" }
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id"],
|
||||
FieldTypes = new() { ["id"] = "string" } // Type changed
|
||||
});
|
||||
|
||||
// Act
|
||||
var report = matrix.Analyze();
|
||||
|
||||
// Assert
|
||||
var pair = report.Pairs.First(p => p.FromVersion == "1.0" && p.ToVersion == "2.0");
|
||||
pair.IsBackwardCompatible.Should().BeFalse();
|
||||
pair.BackwardIssues.Should().Contain(i => i.Contains("Type changed"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompatibilityReport_ToMarkdown_ProducesValidMarkdown()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
matrix.AddVersion("2.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
var report = matrix.Analyze();
|
||||
|
||||
// Act
|
||||
var markdown = report.ToMarkdown();
|
||||
|
||||
// Assert
|
||||
markdown.Should().Contain("# Schema Compatibility Report");
|
||||
markdown.Should().Contain("| From | To |");
|
||||
markdown.Should().Contain("1.0");
|
||||
markdown.Should().Contain("2.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompatibilityReport_ToJson_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
var report = matrix.Analyze();
|
||||
|
||||
// Act
|
||||
var json = report.ToJson();
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"generatedAt\"");
|
||||
json.Should().Contain("\"versions\"");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region VersionCompatibilityFixture Tests
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_Initialize_CreatesCurrentEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture
|
||||
{
|
||||
Config = new VersionCompatibilityConfig { CurrentVersion = "3.0" }
|
||||
};
|
||||
|
||||
// Act
|
||||
await fixture.InitializeAsync();
|
||||
|
||||
// Assert
|
||||
fixture.CurrentEndpoint.Should().NotBeNull();
|
||||
fixture.CurrentEndpoint!.Version.Should().Be("3.0");
|
||||
fixture.CurrentEndpoint.IsHealthy.Should().BeTrue();
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_StartVersion_CreatesEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
|
||||
// Act
|
||||
var endpoint = await fixture.StartVersion("1.0", "EvidenceLocker");
|
||||
|
||||
// Assert
|
||||
endpoint.Should().NotBeNull();
|
||||
endpoint.Version.Should().Be("1.0");
|
||||
endpoint.ServiceName.Should().Be("EvidenceLocker");
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_StartVersion_ReturnsSameEndpointForSameVersion()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
|
||||
// Act
|
||||
var endpoint1 = await fixture.StartVersion("1.0", "Service");
|
||||
var endpoint2 = await fixture.StartVersion("1.0", "Service");
|
||||
|
||||
// Assert
|
||||
endpoint1.Should().BeSameAs(endpoint2);
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_TestHandshake_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
var server = await fixture.StartVersion("1.0", "Service");
|
||||
|
||||
// Act
|
||||
var result = await fixture.TestHandshake(fixture.CurrentEndpoint!, server);
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.ClientVersion.Should().Be(fixture.CurrentEndpoint!.Version);
|
||||
result.ServerVersion.Should().Be("1.0");
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_TestMessageFormat_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
var producer = await fixture.StartVersion("1.0", "Producer");
|
||||
var consumer = await fixture.StartVersion("2.0", "Consumer");
|
||||
|
||||
// Act
|
||||
var result = await fixture.TestMessageFormat(producer, consumer, "EvidenceBundle");
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Message.Should().Contain("EvidenceBundle");
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_TestSchemaMigration_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
|
||||
// Act
|
||||
var result = await fixture.TestSchemaMigration("1.0", "2.0", new { id = 1 });
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.FromVersion.Should().Be("1.0");
|
||||
result.ToVersion.Should().Be("2.0");
|
||||
result.DataPreserved.Should().BeTrue();
|
||||
result.RollbackSupported.Should().BeTrue();
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_StopVersion_RemovesEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
await fixture.StartVersion("1.0", "Service");
|
||||
|
||||
// Act
|
||||
await fixture.StopVersion("1.0", "Service");
|
||||
var newEndpoint = await fixture.StartVersion("1.0", "Service");
|
||||
|
||||
// Assert - new endpoint should be created (different base URL due to increment)
|
||||
newEndpoint.Should().NotBeNull();
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ServiceEndpoint Tests
|
||||
|
||||
[Fact]
|
||||
public void ServiceEndpoint_DefaultValues_AreSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
var endpoint = new ServiceEndpoint();
|
||||
|
||||
// Assert
|
||||
endpoint.ServiceName.Should().BeEmpty();
|
||||
endpoint.Version.Should().BeEmpty();
|
||||
endpoint.BaseUrl.Should().BeEmpty();
|
||||
endpoint.IsHealthy.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CompatibilityResult Tests
|
||||
|
||||
[Fact]
|
||||
public void CompatibilityResult_DefaultValues_AreSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = new CompatibilityResult();
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeFalse();
|
||||
result.Errors.Should().BeEmpty();
|
||||
result.Warnings.Should().BeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Longevity;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for time-extended stability testing infrastructure.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class LongevityTests
|
||||
{
|
||||
#region StabilityMetrics Tests
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_CaptureBaseline_SetsBaseline()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
|
||||
// Act
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
// Assert
|
||||
metrics.MemoryBaseline.Should().BeGreaterThan(0);
|
||||
metrics.Snapshots.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_CaptureSnapshot_AddsSnapshot()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
// Act
|
||||
metrics.CaptureSnapshot();
|
||||
metrics.CaptureSnapshot();
|
||||
|
||||
// Assert
|
||||
metrics.Snapshots.Should().HaveCount(3); // Baseline + 2 snapshots
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_RecordCounter_StoresValue()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
// Act
|
||||
metrics.RecordCounter("requests_total", 100);
|
||||
|
||||
// Assert
|
||||
metrics.CounterValues.Should().ContainKey("requests_total");
|
||||
metrics.CounterValues["requests_total"].Should().Be(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_RecordConnectionPool_StoresValues()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
// Act
|
||||
metrics.RecordConnectionPool(active: 5, leaked: 1);
|
||||
|
||||
// Assert
|
||||
metrics.ConnectionPoolActive.Should().Be(5);
|
||||
metrics.ConnectionPoolLeaked.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_HasMemoryLeak_ReturnsFalseInitially()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
// Act & Assert
|
||||
metrics.HasMemoryLeak().Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_HasConnectionPoolLeak_DetectsLeaks()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
metrics.RecordConnectionPool(active: 10, leaked: 2);
|
||||
|
||||
// Act & Assert
|
||||
metrics.HasConnectionPoolLeak(maxLeaked: 0).Should().BeTrue();
|
||||
metrics.HasConnectionPoolLeak(maxLeaked: 2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_HasDrift_DetectsDriftingCounters()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
metrics.RecordCounter("counter", 100);
|
||||
metrics.CaptureSnapshot();
|
||||
metrics.RecordCounter("counter", 2000);
|
||||
|
||||
// Act & Assert
|
||||
metrics.HasDrift("counter", threshold: 1000).Should().BeTrue();
|
||||
metrics.HasDrift("counter", threshold: 5000).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_MemoryGrowthRate_CalculatesSlope()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
// Capture multiple snapshots (growth rate requires at least 2)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
metrics.CaptureSnapshot();
|
||||
}
|
||||
|
||||
// Act
|
||||
var growthRate = metrics.MemoryGrowthRate;
|
||||
|
||||
// Assert - just verify it's calculated
|
||||
growthRate.Should().BeOfType<double>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_GenerateReport_CreatesValidReport()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
metrics.CaptureSnapshot();
|
||||
metrics.RecordCounter("test", 42);
|
||||
|
||||
// Act
|
||||
var report = metrics.GenerateReport();
|
||||
|
||||
// Assert
|
||||
report.Should().NotBeNull();
|
||||
report.SnapshotCount.Should().Be(2);
|
||||
report.BaselineMemory.Should().BeGreaterThan(0);
|
||||
report.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityReport_ToJson_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
var report = metrics.GenerateReport();
|
||||
|
||||
// Act
|
||||
var json = report.ToJson();
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"snapshotCount\"");
|
||||
json.Should().Contain("\"baselineMemory\"");
|
||||
json.Should().Contain("\"hasMemoryLeak\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityReport_Passed_ReturnsTrueWhenNoIssues()
|
||||
{
|
||||
// Arrange
|
||||
var report = new StabilityReport
|
||||
{
|
||||
HasMemoryLeak = false,
|
||||
HasConnectionPoolLeak = false,
|
||||
DriftingCounters = []
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
report.Passed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityReport_Passed_ReturnsFalseWhenMemoryLeak()
|
||||
{
|
||||
// Arrange
|
||||
var report = new StabilityReport
|
||||
{
|
||||
HasMemoryLeak = true,
|
||||
HasConnectionPoolLeak = false,
|
||||
DriftingCounters = []
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
report.Passed.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StabilityTestRunner Tests
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunIterations_ExecutesScenario()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig { SnapshotInterval = 5 }
|
||||
};
|
||||
var executionCount = 0;
|
||||
|
||||
// Act
|
||||
var report = await runner.RunIterations(
|
||||
scenario: () =>
|
||||
{
|
||||
executionCount++;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
iterations: 10);
|
||||
|
||||
// Assert
|
||||
executionCount.Should().Be(10);
|
||||
report.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunIterations_CapturesSnapshots()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig { SnapshotInterval = 2 }
|
||||
};
|
||||
|
||||
// Act
|
||||
var report = await runner.RunIterations(
|
||||
scenario: () => Task.CompletedTask,
|
||||
iterations: 10);
|
||||
|
||||
// Assert
|
||||
report.SnapshotCount.Should().BeGreaterThan(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunIterations_StopsOnErrorIfConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig
|
||||
{
|
||||
StopOnError = true,
|
||||
SnapshotInterval = 1
|
||||
}
|
||||
};
|
||||
var executionCount = 0;
|
||||
|
||||
// Act
|
||||
var report = await runner.RunIterations(
|
||||
scenario: () =>
|
||||
{
|
||||
executionCount++;
|
||||
if (executionCount == 5)
|
||||
{
|
||||
throw new InvalidOperationException("Test error");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
iterations: 100);
|
||||
|
||||
// Assert
|
||||
executionCount.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunIterations_ContinuesOnErrorIfNotConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig
|
||||
{
|
||||
StopOnError = false,
|
||||
SnapshotInterval = 10
|
||||
}
|
||||
};
|
||||
var executionCount = 0;
|
||||
var errorCount = 0;
|
||||
|
||||
// Act
|
||||
var report = await runner.RunIterations(
|
||||
scenario: () =>
|
||||
{
|
||||
executionCount++;
|
||||
if (executionCount % 3 == 0)
|
||||
{
|
||||
errorCount++;
|
||||
throw new InvalidOperationException("Test error");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
iterations: 10);
|
||||
|
||||
// Assert
|
||||
executionCount.Should().Be(10);
|
||||
errorCount.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunExtended_RunsForDuration()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig { SnapshotInterval = 100 }
|
||||
};
|
||||
var executionCount = 0;
|
||||
|
||||
// Act
|
||||
var report = await runner.RunExtended(
|
||||
scenario: () =>
|
||||
{
|
||||
executionCount++;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
duration: TimeSpan.FromMilliseconds(100));
|
||||
|
||||
// Assert
|
||||
executionCount.Should().BeGreaterThan(0);
|
||||
report.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunExtended_RespectsCancellation()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var executionCount = 0;
|
||||
|
||||
// Act
|
||||
cts.CancelAfter(50);
|
||||
var report = await runner.RunExtended(
|
||||
scenario: async () =>
|
||||
{
|
||||
executionCount++;
|
||||
await Task.Delay(10);
|
||||
},
|
||||
duration: TimeSpan.FromHours(1),
|
||||
cancellationToken: cts.Token);
|
||||
|
||||
// Assert
|
||||
executionCount.Should().BeLessThan(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityTestRunner_Metrics_ExposesUnderlyingMetrics()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner();
|
||||
|
||||
// Act & Assert
|
||||
runner.Metrics.Should().NotBeNull();
|
||||
runner.Metrics.Should().BeOfType<StabilityMetrics>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StabilityTestConfig Tests
|
||||
|
||||
[Fact]
|
||||
public void StabilityTestConfig_Defaults_AreReasonable()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new StabilityTestConfig();
|
||||
|
||||
// Assert
|
||||
config.SnapshotInterval.Should().Be(100);
|
||||
config.MemoryLeakThresholdPercent.Should().Be(10);
|
||||
config.MaxConnectionPoolLeaks.Should().Be(0);
|
||||
config.StopOnError.Should().BeFalse();
|
||||
config.IterationDelay.Should().Be(TimeSpan.Zero);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.TestKit.Observability;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for observability contract assertions.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class ObservabilityContractTests
|
||||
{
|
||||
#region OTelContractAssert Tests
|
||||
|
||||
[Fact]
|
||||
public void HasRequiredSpans_AllPresent_NoException()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource");
|
||||
using var capture = new OtelCapture("TestSource");
|
||||
|
||||
using (source.StartActivity("Span1")) { }
|
||||
using (source.StartActivity("Span2")) { }
|
||||
|
||||
var act = () => OTelContractAssert.HasRequiredSpans(capture, "Span1", "Span2");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasRequiredSpans_Missing_ThrowsContractViolation()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource2");
|
||||
using var capture = new OtelCapture("TestSource2");
|
||||
|
||||
using (source.StartActivity("Span1")) { }
|
||||
|
||||
var act = () => OTelContractAssert.HasRequiredSpans(capture, "Span1", "MissingSpan");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*MissingSpan*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanHasAttributes_AllPresent_NoException()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource3");
|
||||
using var capture = new OtelCapture("TestSource3");
|
||||
|
||||
using (var activity = source.StartActivity("TestSpan"))
|
||||
{
|
||||
activity?.SetTag("user_id", "123");
|
||||
activity?.SetTag("tenant_id", "acme");
|
||||
}
|
||||
|
||||
var span = capture.CapturedActivities.First();
|
||||
var act = () => OTelContractAssert.SpanHasAttributes(span, "user_id", "tenant_id");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanHasAttributes_Missing_ThrowsContractViolation()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource4");
|
||||
using var capture = new OtelCapture("TestSource4");
|
||||
|
||||
using (var activity = source.StartActivity("TestSpan"))
|
||||
{
|
||||
activity?.SetTag("user_id", "123");
|
||||
}
|
||||
|
||||
var span = capture.CapturedActivities.First();
|
||||
var act = () => OTelContractAssert.SpanHasAttributes(span, "user_id", "missing_attr");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*missing_attr*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeCardinality_WithinThreshold_NoException()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource5");
|
||||
using var capture = new OtelCapture("TestSource5");
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
using (var activity = source.StartActivity($"Span{i}"))
|
||||
{
|
||||
activity?.SetTag("status", i % 3 == 0 ? "ok" : "error"); // 2 unique values
|
||||
}
|
||||
}
|
||||
|
||||
var act = () => OTelContractAssert.AttributeCardinality(capture, "status", maxCardinality: 10);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeCardinality_ExceedsThreshold_ThrowsContractViolation()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource6");
|
||||
using var capture = new OtelCapture("TestSource6");
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
using (var activity = source.StartActivity($"Span{i}"))
|
||||
{
|
||||
activity?.SetTag("request_id", $"id-{i}"); // 10 unique values
|
||||
}
|
||||
}
|
||||
|
||||
var act = () => OTelContractAssert.AttributeCardinality(capture, "request_id", maxCardinality: 5);
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*cardinality*exceeds*");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region LogContractAssert Tests
|
||||
|
||||
[Fact]
|
||||
public void HasRequiredFields_AllPresent_NoException()
|
||||
{
|
||||
var record = new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Information,
|
||||
Message = "Test message",
|
||||
StateValues = new Dictionary<string, object?>
|
||||
{
|
||||
["CorrelationId"] = "abc-123",
|
||||
["TenantId"] = "acme"
|
||||
}
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.HasRequiredFields(record, "CorrelationId", "TenantId");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasRequiredFields_Missing_ThrowsContractViolation()
|
||||
{
|
||||
var record = new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Information,
|
||||
Message = "Test message",
|
||||
StateValues = new Dictionary<string, object?>
|
||||
{
|
||||
["CorrelationId"] = "abc-123"
|
||||
}
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.HasRequiredFields(record, "CorrelationId", "MissingField");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*MissingField*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoSensitiveData_Clean_NoException()
|
||||
{
|
||||
var records = new[]
|
||||
{
|
||||
new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Information,
|
||||
Message = "User logged in successfully",
|
||||
StateValues = new Dictionary<string, object?>
|
||||
{
|
||||
["UserId"] = "user-123"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var piiPatterns = new[] { new Regex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b") };
|
||||
|
||||
var act = () => LogContractAssert.NoSensitiveData(records, piiPatterns);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoSensitiveData_ContainsEmail_ThrowsContractViolation()
|
||||
{
|
||||
var records = new[]
|
||||
{
|
||||
new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Information,
|
||||
Message = "User test@example.com logged in",
|
||||
StateValues = new Dictionary<string, object?>()
|
||||
}
|
||||
};
|
||||
|
||||
var piiPatterns = new[] { new Regex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b") };
|
||||
|
||||
var act = () => LogContractAssert.NoSensitiveData(records, piiPatterns);
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*PII*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogLevelAppropriate_WithinRange_NoException()
|
||||
{
|
||||
var record = new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Warning,
|
||||
Message = "Test warning"
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.LogLevelAppropriate(record, LogLevel.Information, LogLevel.Error);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogLevelAppropriate_OutsideRange_ThrowsContractViolation()
|
||||
{
|
||||
var record = new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Critical,
|
||||
Message = "Critical error"
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.LogLevelAppropriate(record, LogLevel.Information, LogLevel.Warning);
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*Critical*outside*range*");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MetricsContractAssert Tests
|
||||
|
||||
[Fact]
|
||||
public void MetricExists_Present_NoException()
|
||||
{
|
||||
using var meter = new Meter("TestMeter1");
|
||||
using var capture = new MetricsCapture("TestMeter1");
|
||||
|
||||
var counter = meter.CreateCounter<long>("test_requests_total");
|
||||
counter.Add(1);
|
||||
|
||||
var act = () => MetricsContractAssert.MetricExists(capture, "test_requests_total");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MetricExists_Missing_ThrowsContractViolation()
|
||||
{
|
||||
using var meter = new Meter("TestMeter2");
|
||||
using var capture = new MetricsCapture("TestMeter2");
|
||||
|
||||
var counter = meter.CreateCounter<long>("some_other_metric");
|
||||
counter.Add(1);
|
||||
|
||||
var act = () => MetricsContractAssert.MetricExists(capture, "missing_metric");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*missing_metric*not found*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LabelCardinalityBounded_WithinThreshold_NoException()
|
||||
{
|
||||
using var meter = new Meter("TestMeter3");
|
||||
using var capture = new MetricsCapture("TestMeter3");
|
||||
|
||||
var counter = meter.CreateCounter<long>("http_requests_total");
|
||||
counter.Add(1, new KeyValuePair<string, object?>("method", "GET"));
|
||||
counter.Add(1, new KeyValuePair<string, object?>("method", "POST"));
|
||||
|
||||
var act = () => MetricsContractAssert.LabelCardinalityBounded(capture, "http_requests_total", maxLabels: 10);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LabelCardinalityBounded_ExceedsThreshold_ThrowsContractViolation()
|
||||
{
|
||||
using var meter = new Meter("TestMeter4");
|
||||
using var capture = new MetricsCapture("TestMeter4");
|
||||
|
||||
var counter = meter.CreateCounter<long>("requests_by_user");
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
counter.Add(1, new KeyValuePair<string, object?>("user_id", $"user-{i}"));
|
||||
}
|
||||
|
||||
var act = () => MetricsContractAssert.LabelCardinalityBounded(capture, "requests_by_user", maxLabels: 5);
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*cardinality*exceeds*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CounterMonotonic_AlwaysIncreasing_NoException()
|
||||
{
|
||||
using var meter = new Meter("TestMeter5");
|
||||
using var capture = new MetricsCapture("TestMeter5");
|
||||
|
||||
var counter = meter.CreateCounter<long>("monotonic_counter");
|
||||
counter.Add(1);
|
||||
counter.Add(2);
|
||||
counter.Add(3);
|
||||
|
||||
var act = () => MetricsContractAssert.CounterMonotonic(capture, "monotonic_counter");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GaugeInBounds_WithinRange_NoException()
|
||||
{
|
||||
using var meter = new Meter("TestMeter6");
|
||||
using var capture = new MetricsCapture("TestMeter6");
|
||||
|
||||
var gauge = meter.CreateObservableGauge("memory_usage_bytes", () => 500);
|
||||
// Force a measurement
|
||||
capture.GetValues("memory_usage_bytes");
|
||||
|
||||
// This test validates the API structure - actual observable gauge testing
|
||||
// requires meter listener callbacks which are triggered asynchronously
|
||||
var act = () => MetricsContractAssert.GaugeInBounds(capture, "memory_usage_bytes", 0, 1000);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ContractViolationException Tests
|
||||
|
||||
[Fact]
|
||||
public void ContractViolationException_ContainsMessage()
|
||||
{
|
||||
var ex = new ContractViolationException("Test violation");
|
||||
|
||||
ex.Message.Should().Be("Test violation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContractViolationException_WithInnerException()
|
||||
{
|
||||
var inner = new InvalidOperationException("Inner error");
|
||||
var ex = new ContractViolationException("Outer error", inner);
|
||||
|
||||
ex.Message.Should().Be("Outer error");
|
||||
ex.InnerException.Should().Be(inner);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user