CultLib/tests/vectors/document-variants-c2a/old-reader-probes/OldReaderGateProbe.cs
Meta Vens de78734f4d C2a format gate: the gate harness, a C# gate probe, leaf probes on the C2a-written vector, and the 1.0.60 ref-list probes
old-reader-refusal.sh gains gate <runtime> and per-runtime old revisions (b8fc5eb0 = cultlib-unity-v2.0.1,
02cf04b5 = cultcache-rs-v0.3.1, 45c2f400 = cultlib-unity-v1.0.60). C2aGateWriter writes the live v3 and v5 stores.
OldReaderGateProbe replaces OldReaderV3Probe. The vectors themselves follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:18:57 +02:00

164 lines
7.2 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using MessagePack;
using NUnit.Framework;
namespace GameCult.Caching.Tests
{
// Copied into a checkout of cultlib-unity-v2.0.1 (b8fc5eb0) by old-reader-refusal.sh. Not part of this tree's suite.
// The 2.0.1 reader meets the stores C2a writes. The source is "committed" (tests/vectors/document-variants-c2a) or, when
// CULTLIB_GATE_OUT is set, "live" (what C2aGateWriter just wrote at the tree under test). Every refusal names the format, and
// the store is byte-identical after it.
public class OldReaderGateProbe
{
private const string SingleName = "c2a-written-ids.msgpack";
private const string DirectoryName = "c2a-written-ids-directory";
private const string ManifestName = "store.cc";
[CultDocument("tests.gate_probe", "tests.gate_probe.v1")]
[MessagePackObject(AllowPrivate = true)]
internal sealed class GateProbeDocument
{
[Key(0)]
[CultName]
public string Name = string.Empty;
}
public static IEnumerable<string> Sources()
{
yield return "committed";
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CULTLIB_GATE_OUT")))
yield return "live";
}
private static string SourceRoot(string source) => source == "live"
? Environment.GetEnvironmentVariable("CULTLIB_GATE_OUT")!
: Path.Combine(Environment.GetEnvironmentVariable("CULTLIB_ROOT")!, "tests", "vectors", "document-variants-c2a");
private string _temp = string.Empty;
[SetUp]
public void CreateTemp()
{
_temp = Path.Combine(Path.GetTempPath(), $"cultlib-gate-{Guid.NewGuid():N}");
Directory.CreateDirectory(_temp);
}
[TearDown]
public void DeleteTemp()
{
if (Directory.Exists(_temp))
Directory.Delete(_temp, recursive: true);
}
private string CopySingle(string source, string name)
{
var file = Path.Combine(_temp, name);
File.Copy(Path.Combine(SourceRoot(source), SingleName), file);
return file;
}
private string CopyDirectory(string source, string name)
{
var root = Path.Combine(_temp, name);
CopyTree(Path.Combine(SourceRoot(source), DirectoryName), root);
return Path.Combine(root, ManifestName);
}
private static void CopyTree(string from, string to)
{
Directory.CreateDirectory(to);
foreach (var file in Directory.GetFiles(from))
File.Copy(file, Path.Combine(to, Path.GetFileName(file)));
foreach (var directory in Directory.GetDirectories(from))
CopyTree(directory, Path.Combine(to, Path.GetFileName(directory)));
}
// Every file under the root with its content hash, in a fixed order: a tree compares equal only if nothing in it moved.
private static string[] Tree(string root) =>
Directory.GetFiles(root, "*", SearchOption.AllDirectories)
.OrderBy(file => file, StringComparer.Ordinal)
.Select(file => Path.GetRelativePath(root, file) + " " + Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(file))))
.ToArray();
private static void Names(Exception? error, string format, string what)
{
Assert.That(error, Is.Not.Null, what);
TestContext.Out.WriteLine($"OLD-READER csharp refused {what}: " + error!.Message);
Assert.That(error.ToString(), Does.Contain(format), what);
}
[TestCaseSource(nameof(Sources))]
public void V3IsRefusedAtAttach(string source)
{
var file = CopySingle(source, "attach.cc");
var before = File.ReadAllBytes(file);
using var cache = new CultCache();
Names(Assert.Catch(() => cache.AddBackingStore(new SingleFileMessagePackBackingStore(file))), "cultcache.store.v3", "attach");
Assert.That(File.ReadAllBytes(file), Is.EqualTo(before));
}
[TestCaseSource(nameof(Sources))]
public void V3IsRefusedByPushAllOfAStoreThatWasNeverAttached(string source)
{
var file = CopySingle(source, "detached.cc");
var before = File.ReadAllBytes(file);
var store = new SingleFileMessagePackBackingStore(file);
Names(Assert.Catch(() => store.PushAll()), "cultcache.store.v3", "PushAll of a detached store");
Assert.That(File.ReadAllBytes(file), Is.EqualTo(before));
}
[TestCaseSource(nameof(Sources))]
public async Task V3SwappedInAfterAttachIsRefusedByFlushAndPushAll(string source)
{
var file = Path.Combine(_temp, "swap.cc");
using var cache = new CultCache();
var store = new SingleFileMessagePackBackingStore(file);
cache.AddBackingStore(store);
await cache.AddAsync(new GateProbeDocument { Name = "staged" });
File.Copy(Path.Combine(SourceRoot(source), SingleName), file, overwrite: true);
var before = File.ReadAllBytes(file);
Names(Assert.Catch(() => cache.FlushAllBackingStores()), "cultcache.store.v3", "flush after a swap");
Assert.That(File.ReadAllBytes(file), Is.EqualTo(before));
Names(Assert.Catch(() => store.PushAll()), "cultcache.store.v3", "PushAll after a swap");
Assert.That(File.ReadAllBytes(file), Is.EqualTo(before));
}
[TestCaseSource(nameof(Sources))]
public void V5IsRefusedByTheDirectoryStoreAtAttach(string source)
{
var manifest = CopyDirectory(source, "attach-directory");
var before = Tree(Path.GetDirectoryName(manifest)!);
using var cache = new CultCache();
Names(Assert.Catch(() => cache.AddBackingStore(new DirectoryMessagePackBackingStore(manifest))), "cultcache.store.v5", "directory attach");
Assert.That(Tree(Path.GetDirectoryName(manifest)!), Is.EqualTo(before));
}
[TestCaseSource(nameof(Sources))]
public async Task V5SwappedInAfterAttachIsRefusedByTheDirectoryStoreAtFlush(string source)
{
var root = Path.Combine(_temp, "swap-directory");
Directory.CreateDirectory(root);
var manifest = Path.Combine(root, ManifestName);
using var cache = new CultCache();
var store = new DirectoryMessagePackBackingStore(manifest);
cache.AddBackingStore(store);
await cache.AddAsync(new GateProbeDocument { Name = "staged" });
CopyTree(Path.Combine(SourceRoot(source), DirectoryName), root);
var before = Tree(root);
Names(Assert.Catch(() => cache.FlushAllBackingStores()), "cultcache.store.v5", "directory flush after a swap");
Assert.That(Tree(root), Is.EqualTo(before));
Names(Assert.Catch(() => store.PushAll()), "cultcache.store.v5", "directory PushAll after a swap");
Assert.That(Tree(root), Is.EqualTo(before));
}
}
}