using System.Text; using System.Text.Json; using System.Runtime.CompilerServices; using Xunit; namespace StellaOps.TestKit.Assertions; /// /// Provides snapshot testing assertions for golden master testing. /// Snapshots are stored in the test project's `Snapshots/` directory. /// /// /// Usage: /// /// [Fact] /// public void TestSbomGeneration() /// { /// var sbom = GenerateSbom(); /// /// // Snapshot will be stored in Snapshots/TestSbomGeneration.json /// SnapshotAssert.MatchesSnapshot(sbom, snapshotName: "TestSbomGeneration"); /// } /// /// /// To update snapshots (e.g., after intentional changes), set environment variable: /// UPDATE_SNAPSHOTS=1 dotnet test /// public static class SnapshotAssert { private static readonly bool UpdateSnapshotsMode = global::System.Environment.GetEnvironmentVariable("UPDATE_SNAPSHOTS") == "1"; /// /// Resolves the default snapshot directory for a caller. /// /// /// If the caller source file lives inside a Snapshots directory, that directory is used so /// snapshots stay source-controlled and deterministic across environments. /// Otherwise the legacy runtime path (current working directory/Snapshots) is used. /// public static string ResolveDefaultSnapshotsDirectory([CallerFilePath] string callerFilePath = "") { if (!string.IsNullOrWhiteSpace(callerFilePath)) { var callerDirectory = Path.GetDirectoryName(callerFilePath); if (!string.IsNullOrWhiteSpace(callerDirectory)) { if (string.Equals( Path.GetFileName(callerDirectory), "Snapshots", StringComparison.OrdinalIgnoreCase)) { return callerDirectory; } var siblingSnapshots = Path.Combine(callerDirectory, "Snapshots"); if (Directory.Exists(siblingSnapshots)) { return siblingSnapshots; } } } return Path.Combine(Directory.GetCurrentDirectory(), "Snapshots"); } /// /// Asserts that the value matches the stored snapshot. If UPDATE_SNAPSHOTS=1, updates the snapshot. /// /// The value to snapshot (will be JSON-serialized). /// The snapshot name (filename without extension). /// Optional directory for snapshots (default: "Snapshots" in test project). public static void MatchesSnapshot( T value, string snapshotName, string? snapshotsDirectory = null, [CallerFilePath] string callerFilePath = "") { snapshotsDirectory ??= ResolveDefaultSnapshotsDirectory(callerFilePath); Directory.CreateDirectory(snapshotsDirectory); string snapshotPath = Path.Combine(snapshotsDirectory, $"{snapshotName}.json"); // Serialize to pretty JSON for readability string actualJson = JsonSerializer.Serialize(value, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); if (UpdateSnapshotsMode) { // Update snapshot File.WriteAllText(snapshotPath, actualJson, Encoding.UTF8); return; // Don't assert in update mode } // Verify snapshot exists Assert.True(File.Exists(snapshotPath), $"Snapshot '{snapshotName}' not found at {snapshotPath}. Run with UPDATE_SNAPSHOTS=1 to create it."); // Compare with stored snapshot string expectedJson = File.ReadAllText(snapshotPath, Encoding.UTF8); Assert.Equal(expectedJson, actualJson); } /// /// Asserts that the text matches the stored snapshot. /// public static void MatchesTextSnapshot( string value, string snapshotName, string? snapshotsDirectory = null, [CallerFilePath] string callerFilePath = "") { snapshotsDirectory ??= ResolveDefaultSnapshotsDirectory(callerFilePath); Directory.CreateDirectory(snapshotsDirectory); string snapshotPath = Path.Combine(snapshotsDirectory, $"{snapshotName}.txt"); if (UpdateSnapshotsMode) { File.WriteAllText(snapshotPath, value, Encoding.UTF8); return; } Assert.True(File.Exists(snapshotPath), $"Snapshot '{snapshotName}' not found at {snapshotPath}. Run with UPDATE_SNAPSHOTS=1 to create it."); string expected = File.ReadAllText(snapshotPath, Encoding.UTF8); Assert.Equal(expected, value); } /// /// Asserts that binary data matches the stored snapshot. /// public static void MatchesBinarySnapshot( byte[] value, string snapshotName, string? snapshotsDirectory = null, [CallerFilePath] string callerFilePath = "") { snapshotsDirectory ??= ResolveDefaultSnapshotsDirectory(callerFilePath); Directory.CreateDirectory(snapshotsDirectory); string snapshotPath = Path.Combine(snapshotsDirectory, $"{snapshotName}.bin"); if (UpdateSnapshotsMode) { File.WriteAllBytes(snapshotPath, value); return; } Assert.True(File.Exists(snapshotPath), $"Snapshot '{snapshotName}' not found at {snapshotPath}. Run with UPDATE_SNAPSHOTS=1 to create it."); byte[] expected = File.ReadAllBytes(snapshotPath); Assert.Equal(expected, value); } }