Add tests for SBOM generation determinism across multiple formats
- Created `StellaOps.TestKit.Tests` project for unit tests related to determinism. - Implemented `DeterminismManifestTests` to validate deterministic output for canonical bytes and strings, file read/write operations, and error handling for invalid schema versions. - Added `SbomDeterminismTests` to ensure identical inputs produce consistent SBOMs across SPDX 3.0.1 and CycloneDX 1.6/1.7 formats, including parallel execution tests. - Updated project references in `StellaOps.Integration.Determinism` to include the new determinism testing library.
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DeterminismBaselineStoreTests.cs
|
||||
// Sprint: SPRINT_5100_0007_0003 - Epic B (Determinism Gate)
|
||||
// Task: T9 - Determinism Baseline Storage
|
||||
// Description: Tests for baseline storage and comparison functionality
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Testing.Determinism;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Testing.Determinism.Tests;
|
||||
|
||||
public sealed class DeterminismBaselineStoreTests : IDisposable
|
||||
{
|
||||
private readonly string _testDirectory;
|
||||
private readonly DeterminismBaselineStore _store;
|
||||
|
||||
public DeterminismBaselineStoreTests()
|
||||
{
|
||||
_testDirectory = Path.Combine(Path.GetTempPath(), $"determinism-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_testDirectory);
|
||||
_store = new DeterminismBaselineStore(_testDirectory);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_testDirectory))
|
||||
{
|
||||
Directory.Delete(_testDirectory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
#region CreateBaseline Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateBaseline_WithValidInput_ReturnsCorrectHash()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = Encoding.UTF8.GetBytes("{\"test\":\"data\"}");
|
||||
var version = "1.0.0";
|
||||
|
||||
// Act
|
||||
var baseline = DeterminismBaselineStore.CreateBaseline(artifactBytes, version);
|
||||
|
||||
// Assert
|
||||
baseline.Should().NotBeNull();
|
||||
baseline.CanonicalHash.Should().MatchRegex("^[0-9a-f]{64}$");
|
||||
baseline.Algorithm.Should().Be("SHA-256");
|
||||
baseline.Version.Should().Be("1.0.0");
|
||||
baseline.UpdatedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateBaseline_WithSameInput_ProducesSameHash()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = Encoding.UTF8.GetBytes("{\"test\":\"data\"}");
|
||||
|
||||
// Act
|
||||
var baseline1 = DeterminismBaselineStore.CreateBaseline(artifactBytes, "1.0.0");
|
||||
var baseline2 = DeterminismBaselineStore.CreateBaseline(artifactBytes, "1.0.0");
|
||||
|
||||
// Assert
|
||||
baseline1.CanonicalHash.Should().Be(baseline2.CanonicalHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateBaseline_WithDifferentInput_ProducesDifferentHash()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes1 = Encoding.UTF8.GetBytes("{\"test\":\"data1\"}");
|
||||
var artifactBytes2 = Encoding.UTF8.GetBytes("{\"test\":\"data2\"}");
|
||||
|
||||
// Act
|
||||
var baseline1 = DeterminismBaselineStore.CreateBaseline(artifactBytes1, "1.0.0");
|
||||
var baseline2 = DeterminismBaselineStore.CreateBaseline(artifactBytes2, "1.0.0");
|
||||
|
||||
// Assert
|
||||
baseline1.CanonicalHash.Should().NotBe(baseline2.CanonicalHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateBaseline_WithMetadata_IncludesMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = Encoding.UTF8.GetBytes("{\"test\":\"data\"}");
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
["format"] = "CycloneDX 1.6",
|
||||
["source"] = "scanner-test"
|
||||
};
|
||||
|
||||
// Act
|
||||
var baseline = DeterminismBaselineStore.CreateBaseline(artifactBytes, "1.0.0", metadata);
|
||||
|
||||
// Assert
|
||||
baseline.Metadata.Should().NotBeNull();
|
||||
baseline.Metadata.Should().ContainKey("format").WhoseValue.Should().Be("CycloneDX 1.6");
|
||||
baseline.Metadata.Should().ContainKey("source").WhoseValue.Should().Be("scanner-test");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Store and Retrieve Tests
|
||||
|
||||
[Fact]
|
||||
public async Task StoreBaseline_AndRetrieve_RoundTripsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = Encoding.UTF8.GetBytes("{\"component\":\"test\"}");
|
||||
var baseline = DeterminismBaselineStore.CreateBaseline(artifactBytes, "2.0.0");
|
||||
|
||||
// Act
|
||||
await _store.StoreBaselineAsync("sbom", "test-artifact", baseline);
|
||||
var retrieved = await _store.GetBaselineAsync("sbom", "test-artifact");
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.CanonicalHash.Should().Be(baseline.CanonicalHash);
|
||||
retrieved.Version.Should().Be("2.0.0");
|
||||
retrieved.Algorithm.Should().Be("SHA-256");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBaseline_WhenNotExists_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = await _store.GetBaselineAsync("sbom", "nonexistent");
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StoreBaseline_CreatesCorrectDirectoryStructure()
|
||||
{
|
||||
// Arrange
|
||||
var baseline = DeterminismBaselineStore.CreateBaseline(
|
||||
Encoding.UTF8.GetBytes("test"),
|
||||
"1.0.0");
|
||||
|
||||
// Act
|
||||
await _store.StoreBaselineAsync("vex", "openevex-document", baseline);
|
||||
|
||||
// Assert
|
||||
var expectedPath = Path.Combine(_testDirectory, "vex", "openevex-document.baseline.json");
|
||||
File.Exists(expectedPath).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StoreBaseline_OverwritesExistingBaseline()
|
||||
{
|
||||
// Arrange
|
||||
var baseline1 = DeterminismBaselineStore.CreateBaseline(
|
||||
Encoding.UTF8.GetBytes("original"),
|
||||
"1.0.0");
|
||||
var baseline2 = DeterminismBaselineStore.CreateBaseline(
|
||||
Encoding.UTF8.GetBytes("updated"),
|
||||
"2.0.0");
|
||||
|
||||
// Act
|
||||
await _store.StoreBaselineAsync("sbom", "artifact", baseline1);
|
||||
await _store.StoreBaselineAsync("sbom", "artifact", baseline2);
|
||||
var retrieved = await _store.GetBaselineAsync("sbom", "artifact");
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.CanonicalHash.Should().Be(baseline2.CanonicalHash);
|
||||
retrieved.Version.Should().Be("2.0.0");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Compare Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Compare_WhenMatches_ReturnsMatchStatus()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = Encoding.UTF8.GetBytes("{\"test\":\"data\"}");
|
||||
var baseline = DeterminismBaselineStore.CreateBaseline(artifactBytes, "1.0.0");
|
||||
await _store.StoreBaselineAsync("sbom", "test", baseline);
|
||||
|
||||
// Act
|
||||
var result = await _store.CompareAsync("sbom", "test", baseline.CanonicalHash);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(BaselineStatus.Match);
|
||||
result.CurrentHash.Should().Be(baseline.CanonicalHash);
|
||||
result.BaselineHash.Should().Be(baseline.CanonicalHash);
|
||||
result.Message.Should().Contain("matches baseline");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Compare_WhenDrifted_ReturnsDriftStatus()
|
||||
{
|
||||
// Arrange
|
||||
var originalBytes = Encoding.UTF8.GetBytes("{\"test\":\"original\"}");
|
||||
var baseline = DeterminismBaselineStore.CreateBaseline(originalBytes, "1.0.0");
|
||||
await _store.StoreBaselineAsync("sbom", "test", baseline);
|
||||
|
||||
var newBytes = Encoding.UTF8.GetBytes("{\"test\":\"changed\"}");
|
||||
var newBaseline = DeterminismBaselineStore.CreateBaseline(newBytes, "1.0.0");
|
||||
|
||||
// Act
|
||||
var result = await _store.CompareAsync("sbom", "test", newBaseline.CanonicalHash);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(BaselineStatus.Drift);
|
||||
result.CurrentHash.Should().Be(newBaseline.CanonicalHash);
|
||||
result.BaselineHash.Should().Be(baseline.CanonicalHash);
|
||||
result.Message.Should().Contain("DRIFT DETECTED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Compare_WhenMissing_ReturnsMissingStatus()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = Encoding.UTF8.GetBytes("{\"test\":\"data\"}");
|
||||
var baseline = DeterminismBaselineStore.CreateBaseline(artifactBytes, "1.0.0");
|
||||
|
||||
// Act
|
||||
var result = await _store.CompareAsync("sbom", "nonexistent", baseline.CanonicalHash);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(BaselineStatus.Missing);
|
||||
result.CurrentHash.Should().Be(baseline.CanonicalHash);
|
||||
result.BaselineHash.Should().BeNull();
|
||||
result.Message.Should().Contain("No baseline found");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ListBaselines Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ListBaselines_WhenEmpty_ReturnsEmptyList()
|
||||
{
|
||||
// Act
|
||||
var baselines = await _store.ListBaselinesAsync();
|
||||
|
||||
// Assert
|
||||
baselines.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListBaselines_ReturnsAllStoredBaselines()
|
||||
{
|
||||
// Arrange
|
||||
await _store.StoreBaselineAsync("sbom", "artifact1",
|
||||
DeterminismBaselineStore.CreateBaseline(Encoding.UTF8.GetBytes("1"), "1.0.0"));
|
||||
await _store.StoreBaselineAsync("sbom", "artifact2",
|
||||
DeterminismBaselineStore.CreateBaseline(Encoding.UTF8.GetBytes("2"), "1.0.0"));
|
||||
await _store.StoreBaselineAsync("vex", "document1",
|
||||
DeterminismBaselineStore.CreateBaseline(Encoding.UTF8.GetBytes("3"), "1.0.0"));
|
||||
|
||||
// Act
|
||||
var baselines = await _store.ListBaselinesAsync();
|
||||
|
||||
// Assert
|
||||
baselines.Should().HaveCount(3);
|
||||
baselines.Should().Contain(e => e.ArtifactType == "sbom" && e.ArtifactName == "artifact1");
|
||||
baselines.Should().Contain(e => e.ArtifactType == "sbom" && e.ArtifactName == "artifact2");
|
||||
baselines.Should().Contain(e => e.ArtifactType == "vex" && e.ArtifactName == "document1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListBaselines_ReturnsOrderedResults()
|
||||
{
|
||||
// Arrange
|
||||
await _store.StoreBaselineAsync("vex", "z-document",
|
||||
DeterminismBaselineStore.CreateBaseline(Encoding.UTF8.GetBytes("1"), "1.0.0"));
|
||||
await _store.StoreBaselineAsync("sbom", "a-artifact",
|
||||
DeterminismBaselineStore.CreateBaseline(Encoding.UTF8.GetBytes("2"), "1.0.0"));
|
||||
await _store.StoreBaselineAsync("sbom", "b-artifact",
|
||||
DeterminismBaselineStore.CreateBaseline(Encoding.UTF8.GetBytes("3"), "1.0.0"));
|
||||
|
||||
// Act
|
||||
var baselines = await _store.ListBaselinesAsync();
|
||||
|
||||
// Assert
|
||||
baselines[0].ArtifactType.Should().Be("sbom");
|
||||
baselines[0].ArtifactName.Should().Be("a-artifact");
|
||||
baselines[1].ArtifactType.Should().Be("sbom");
|
||||
baselines[1].ArtifactName.Should().Be("b-artifact");
|
||||
baselines[2].ArtifactType.Should().Be("vex");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateDefault Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateDefault_CreatesStoreWithCorrectPath()
|
||||
{
|
||||
// Act
|
||||
var store = DeterminismBaselineStore.CreateDefault(_testDirectory);
|
||||
|
||||
// Assert
|
||||
store.BaselineDirectory.Should().Be(Path.Combine(_testDirectory, "tests", "baselines", "determinism"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Testing.Determinism;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Testing.Determinism.Tests;
|
||||
|
||||
public sealed class DeterminismManifestTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToCanonicalBytes_WithValidManifest_ProducesDeterministicOutput()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
|
||||
// Act
|
||||
var bytes1 = DeterminismManifestWriter.ToCanonicalBytes(manifest);
|
||||
var bytes2 = DeterminismManifestWriter.ToCanonicalBytes(manifest);
|
||||
|
||||
// Assert
|
||||
bytes1.Should().Equal(bytes2, "Same manifest should produce identical canonical bytes");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToCanonicalString_WithValidManifest_ProducesDeterministicString()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
|
||||
// Act
|
||||
var json1 = DeterminismManifestWriter.ToCanonicalString(manifest);
|
||||
var json2 = DeterminismManifestWriter.ToCanonicalString(manifest);
|
||||
|
||||
// Assert
|
||||
json1.Should().Be(json2, "Same manifest should produce identical canonical JSON string");
|
||||
json1.Should().NotContain("\n", "Canonical JSON should have no newlines");
|
||||
json1.Should().NotContain(" ", "Canonical JSON should have no indentation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteToFile_AndReadFromFile_RoundTripsSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
var tempFile = Path.GetTempFileName();
|
||||
|
||||
try
|
||||
{
|
||||
// Act - Write
|
||||
DeterminismManifestWriter.WriteToFile(manifest, tempFile);
|
||||
|
||||
// Act - Read
|
||||
var readManifest = DeterminismManifestReader.ReadFromFile(tempFile);
|
||||
|
||||
// Assert
|
||||
readManifest.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteToFileAsync_AndReadFromFileAsync_RoundTripsSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
var tempFile = Path.GetTempFileName();
|
||||
|
||||
try
|
||||
{
|
||||
// Act - Write
|
||||
await DeterminismManifestWriter.WriteToFileAsync(manifest, tempFile);
|
||||
|
||||
// Act - Read
|
||||
var readManifest = await DeterminismManifestReader.ReadFromFileAsync(tempFile);
|
||||
|
||||
// Assert
|
||||
readManifest.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromBytes_WithValidJson_DeserializesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
var bytes = DeterminismManifestWriter.ToCanonicalBytes(manifest);
|
||||
|
||||
// Act
|
||||
var deserialized = DeterminismManifestReader.FromBytes(bytes);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromString_WithValidJson_DeserializesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
var json = DeterminismManifestWriter.ToCanonicalString(manifest);
|
||||
|
||||
// Act
|
||||
var deserialized = DeterminismManifestReader.FromString(json);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToCanonicalBytes_WithInvalidSchemaVersion_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest() with { SchemaVersion = "2.0" };
|
||||
|
||||
// Act
|
||||
Action act = () => DeterminismManifestWriter.ToCanonicalBytes(manifest);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*schema version*2.0*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryReadFromFileAsync_WithNonExistentFile_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
|
||||
// Act
|
||||
var result = DeterminismManifestReader.TryReadFromFileAsync(nonExistentPath).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadFromFile_WithNonExistentFile_ThrowsFileNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
|
||||
// Act
|
||||
Action act = () => DeterminismManifestReader.ReadFromFile(nonExistentPath);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<FileNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeCanonicalHash_ProducesDeterministicHash()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
|
||||
// Act
|
||||
var hash1 = DeterminismManifestWriter.ComputeCanonicalHash(manifest);
|
||||
var hash2 = DeterminismManifestWriter.ComputeCanonicalHash(manifest);
|
||||
|
||||
// Assert
|
||||
hash1.Should().Be(hash2, "Same manifest should produce same hash");
|
||||
hash1.Should().MatchRegex("^[0-9a-f]{64}$", "Hash should be 64-character hex string");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateManifest_WithValidInputs_CreatesManifestWithCorrectHash()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = "Test artifact content"u8.ToArray();
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test-sbom",
|
||||
Version = "1.0.0",
|
||||
Format = "SPDX 3.0.1"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[]
|
||||
{
|
||||
new ComponentInfo { Name = "StellaOps.Scanner", Version = "1.0.0" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = DeterminismManifestWriter.CreateManifest(
|
||||
artifactBytes,
|
||||
artifactInfo,
|
||||
toolchain);
|
||||
|
||||
// Assert
|
||||
manifest.SchemaVersion.Should().Be("1.0");
|
||||
manifest.Artifact.Should().Be(artifactInfo);
|
||||
manifest.Toolchain.Should().Be(toolchain);
|
||||
manifest.CanonicalHash.Algorithm.Should().Be("SHA-256");
|
||||
manifest.CanonicalHash.Encoding.Should().Be("hex");
|
||||
manifest.CanonicalHash.Value.Should().MatchRegex("^[0-9a-f]{64}$");
|
||||
manifest.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
|
||||
// Verify hash is correct
|
||||
var expectedHash = CanonJson.Sha256Hex(artifactBytes);
|
||||
manifest.CanonicalHash.Value.Should().Be(expectedHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateManifestForJsonArtifact_WithValidInputs_CreatesManifestWithCanonicalHash()
|
||||
{
|
||||
// Arrange
|
||||
var artifact = new { Name = "test", Value = 123, Items = new[] { "a", "b", "c" } };
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "verdict",
|
||||
Name = "test-verdict",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[]
|
||||
{
|
||||
new ComponentInfo { Name = "StellaOps.Policy.Engine", Version = "1.0.0" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = DeterminismManifestWriter.CreateManifestForJsonArtifact(
|
||||
artifact,
|
||||
artifactInfo,
|
||||
toolchain);
|
||||
|
||||
// Assert
|
||||
manifest.SchemaVersion.Should().Be("1.0");
|
||||
manifest.Artifact.Should().Be(artifactInfo);
|
||||
manifest.Toolchain.Should().Be(toolchain);
|
||||
manifest.CanonicalHash.Algorithm.Should().Be("SHA-256");
|
||||
manifest.CanonicalHash.Encoding.Should().Be("hex");
|
||||
manifest.CanonicalHash.Value.Should().MatchRegex("^[0-9a-f]{64}$");
|
||||
|
||||
// Verify hash is correct (should use canonical JSON)
|
||||
var expectedHash = CanonJson.Hash(artifact);
|
||||
manifest.CanonicalHash.Value.Should().Be(expectedHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateManifest_WithInputStamps_IncludesInputStamps()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = "Test artifact"u8.ToArray();
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
|
||||
};
|
||||
var inputs = new InputStamps
|
||||
{
|
||||
FeedSnapshotHash = "abc123",
|
||||
PolicyManifestHash = "def456",
|
||||
SourceCodeHash = "789abc"
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = DeterminismManifestWriter.CreateManifest(
|
||||
artifactBytes,
|
||||
artifactInfo,
|
||||
toolchain,
|
||||
inputs: inputs);
|
||||
|
||||
// Assert
|
||||
manifest.Inputs.Should().NotBeNull();
|
||||
manifest.Inputs!.FeedSnapshotHash.Should().Be("abc123");
|
||||
manifest.Inputs.PolicyManifestHash.Should().Be("def456");
|
||||
manifest.Inputs.SourceCodeHash.Should().Be("789abc");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateManifest_WithReproducibilityMetadata_IncludesMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = "Test artifact"u8.ToArray();
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
|
||||
};
|
||||
var reproducibility = new ReproducibilityMetadata
|
||||
{
|
||||
DeterministicSeed = 42,
|
||||
ClockFixed = true,
|
||||
OrderingGuarantee = "sorted",
|
||||
NormalizationRules = new[] { "UTF-8", "LF line endings", "sorted JSON keys" }
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = DeterminismManifestWriter.CreateManifest(
|
||||
artifactBytes,
|
||||
artifactInfo,
|
||||
toolchain,
|
||||
reproducibility: reproducibility);
|
||||
|
||||
// Assert
|
||||
manifest.Reproducibility.Should().NotBeNull();
|
||||
manifest.Reproducibility!.DeterministicSeed.Should().Be(42);
|
||||
manifest.Reproducibility.ClockFixed.Should().BeTrue();
|
||||
manifest.Reproducibility.OrderingGuarantee.Should().Be("sorted");
|
||||
manifest.Reproducibility.NormalizationRules.Should().ContainInOrder("UTF-8", "LF line endings", "sorted JSON keys");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateManifest_WithVerificationInfo_IncludesVerification()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = "Test artifact"u8.ToArray();
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
|
||||
};
|
||||
var verification = new VerificationInfo
|
||||
{
|
||||
Command = "dotnet run --project Scanner -- scan container alpine:3.18",
|
||||
ExpectedHash = "abc123def456",
|
||||
Baseline = "tests/baselines/sbom-alpine-3.18.determinism.json"
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = DeterminismManifestWriter.CreateManifest(
|
||||
artifactBytes,
|
||||
artifactInfo,
|
||||
toolchain,
|
||||
verification: verification);
|
||||
|
||||
// Assert
|
||||
manifest.Verification.Should().NotBeNull();
|
||||
manifest.Verification!.Command.Should().Be("dotnet run --project Scanner -- scan container alpine:3.18");
|
||||
manifest.Verification.ExpectedHash.Should().Be("abc123def456");
|
||||
manifest.Verification.Baseline.Should().Be("tests/baselines/sbom-alpine-3.18.determinism.json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManifestSerialization_WithComplexMetadata_PreservesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateComplexManifest();
|
||||
|
||||
// Act
|
||||
var json = DeterminismManifestWriter.ToCanonicalString(manifest);
|
||||
var deserialized = DeterminismManifestReader.FromString(json);
|
||||
|
||||
// Assert - Use custom comparison to handle JsonElement values in metadata
|
||||
deserialized.Should().BeEquivalentTo(manifest, options => options
|
||||
.Excluding(m => m.Artifact.Metadata));
|
||||
|
||||
// Verify metadata separately (JSON deserialization converts values to JsonElement)
|
||||
deserialized.Artifact.Metadata.Should().NotBeNull();
|
||||
deserialized.Artifact.Metadata.Should().HaveCount(2);
|
||||
deserialized.Artifact.Metadata.Should().ContainKey("predicateType");
|
||||
deserialized.Artifact.Metadata.Should().ContainKey("subject");
|
||||
}
|
||||
|
||||
private static DeterminismManifest CreateSampleManifest()
|
||||
{
|
||||
return new DeterminismManifest
|
||||
{
|
||||
SchemaVersion = "1.0",
|
||||
Artifact = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test-sbom",
|
||||
Version = "1.0.0",
|
||||
Format = "SPDX 3.0.1"
|
||||
},
|
||||
CanonicalHash = new CanonicalHashInfo
|
||||
{
|
||||
Algorithm = "SHA-256",
|
||||
Value = "abc123def456789012345678901234567890123456789012345678901234",
|
||||
Encoding = "hex"
|
||||
},
|
||||
Toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[]
|
||||
{
|
||||
new ComponentInfo
|
||||
{
|
||||
Name = "StellaOps.Scanner",
|
||||
Version = "1.0.0",
|
||||
Hash = "def456abc789012345678901234567890123456789012345678901234567"
|
||||
}
|
||||
}
|
||||
},
|
||||
GeneratedAt = new DateTimeOffset(2025, 12, 23, 17, 45, 0, TimeSpan.Zero)
|
||||
};
|
||||
}
|
||||
|
||||
private static DeterminismManifest CreateComplexManifest()
|
||||
{
|
||||
return new DeterminismManifest
|
||||
{
|
||||
SchemaVersion = "1.0",
|
||||
Artifact = new ArtifactInfo
|
||||
{
|
||||
Type = "evidence-bundle",
|
||||
Name = "test-bundle",
|
||||
Version = "2.0.0",
|
||||
Format = "DSSE Envelope",
|
||||
Metadata = new Dictionary<string, object?>
|
||||
{
|
||||
["predicateType"] = "https://in-toto.io/attestation/v1",
|
||||
["subject"] = "pkg:docker/alpine@3.18"
|
||||
}
|
||||
},
|
||||
CanonicalHash = new CanonicalHashInfo
|
||||
{
|
||||
Algorithm = "SHA-256",
|
||||
Value = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
Encoding = "hex"
|
||||
},
|
||||
Inputs = new InputStamps
|
||||
{
|
||||
FeedSnapshotHash = "feed123abc",
|
||||
PolicyManifestHash = "policy456def",
|
||||
SourceCodeHash = "git789ghi",
|
||||
VexDocumentHashes = new[] { "vex123", "vex456" },
|
||||
Custom = new Dictionary<string, string>
|
||||
{
|
||||
["baselineVersion"] = "1.0.0",
|
||||
["environment"] = "production"
|
||||
}
|
||||
},
|
||||
Toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[]
|
||||
{
|
||||
new ComponentInfo { Name = "StellaOps.Attestor", Version = "2.0.0", Hash = "hash123" },
|
||||
new ComponentInfo { Name = "StellaOps.Signer", Version = "2.1.0" }
|
||||
},
|
||||
Compiler = new CompilerInfo
|
||||
{
|
||||
Name = "Roslyn",
|
||||
Version = "4.8.0"
|
||||
}
|
||||
},
|
||||
GeneratedAt = new DateTimeOffset(2025, 12, 23, 18, 0, 0, TimeSpan.Zero),
|
||||
Reproducibility = new ReproducibilityMetadata
|
||||
{
|
||||
DeterministicSeed = 12345,
|
||||
ClockFixed = true,
|
||||
OrderingGuarantee = "stable",
|
||||
NormalizationRules = new[] { "UTF-8", "LF line endings", "no trailing whitespace" }
|
||||
},
|
||||
Verification = new VerificationInfo
|
||||
{
|
||||
Command = "dotnet test --verify-determinism",
|
||||
ExpectedHash = "abc123def456",
|
||||
Baseline = "baselines/test-bundle.json"
|
||||
},
|
||||
Signatures = new[]
|
||||
{
|
||||
new SignatureInfo
|
||||
{
|
||||
Algorithm = "ES256",
|
||||
KeyId = "signing-key-1",
|
||||
Signature = "base64encodedSig==",
|
||||
Timestamp = new DateTimeOffset(2025, 12, 23, 18, 0, 30, TimeSpan.Zero)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DeterminismSummaryTests.cs
|
||||
// Sprint: SPRINT_5100_0007_0003 - Epic B (Determinism Gate)
|
||||
// Task: T9 - Determinism Baseline Storage
|
||||
// Description: Tests for determinism summary and CI artifact generation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Testing.Determinism;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Testing.Determinism.Tests;
|
||||
|
||||
public sealed class DeterminismSummaryTests : IDisposable
|
||||
{
|
||||
private readonly string _testDirectory;
|
||||
|
||||
public DeterminismSummaryTests()
|
||||
{
|
||||
_testDirectory = Path.Combine(Path.GetTempPath(), $"summary-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_testDirectory);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_testDirectory))
|
||||
{
|
||||
Directory.Delete(_testDirectory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
#region DeterminismSummaryBuilder Tests
|
||||
|
||||
[Fact]
|
||||
public void Build_WithNoResults_ReturnsPassStatus()
|
||||
{
|
||||
// Act
|
||||
var summary = new DeterminismSummaryBuilder().Build();
|
||||
|
||||
// Assert
|
||||
summary.Status.Should().Be(DeterminismCheckStatus.Pass);
|
||||
summary.Statistics.Total.Should().Be(0);
|
||||
summary.Statistics.Matched.Should().Be(0);
|
||||
summary.Statistics.Drifted.Should().Be(0);
|
||||
summary.Statistics.Missing.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithAllMatching_ReturnsPassStatus()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeterminismSummaryBuilder()
|
||||
.AddResult(CreateMatchResult("sbom", "artifact1"))
|
||||
.AddResult(CreateMatchResult("sbom", "artifact2"))
|
||||
.AddResult(CreateMatchResult("vex", "document1"));
|
||||
|
||||
// Act
|
||||
var summary = builder.Build();
|
||||
|
||||
// Assert
|
||||
summary.Status.Should().Be(DeterminismCheckStatus.Pass);
|
||||
summary.Statistics.Total.Should().Be(3);
|
||||
summary.Statistics.Matched.Should().Be(3);
|
||||
summary.Statistics.Drifted.Should().Be(0);
|
||||
summary.Statistics.Missing.Should().Be(0);
|
||||
summary.Drift.Should().BeNull();
|
||||
summary.Missing.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithDrift_ReturnsFailStatus()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeterminismSummaryBuilder()
|
||||
.AddResult(CreateMatchResult("sbom", "artifact1"))
|
||||
.AddResult(CreateDriftResult("sbom", "artifact2"))
|
||||
.AddResult(CreateMatchResult("vex", "document1"));
|
||||
|
||||
// Act
|
||||
var summary = builder.Build();
|
||||
|
||||
// Assert
|
||||
summary.Status.Should().Be(DeterminismCheckStatus.Fail);
|
||||
summary.Statistics.Total.Should().Be(3);
|
||||
summary.Statistics.Matched.Should().Be(2);
|
||||
summary.Statistics.Drifted.Should().Be(1);
|
||||
summary.Statistics.Missing.Should().Be(0);
|
||||
summary.Drift.Should().HaveCount(1);
|
||||
summary.Drift![0].ArtifactName.Should().Be("artifact2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMissing_ReturnsWarningStatus()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeterminismSummaryBuilder()
|
||||
.AddResult(CreateMatchResult("sbom", "artifact1"))
|
||||
.AddResult(CreateMissingResult("sbom", "artifact2"));
|
||||
|
||||
// Act
|
||||
var summary = builder.Build();
|
||||
|
||||
// Assert
|
||||
summary.Status.Should().Be(DeterminismCheckStatus.Warning);
|
||||
summary.Statistics.Total.Should().Be(2);
|
||||
summary.Statistics.Matched.Should().Be(1);
|
||||
summary.Statistics.Missing.Should().Be(1);
|
||||
summary.Missing.Should().HaveCount(1);
|
||||
summary.Missing![0].ArtifactName.Should().Be("artifact2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMissing_AndFailOnMissing_ReturnsFailStatus()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeterminismSummaryBuilder()
|
||||
.FailOnMissingBaselines()
|
||||
.AddResult(CreateMatchResult("sbom", "artifact1"))
|
||||
.AddResult(CreateMissingResult("sbom", "artifact2"));
|
||||
|
||||
// Act
|
||||
var summary = builder.Build();
|
||||
|
||||
// Assert
|
||||
summary.Status.Should().Be(DeterminismCheckStatus.Fail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_DriftTakesPrecedenceOverMissing()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeterminismSummaryBuilder()
|
||||
.AddResult(CreateDriftResult("sbom", "artifact1"))
|
||||
.AddResult(CreateMissingResult("sbom", "artifact2"));
|
||||
|
||||
// Act
|
||||
var summary = builder.Build();
|
||||
|
||||
// Assert
|
||||
summary.Status.Should().Be(DeterminismCheckStatus.Fail);
|
||||
summary.Statistics.Drifted.Should().Be(1);
|
||||
summary.Statistics.Missing.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithSourceRef_IncludesSourceRef()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeterminismSummaryBuilder()
|
||||
.WithSourceRef("abc123def456")
|
||||
.AddResult(CreateMatchResult("sbom", "artifact"));
|
||||
|
||||
// Act
|
||||
var summary = builder.Build();
|
||||
|
||||
// Assert
|
||||
summary.SourceRef.Should().Be("abc123def456");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithCiRunId_IncludesCiRunId()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeterminismSummaryBuilder()
|
||||
.WithCiRunId("run-12345")
|
||||
.AddResult(CreateMatchResult("sbom", "artifact"));
|
||||
|
||||
// Act
|
||||
var summary = builder.Build();
|
||||
|
||||
// Assert
|
||||
summary.CiRunId.Should().Be("run-12345");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_SetsGeneratedAtToUtcNow()
|
||||
{
|
||||
// Arrange
|
||||
var before = DateTimeOffset.UtcNow;
|
||||
var builder = new DeterminismSummaryBuilder();
|
||||
|
||||
// Act
|
||||
var summary = builder.Build();
|
||||
var after = DateTimeOffset.UtcNow;
|
||||
|
||||
// Assert
|
||||
summary.GeneratedAt.Should().BeOnOrAfter(before);
|
||||
summary.GeneratedAt.Should().BeOnOrBefore(after);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DeterminismSummaryWriter Tests
|
||||
|
||||
[Fact]
|
||||
public async Task WriteToFileAsync_CreatesValidJsonFile()
|
||||
{
|
||||
// Arrange
|
||||
var summary = new DeterminismSummaryBuilder()
|
||||
.WithSourceRef("test-sha")
|
||||
.AddResult(CreateMatchResult("sbom", "test-artifact"))
|
||||
.Build();
|
||||
|
||||
var filePath = Path.Combine(_testDirectory, "determinism.json");
|
||||
|
||||
// Act
|
||||
await DeterminismSummaryWriter.WriteToFileAsync(summary, filePath);
|
||||
|
||||
// Assert
|
||||
File.Exists(filePath).Should().BeTrue();
|
||||
var content = await File.ReadAllTextAsync(filePath);
|
||||
content.Should().Contain("\"schemaVersion\": \"1.0\"");
|
||||
content.Should().Contain("\"sourceRef\": \"test-sha\"");
|
||||
content.Should().Contain("\"status\": \"pass\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteToFileAsync_CreatesDirectoryIfNeeded()
|
||||
{
|
||||
// Arrange
|
||||
var summary = new DeterminismSummaryBuilder().Build();
|
||||
var filePath = Path.Combine(_testDirectory, "nested", "dir", "determinism.json");
|
||||
|
||||
// Act
|
||||
await DeterminismSummaryWriter.WriteToFileAsync(summary, filePath);
|
||||
|
||||
// Assert
|
||||
File.Exists(filePath).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToJson_ReturnsValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var summary = new DeterminismSummaryBuilder()
|
||||
.AddResult(CreateMatchResult("sbom", "artifact"))
|
||||
.AddResult(CreateDriftResult("vex", "document"))
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var json = DeterminismSummaryWriter.ToJson(summary);
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"status\": \"fail\"");
|
||||
json.Should().Contain("\"total\": 2");
|
||||
json.Should().Contain("\"matched\": 1");
|
||||
json.Should().Contain("\"drifted\": 1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteHashFilesAsync_CreatesHashFilesForAllArtifacts()
|
||||
{
|
||||
// Arrange
|
||||
var summary = new DeterminismSummaryBuilder()
|
||||
.AddResult(CreateMatchResult("sbom", "artifact1"))
|
||||
.AddResult(CreateMatchResult("vex", "document1"))
|
||||
.Build();
|
||||
|
||||
var hashDir = Path.Combine(_testDirectory, "hashes");
|
||||
|
||||
// Act
|
||||
await DeterminismSummaryWriter.WriteHashFilesAsync(summary, hashDir);
|
||||
|
||||
// Assert
|
||||
File.Exists(Path.Combine(hashDir, "sbom_artifact1.sha256.txt")).Should().BeTrue();
|
||||
File.Exists(Path.Combine(hashDir, "vex_document1.sha256.txt")).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteHashFilesAsync_HashFileContainsCorrectFormat()
|
||||
{
|
||||
// Arrange
|
||||
var summary = new DeterminismSummaryBuilder()
|
||||
.AddResult(new BaselineComparisonResult
|
||||
{
|
||||
ArtifactType = "sbom",
|
||||
ArtifactName = "test",
|
||||
Status = BaselineStatus.Match,
|
||||
CurrentHash = "abc123def456",
|
||||
Message = "Test"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var hashDir = Path.Combine(_testDirectory, "hashes");
|
||||
|
||||
// Act
|
||||
await DeterminismSummaryWriter.WriteHashFilesAsync(summary, hashDir);
|
||||
|
||||
// Assert
|
||||
var content = await File.ReadAllTextAsync(Path.Combine(hashDir, "sbom_test.sha256.txt"));
|
||||
content.Should().Be("abc123def456 sbom/test");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static BaselineComparisonResult CreateMatchResult(string artifactType, string artifactName)
|
||||
{
|
||||
var hash = $"hash-{artifactType}-{artifactName}";
|
||||
return new BaselineComparisonResult
|
||||
{
|
||||
ArtifactType = artifactType,
|
||||
ArtifactName = artifactName,
|
||||
Status = BaselineStatus.Match,
|
||||
CurrentHash = hash,
|
||||
BaselineHash = hash,
|
||||
Message = $"Artifact {artifactType}/{artifactName} matches baseline."
|
||||
};
|
||||
}
|
||||
|
||||
private static BaselineComparisonResult CreateDriftResult(string artifactType, string artifactName)
|
||||
{
|
||||
return new BaselineComparisonResult
|
||||
{
|
||||
ArtifactType = artifactType,
|
||||
ArtifactName = artifactName,
|
||||
Status = BaselineStatus.Drift,
|
||||
CurrentHash = $"new-hash-{artifactType}-{artifactName}",
|
||||
BaselineHash = $"old-hash-{artifactType}-{artifactName}",
|
||||
Message = $"DRIFT DETECTED: {artifactType}/{artifactName}"
|
||||
};
|
||||
}
|
||||
|
||||
private static BaselineComparisonResult CreateMissingResult(string artifactType, string artifactName)
|
||||
{
|
||||
return new BaselineComparisonResult
|
||||
{
|
||||
ArtifactType = artifactType,
|
||||
ArtifactName = artifactName,
|
||||
Status = BaselineStatus.Missing,
|
||||
CurrentHash = $"hash-{artifactType}-{artifactName}",
|
||||
Message = $"No baseline found for {artifactType}/{artifactName}"
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Testing.Determinism\StellaOps.Testing.Determinism.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user