UI work to fill SBOM sourcing management gap. UI planning remaining functionality exposure. Work on CI/Tests stabilization
Introduces CGS determinism test runs to CI workflows for Windows, macOS, Linux, Alpine, and Debian, fulfilling CGS-008 cross-platform requirements. Updates local-ci scripts to support new smoke steps, test timeouts, progress intervals, and project slicing for improved test isolation and diagnostics.
This commit is contained in:
126
src/__Tests/Tools/FixtureHarvester/Commands/HarvestCommand.cs
Normal file
126
src/__Tests/Tools/FixtureHarvester/Commands/HarvestCommand.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
// <copyright file="HarvestCommand.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Testing.FixtureHarvester.Models;
|
||||
|
||||
namespace StellaOps.Testing.FixtureHarvester.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Harvest command - fetch, hash, store, and create metadata for fixtures
|
||||
/// </summary>
|
||||
internal static class HarvestCommand
|
||||
{
|
||||
internal static async Task ExecuteAsync(string type, string id, string? source, string output)
|
||||
{
|
||||
Console.WriteLine($"Harvesting {type} fixture '{id}'...");
|
||||
|
||||
var fixtureDir = Path.Combine(output, type, id);
|
||||
Directory.CreateDirectory(fixtureDir);
|
||||
Directory.CreateDirectory(Path.Combine(fixtureDir, "raw"));
|
||||
Directory.CreateDirectory(Path.Combine(fixtureDir, "normalized"));
|
||||
Directory.CreateDirectory(Path.Combine(fixtureDir, "expected"));
|
||||
|
||||
string contentPath;
|
||||
string sha256Hash;
|
||||
|
||||
if (string.IsNullOrEmpty(source))
|
||||
{
|
||||
Console.WriteLine("No source provided - manual fixture creation mode");
|
||||
Console.WriteLine($"Place fixture content in: {Path.Combine(fixtureDir, "raw")}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if source is URL or file path
|
||||
if (Uri.TryCreate(source, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
|
||||
{
|
||||
Console.WriteLine($"Fetching from URL: {source}");
|
||||
contentPath = await FetchFromUrlAsync(source, fixtureDir);
|
||||
}
|
||||
else if (File.Exists(source))
|
||||
{
|
||||
Console.WriteLine($"Copying from file: {source}");
|
||||
contentPath = await CopyFromFileAsync(source, fixtureDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"ERROR: Invalid source - not a valid URL or file path: {source}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute SHA-256 hash
|
||||
sha256Hash = await ComputeSha256Async(contentPath);
|
||||
Console.WriteLine($"SHA-256: {sha256Hash}");
|
||||
|
||||
// Create metadata
|
||||
var meta = new FixtureMeta
|
||||
{
|
||||
Id = id,
|
||||
Source = Uri.TryCreate(source, UriKind.Absolute, out _) ? "url" : "file",
|
||||
SourceUrl = Uri.TryCreate(source, UriKind.Absolute, out _) ? source : null,
|
||||
RetrievedAt = DateTime.UtcNow.ToString("O"),
|
||||
License = "CC0-1.0", // Default; update manually if needed
|
||||
Sha256 = sha256Hash,
|
||||
RefreshPolicy = "manual", // Default
|
||||
Notes = $"Harvested from: {source}",
|
||||
Tier = "T2", // Real sample default
|
||||
};
|
||||
|
||||
var metaPath = Path.Combine(fixtureDir, "meta.json");
|
||||
var metaJson = JsonSerializer.Serialize(meta, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
});
|
||||
await File.WriteAllTextAsync(metaPath, metaJson);
|
||||
|
||||
Console.WriteLine($"✓ Fixture harvested: {fixtureDir}");
|
||||
Console.WriteLine($" Meta: {metaPath}");
|
||||
Console.WriteLine($" Content: {contentPath}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Next steps:");
|
||||
Console.WriteLine("1. Review and update meta.json (license, tier, notes, etc.)");
|
||||
Console.WriteLine("2. Add fixture to fixtures.manifest.yml");
|
||||
Console.WriteLine("3. Run: fixture-harvester validate");
|
||||
}
|
||||
|
||||
private static async Task<string> FetchFromUrlAsync(string url, string fixtureDir)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
var response = await client.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var filename = Path.GetFileName(new Uri(url).LocalPath);
|
||||
if (string.IsNullOrEmpty(filename) || filename == "/")
|
||||
{
|
||||
filename = "downloaded.json";
|
||||
}
|
||||
|
||||
var rawPath = Path.Combine(fixtureDir, "raw", filename);
|
||||
await using var fileStream = File.Create(rawPath);
|
||||
await response.Content.CopyToAsync(fileStream);
|
||||
|
||||
return rawPath;
|
||||
}
|
||||
|
||||
private static async Task<string> CopyFromFileAsync(string sourcePath, string fixtureDir)
|
||||
{
|
||||
var filename = Path.GetFileName(sourcePath);
|
||||
var rawPath = Path.Combine(fixtureDir, "raw", filename);
|
||||
|
||||
await using var sourceStream = File.OpenRead(sourcePath);
|
||||
await using var destStream = File.Create(rawPath);
|
||||
await sourceStream.CopyToAsync(destStream);
|
||||
|
||||
return rawPath;
|
||||
}
|
||||
|
||||
private static async Task<string> ComputeSha256Async(string filePath)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
await using var stream = File.OpenRead(filePath);
|
||||
var hashBytes = await sha256.ComputeHashAsync(stream);
|
||||
return "sha256:" + BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
59
src/__Tests/Tools/FixtureHarvester/Commands/RegenCommand.cs
Normal file
59
src/__Tests/Tools/FixtureHarvester/Commands/RegenCommand.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
// <copyright file="RegenCommand.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Testing.FixtureHarvester.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Regen command - regenerate expected outputs (use with caution)
|
||||
/// </summary>
|
||||
internal static class RegenCommand
|
||||
{
|
||||
internal static Task ExecuteAsync(string? fixture, bool all, bool confirm)
|
||||
{
|
||||
if (!confirm)
|
||||
{
|
||||
Console.WriteLine("ERROR: Regeneration requires --confirm flag");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("WARNING: This command will regenerate expected outputs for test fixtures.");
|
||||
Console.WriteLine(" Only use this after MANUALLY VERIFYING that the new outputs are correct.");
|
||||
Console.WriteLine(" Incorrect regeneration can mask bugs and break determinism guarantees.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Usage:");
|
||||
Console.WriteLine(" fixture-harvester regen --fixture <id> --confirm");
|
||||
Console.WriteLine(" fixture-harvester regen --all --confirm");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (all)
|
||||
{
|
||||
Console.WriteLine("Regenerating all fixtures...");
|
||||
Console.WriteLine("(Not implemented - manual process required)");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Steps:");
|
||||
Console.WriteLine("1. Run Scanner/VexLens/etc with fixture inputs");
|
||||
Console.WriteLine("2. Capture outputs to expected/ directory");
|
||||
Console.WriteLine("3. Compute and record expected hash in meta.json");
|
||||
Console.WriteLine("4. Run: fixture-harvester validate");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(fixture))
|
||||
{
|
||||
Console.WriteLine("ERROR: Either --fixture <id> or --all must be specified");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Regenerating fixture: {fixture}");
|
||||
Console.WriteLine("(Not implemented - manual process required)");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Steps:");
|
||||
Console.WriteLine($"1. Locate fixture: src/__Tests/fixtures/*/{fixture}");
|
||||
Console.WriteLine("2. Run relevant tool with fixture inputs");
|
||||
Console.WriteLine("3. Copy output to expected/ directory");
|
||||
Console.WriteLine("4. Update meta.json with new expected hash");
|
||||
Console.WriteLine("5. Run: fixture-harvester validate");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
176
src/__Tests/Tools/FixtureHarvester/Commands/ValidateCommand.cs
Normal file
176
src/__Tests/Tools/FixtureHarvester/Commands/ValidateCommand.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
// <copyright file="ValidateCommand.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Testing.FixtureHarvester.Models;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace StellaOps.Testing.FixtureHarvester.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Validate command - verify fixture integrity and manifest consistency
|
||||
/// </summary>
|
||||
internal static class ValidateCommand
|
||||
{
|
||||
internal static async Task ExecuteAsync(string path)
|
||||
{
|
||||
Console.WriteLine($"Validating fixtures in: {path}");
|
||||
Console.WriteLine();
|
||||
|
||||
var manifestPath = Path.Combine(path, "fixtures.manifest.yml");
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
Console.WriteLine($"ERROR: Manifest not found: {manifestPath}");
|
||||
Console.WriteLine("Create fixtures.manifest.yml first.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Load manifest
|
||||
var yamlContent = await File.ReadAllTextAsync(manifestPath);
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.Build();
|
||||
|
||||
var manifest = deserializer.Deserialize<FixtureManifest>(yamlContent);
|
||||
|
||||
int totalFixtures = 0;
|
||||
int validFixtures = 0;
|
||||
int errors = 0;
|
||||
|
||||
// Validate SBOM fixtures
|
||||
Console.WriteLine("Validating SBOM fixtures...");
|
||||
foreach (var fixture in manifest.Fixtures.Sbom)
|
||||
{
|
||||
totalFixtures++;
|
||||
if (await ValidateFixtureAsync(path, "sbom", fixture.Id))
|
||||
{
|
||||
validFixtures++;
|
||||
}
|
||||
else
|
||||
{
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Feed fixtures
|
||||
Console.WriteLine("\nValidating Feed fixtures...");
|
||||
foreach (var fixture in manifest.Fixtures.Feeds)
|
||||
{
|
||||
totalFixtures++;
|
||||
if (await ValidateFixtureAsync(path, "feeds", fixture.Id))
|
||||
{
|
||||
validFixtures++;
|
||||
}
|
||||
else
|
||||
{
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate VEX fixtures
|
||||
Console.WriteLine("\nValidating VEX fixtures...");
|
||||
foreach (var fixture in manifest.Fixtures.Vex)
|
||||
{
|
||||
totalFixtures++;
|
||||
if (await ValidateFixtureAsync(path, "vex", fixture.Id))
|
||||
{
|
||||
validFixtures++;
|
||||
}
|
||||
else
|
||||
{
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("=".PadRight(50, '='));
|
||||
Console.WriteLine($"Total fixtures: {totalFixtures}");
|
||||
Console.WriteLine($"Valid: {validFixtures}");
|
||||
Console.WriteLine($"Errors: {errors}");
|
||||
|
||||
if (errors == 0)
|
||||
{
|
||||
Console.WriteLine("✓ All fixtures valid!");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"✗ {errors} fixture(s) failed validation");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> ValidateFixtureAsync(string basePath, string type, string id)
|
||||
{
|
||||
var fixtureDir = Path.Combine(basePath, type, id);
|
||||
var metaPath = Path.Combine(fixtureDir, "meta.json");
|
||||
|
||||
Console.Write($" [{id}] ");
|
||||
|
||||
// Check directory exists
|
||||
if (!Directory.Exists(fixtureDir))
|
||||
{
|
||||
Console.WriteLine($"✗ Directory not found: {fixtureDir}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check meta.json exists
|
||||
if (!File.Exists(metaPath))
|
||||
{
|
||||
Console.WriteLine($"✗ meta.json not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load and validate meta
|
||||
var metaJson = await File.ReadAllTextAsync(metaPath);
|
||||
FixtureMeta? meta;
|
||||
try
|
||||
{
|
||||
meta = JsonSerializer.Deserialize<FixtureMeta>(metaJson);
|
||||
if (meta == null)
|
||||
{
|
||||
Console.WriteLine("✗ Invalid meta.json format");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Console.WriteLine($"✗ Failed to parse meta.json: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify raw directory exists and has files
|
||||
var rawDir = Path.Combine(fixtureDir, "raw");
|
||||
if (!Directory.Exists(rawDir) || !Directory.EnumerateFiles(rawDir).Any())
|
||||
{
|
||||
Console.WriteLine("✗ No files in raw/ directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify SHA-256 hash
|
||||
var rawFiles = Directory.GetFiles(rawDir);
|
||||
if (rawFiles.Length > 0)
|
||||
{
|
||||
var actualHash = await ComputeSha256Async(rawFiles[0]);
|
||||
if (actualHash != meta.Sha256)
|
||||
{
|
||||
Console.WriteLine($"✗ Hash mismatch! Expected: {meta.Sha256}, Got: {actualHash}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("✓ Valid");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<string> ComputeSha256Async(string filePath)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
await using var stream = File.OpenRead(filePath);
|
||||
var hashBytes = await sha256.ComputeHashAsync(stream);
|
||||
return "sha256:" + BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RootNamespace>StellaOps.Testing.FixtureHarvester.Tests</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FixtureHarvester.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
19
src/__Tests/Tools/FixtureHarvester/FixtureHarvester.csproj
Normal file
19
src/__Tests/Tools/FixtureHarvester/FixtureHarvester.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RootNamespace>StellaOps.Testing.FixtureHarvester</RootNamespace>
|
||||
<AssemblyName>fixture-harvester</AssemblyName>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
169
src/__Tests/Tools/FixtureHarvester/FixtureValidationTests.cs
Normal file
169
src/__Tests/Tools/FixtureHarvester/FixtureValidationTests.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
// <copyright file="FixtureValidationTests.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Text.Json;
|
||||
using StellaOps.Testing.FixtureHarvester.Models;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Testing.FixtureHarvester.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for fixture infrastructure
|
||||
/// </summary>
|
||||
public sealed class FixtureValidationTests
|
||||
{
|
||||
private const string FixturesBasePath = "../../../fixtures";
|
||||
private readonly string _manifestPath = Path.Combine(FixturesBasePath, "fixtures.manifest.yml");
|
||||
|
||||
[Fact(Skip = "Fixtures not yet populated")]
|
||||
public void ManifestFile_Exists_AndIsValid()
|
||||
{
|
||||
// Arrange & Act
|
||||
var exists = File.Exists(_manifestPath);
|
||||
|
||||
// Assert
|
||||
Assert.True(exists, $"fixtures.manifest.yml should exist at {_manifestPath}");
|
||||
}
|
||||
|
||||
[Fact(Skip = "Fixtures not yet populated")]
|
||||
public async Task ManifestFile_CanBeParsed_Successfully()
|
||||
{
|
||||
// Arrange
|
||||
if (!File.Exists(_manifestPath))
|
||||
{
|
||||
// Skip if manifest doesn't exist yet
|
||||
return;
|
||||
}
|
||||
|
||||
var yamlContent = await File.ReadAllTextAsync(_manifestPath);
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var manifest = deserializer.Deserialize<FixtureManifest>(yamlContent);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(manifest);
|
||||
Assert.Equal("1.0", manifest.SchemaVersion);
|
||||
Assert.NotNull(manifest.Fixtures);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Fixtures not yet populated")]
|
||||
public async Task AllFixtures_HaveValidMetadata()
|
||||
{
|
||||
// Arrange
|
||||
if (!File.Exists(_manifestPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var yamlContent = await File.ReadAllTextAsync(_manifestPath);
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.Build();
|
||||
var manifest = deserializer.Deserialize<FixtureManifest>(yamlContent);
|
||||
|
||||
var allFixtures = new List<(string Type, string Id)>();
|
||||
foreach (var sbom in manifest.Fixtures.Sbom)
|
||||
{
|
||||
allFixtures.Add(("sbom", sbom.Id));
|
||||
}
|
||||
|
||||
foreach (var feed in manifest.Fixtures.Feeds)
|
||||
{
|
||||
allFixtures.Add(("feeds", feed.Id));
|
||||
}
|
||||
|
||||
foreach (var vex in manifest.Fixtures.Vex)
|
||||
{
|
||||
allFixtures.Add(("vex", vex.Id));
|
||||
}
|
||||
|
||||
// Act & Assert
|
||||
foreach (var (type, id) in allFixtures)
|
||||
{
|
||||
var fixtureDir = Path.Combine(FixturesBasePath, type, id);
|
||||
var metaPath = Path.Combine(fixtureDir, "meta.json");
|
||||
|
||||
if (!File.Exists(metaPath))
|
||||
{
|
||||
// Fixture not yet created - skip
|
||||
continue;
|
||||
}
|
||||
|
||||
var metaJson = await File.ReadAllTextAsync(metaPath);
|
||||
var meta = JsonSerializer.Deserialize<FixtureMeta>(metaJson);
|
||||
|
||||
Assert.NotNull(meta);
|
||||
Assert.Equal(id, meta.Id);
|
||||
Assert.NotEmpty(meta.Source);
|
||||
Assert.NotEmpty(meta.License);
|
||||
Assert.NotEmpty(meta.Sha256);
|
||||
Assert.StartsWith("sha256:", meta.Sha256);
|
||||
Assert.NotEmpty(meta.RefreshPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Skip = "Fixtures not yet populated")]
|
||||
public async Task AllFixtures_HaveRawDirectory()
|
||||
{
|
||||
// Arrange
|
||||
if (!File.Exists(_manifestPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var yamlContent = await File.ReadAllTextAsync(_manifestPath);
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.Build();
|
||||
var manifest = deserializer.Deserialize<FixtureManifest>(yamlContent);
|
||||
|
||||
var allFixtures = new List<(string Type, string Id)>();
|
||||
foreach (var sbom in manifest.Fixtures.Sbom)
|
||||
{
|
||||
allFixtures.Add(("sbom", sbom.Id));
|
||||
}
|
||||
|
||||
foreach (var feed in manifest.Fixtures.Feeds)
|
||||
{
|
||||
allFixtures.Add(("feeds", feed.Id));
|
||||
}
|
||||
|
||||
foreach (var vex in manifest.Fixtures.Vex)
|
||||
{
|
||||
allFixtures.Add(("vex", vex.Id));
|
||||
}
|
||||
|
||||
// Act & Assert
|
||||
foreach (var (type, id) in allFixtures)
|
||||
{
|
||||
var fixtureDir = Path.Combine(FixturesBasePath, type, id);
|
||||
var rawDir = Path.Combine(fixtureDir, "raw");
|
||||
|
||||
if (!Directory.Exists(fixtureDir))
|
||||
{
|
||||
// Fixture not yet created - skip
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.True(Directory.Exists(rawDir), $"Fixture {id} should have raw/ directory");
|
||||
}
|
||||
}
|
||||
|
||||
[Theory(Skip = "Fixtures not yet populated")]
|
||||
[InlineData("T0")]
|
||||
[InlineData("T1")]
|
||||
[InlineData("T2")]
|
||||
[InlineData("T3")]
|
||||
public void FixtureTiers_AreDocumented(string tier)
|
||||
{
|
||||
// This is a documentation test to ensure all tiers are defined
|
||||
var validTiers = new[] { "T0", "T1", "T2", "T3" };
|
||||
Assert.Contains(tier, validTiers);
|
||||
}
|
||||
}
|
||||
64
src/__Tests/Tools/FixtureHarvester/Models/FixtureManifest.cs
Normal file
64
src/__Tests/Tools/FixtureHarvester/Models/FixtureManifest.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
// <copyright file="FixtureManifest.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Testing.FixtureHarvester.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Root manifest listing all fixture sets
|
||||
/// </summary>
|
||||
/// <param name="SchemaVersion">Manifest schema version</param>
|
||||
/// <param name="Fixtures">Fixture sets grouped by category</param>
|
||||
public sealed record FixtureManifest(
|
||||
string SchemaVersion,
|
||||
FixtureSets Fixtures
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Fixture sets grouped by category
|
||||
/// </summary>
|
||||
/// <param name="Sbom">SBOM fixture definitions</param>
|
||||
/// <param name="Feeds">Feed snapshot fixtures</param>
|
||||
/// <param name="Vex">VEX document fixtures</param>
|
||||
public sealed record FixtureSets(
|
||||
List<SbomFixtureDef> Sbom,
|
||||
List<FeedFixtureDef> Feeds,
|
||||
List<VexFixtureDef> Vex
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// SBOM fixture definition
|
||||
/// </summary>
|
||||
public sealed record SbomFixtureDef
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required string Source { get; init; }
|
||||
public string? ImageDigest { get; init; }
|
||||
public string? ExpectedSbomHash { get; init; }
|
||||
public required string RefreshPolicy { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Feed snapshot fixture definition
|
||||
/// </summary>
|
||||
public sealed record FeedFixtureDef
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required string Source { get; init; }
|
||||
public int Count { get; init; }
|
||||
public required string CapturedAt { get; init; }
|
||||
public required string Sha256 { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX document fixture definition
|
||||
/// </summary>
|
||||
public sealed record VexFixtureDef
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required string Source { get; init; }
|
||||
public required string Sha256 { get; init; }
|
||||
}
|
||||
60
src/__Tests/Tools/FixtureHarvester/Models/FixtureMeta.cs
Normal file
60
src/__Tests/Tools/FixtureHarvester/Models/FixtureMeta.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
// <copyright file="FixtureMeta.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Testing.FixtureHarvester.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Per-fixture metadata
|
||||
/// </summary>
|
||||
public sealed record FixtureMeta
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique fixture identifier
|
||||
/// </summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source type (local-build, url, api, etc.)
|
||||
/// </summary>
|
||||
public required string Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional source URL for remote fixtures
|
||||
/// </summary>
|
||||
public string? SourceUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when fixture was retrieved/created (ISO 8601 UTC)
|
||||
/// </summary>
|
||||
public required string RetrievedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// License identifier (SPDX format)
|
||||
/// </summary>
|
||||
public required string License { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 hash of fixture content
|
||||
/// </summary>
|
||||
public required string Sha256 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Refresh policy (manual, daily, weekly, etc.)
|
||||
/// </summary>
|
||||
public required string RefreshPolicy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional notes about the fixture
|
||||
/// </summary>
|
||||
public string? Notes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Fixture tier (T0-T3)
|
||||
/// T0: Synthetic (generated, minimal)
|
||||
/// T1: Spec examples (from standards)
|
||||
/// T2: Real samples (production-like)
|
||||
/// T3: Regression (captures actual bugs)
|
||||
/// </summary>
|
||||
public string? Tier { get; init; }
|
||||
}
|
||||
76
src/__Tests/Tools/FixtureHarvester/Program.cs
Normal file
76
src/__Tests/Tools/FixtureHarvester/Program.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
// <copyright file="Program.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.CommandLine;
|
||||
using StellaOps.Testing.FixtureHarvester.Commands;
|
||||
|
||||
namespace StellaOps.Testing.FixtureHarvester;
|
||||
|
||||
/// <summary>
|
||||
/// Fixture Harvester CLI entry point
|
||||
/// </summary>
|
||||
internal static class Program
|
||||
{
|
||||
internal static async Task<int> Main(string[] args)
|
||||
{
|
||||
var rootCommand = new RootCommand("Stella Ops Fixture Harvester - Acquire, curate, and pin test fixtures");
|
||||
|
||||
// Harvest command
|
||||
var harvestCommand = new Command("harvest", "Harvest and store a fixture with metadata");
|
||||
var harvestTypeOption = new Option<string>(
|
||||
"--type",
|
||||
description: "Fixture type: sbom, feed, vex") { IsRequired = true };
|
||||
var harvestIdOption = new Option<string>(
|
||||
"--id",
|
||||
description: "Unique fixture identifier") { IsRequired = true };
|
||||
var harvestSourceOption = new Option<string>(
|
||||
"--source",
|
||||
description: "Source URL or path");
|
||||
var harvestOutputOption = new Option<string>(
|
||||
"--output",
|
||||
description: "Output directory",
|
||||
getDefaultValue: () => "src/__Tests/fixtures");
|
||||
|
||||
harvestCommand.AddOption(harvestTypeOption);
|
||||
harvestCommand.AddOption(harvestIdOption);
|
||||
harvestCommand.AddOption(harvestSourceOption);
|
||||
harvestCommand.AddOption(harvestOutputOption);
|
||||
harvestCommand.SetHandler(HarvestCommand.ExecuteAsync, harvestTypeOption, harvestIdOption, harvestSourceOption, harvestOutputOption);
|
||||
|
||||
// Validate command
|
||||
var validateCommand = new Command("validate", "Validate fixtures against manifest");
|
||||
var validatePathOption = new Option<string>(
|
||||
"--path",
|
||||
description: "Fixtures directory path",
|
||||
getDefaultValue: () => "src/__Tests/fixtures");
|
||||
|
||||
validateCommand.AddOption(validatePathOption);
|
||||
validateCommand.SetHandler(ValidateCommand.ExecuteAsync, validatePathOption);
|
||||
|
||||
// Regen command
|
||||
var regenCommand = new Command("regen", "Regenerate expected outputs (manual, use with caution)");
|
||||
var regenFixtureOption = new Option<string>(
|
||||
"--fixture",
|
||||
description: "Fixture ID to regenerate");
|
||||
var regenAllOption = new Option<bool>(
|
||||
"--all",
|
||||
description: "Regenerate all fixtures",
|
||||
getDefaultValue: () => false);
|
||||
var regenConfirmOption = new Option<bool>(
|
||||
"--confirm",
|
||||
description: "Confirm regeneration",
|
||||
getDefaultValue: () => false);
|
||||
|
||||
regenCommand.AddOption(regenFixtureOption);
|
||||
regenCommand.AddOption(regenAllOption);
|
||||
regenCommand.AddOption(regenConfirmOption);
|
||||
regenCommand.SetHandler(RegenCommand.ExecuteAsync, regenFixtureOption, regenAllOption, regenConfirmOption);
|
||||
|
||||
rootCommand.AddCommand(harvestCommand);
|
||||
rootCommand.AddCommand(validateCommand);
|
||||
rootCommand.AddCommand(regenCommand);
|
||||
|
||||
return await rootCommand.InvokeAsync(args);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user