162 lines
5.7 KiB
C#
162 lines
5.7 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Runtime.CompilerServices;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.TestKit.Assertions;
|
|
|
|
/// <summary>
|
|
/// Provides snapshot testing assertions for golden master testing.
|
|
/// Snapshots are stored in the test project's `Snapshots/` directory.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Usage:
|
|
/// <code>
|
|
/// [Fact]
|
|
/// public void TestSbomGeneration()
|
|
/// {
|
|
/// var sbom = GenerateSbom();
|
|
///
|
|
/// // Snapshot will be stored in Snapshots/TestSbomGeneration.json
|
|
/// SnapshotAssert.MatchesSnapshot(sbom, snapshotName: "TestSbomGeneration");
|
|
/// }
|
|
/// </code>
|
|
///
|
|
/// To update snapshots (e.g., after intentional changes), set environment variable:
|
|
/// UPDATE_SNAPSHOTS=1 dotnet test
|
|
/// </remarks>
|
|
public static class SnapshotAssert
|
|
{
|
|
private static readonly bool UpdateSnapshotsMode =
|
|
global::System.Environment.GetEnvironmentVariable("UPDATE_SNAPSHOTS") == "1";
|
|
|
|
/// <summary>
|
|
/// Resolves the default snapshot directory for a caller.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the caller source file lives inside a <c>Snapshots</c> directory, that directory is used so
|
|
/// snapshots stay source-controlled and deterministic across environments.
|
|
/// Otherwise the legacy runtime path (<c>current working directory/Snapshots</c>) is used.
|
|
/// </remarks>
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that the value matches the stored snapshot. If UPDATE_SNAPSHOTS=1, updates the snapshot.
|
|
/// </summary>
|
|
/// <param name="value">The value to snapshot (will be JSON-serialized).</param>
|
|
/// <param name="snapshotName">The snapshot name (filename without extension).</param>
|
|
/// <param name="snapshotsDirectory">Optional directory for snapshots (default: "Snapshots" in test project).</param>
|
|
public static void MatchesSnapshot<T>(
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that the text matches the stored snapshot.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that binary data matches the stored snapshot.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|