CultLib/tests/GameCult.Caching.Tests/PreCut2StoreFormatTests.cs
Meta Vens 66bd07b941 Pin the legacy-layout and missing-header refusals
The legacy catalog layout reaches CultMesh's fallback reader, and a nil header
was the only defaulted header; each gets one vector or test. The C# store
tests catch the refusal where the store attaches.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:35:57 +02:00

196 lines
8.6 KiB
C#

#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using NUnit.Framework;
namespace GameCult.Caching.Tests
{
public class PreCut2StoreFormatTests
{
private static string FixtureRoot =>
Path.Combine(TestContext.CurrentContext.TestDirectory, "Fixtures", "pre-cut2");
private static string CopyFixture(string name)
{
var target = Path.Combine(Path.GetTempPath(), $"cultlib-precut2-{Guid.NewGuid():N}");
var source = Path.Combine(FixtureRoot, name);
foreach (var file in Directory.GetFiles(source, "*", SearchOption.AllDirectories))
{
var destination = Path.Combine(target, Path.GetRelativePath(source, file));
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
File.Copy(file, destination);
}
return target;
}
private static void AssertFixtureRecords(CultCache cache)
{
Assert.That(cache.Get<PreCut2FixtureItem>(new CultRecordKey("item:anvil")), Is.Not.Null);
Assert.That(cache.Get<PreCut2FixtureItem>(new CultRecordKey("item:anvil"))!.Name, Is.EqualTo("anvil"));
Assert.That(cache.Get<PreCut2FixtureItem>(new CultRecordKey("item:anvil"))!.Count, Is.EqualTo(3));
Assert.That(cache.Get<PreCut2FixtureItem>(new CultRecordKey("item:bellows"))?.Name, Is.EqualTo("bellows"));
Assert.That(cache.Get<PreCut2FixtureItem>(new CultRecordKey("item:bellows"))?.Count, Is.EqualTo(42));
Assert.That(cache.Get<PreCut2FixtureNote>(new CultRecordKey("note:origin"))?.Text,
Is.EqualTo("written before cut 2"));
Assert.That(cache.GetAll<PreCut2FixtureItem>(), Has.Length.EqualTo(2));
Assert.That(cache.GetAll<PreCut2FixtureNote>(), Has.Length.EqualTo(1));
}
[Test]
public async Task DirectoryStoreOpensV4ManifestWrittenBeforeCut2()
{
var root = CopyFixture("directory-v4");
try
{
var manifest = Path.Combine(root, "store.cc");
Assert.That(CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(manifest)).FormatVersion,
Is.EqualTo("cultcache.store.v4.directory-content-addressed-pages"));
using var cache = new CultCache();
cache.AddBackingStore(new DirectoryMessagePackBackingStore(manifest));
await cache.PullAllBackingStoresAsync();
AssertFixtureRecords(cache);
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
[Test]
public async Task SingleFileOpensV1SnapshotWrittenBeforeCut2()
{
var root = CopyFixture("single-file-v1");
try
{
var file = Path.Combine(root, "store.msgpack");
Assert.That(CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(file)).FormatVersion,
Is.EqualTo("cultcache.store.v1"));
using var cache = new CultCache();
cache.AddBackingStore(new SingleFileMessagePackBackingStore(file));
await cache.PullAllBackingStoresAsync();
AssertFixtureRecords(cache);
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
// Shared refusal vectors: tests/vectors/document-variants-c0, read by every runtime's tests.
private const string ItemSchemaId = "sha256:88d3fdf0a927acf3b163940d8f8c7fe62b3316542ce771a67ec8bc038f594788";
private static string VectorPath(string name)
{
for (var dir = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); dir != null; dir = dir.Parent)
{
var candidate = Path.Combine(dir.FullName, "tests", "vectors", "document-variants-c0", name);
if (File.Exists(candidate)) return candidate;
}
throw new FileNotFoundException($"Shared vector {name} not found above {TestContext.CurrentContext.TestDirectory}.");
}
private static string Refusal(string vector)
{
var root = Path.Combine(Path.GetTempPath(), $"cultlib-vector-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
try
{
var file = Path.Combine(root, "store.msgpack");
File.Copy(VectorPath(vector), file);
using var cache = new CultCache();
var error = Assert.Catch(() => cache.AddBackingStore(new SingleFileMessagePackBackingStore(file)));
Assert.That(error, Is.Not.Null);
Assert.That(error!.ToString(), Does.Contain(nameof(NotSupportedException)));
return error.ToString();
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
[Test]
public void SingleFileRefusesUnknownHeaderByName()
{
Assert.That(Refusal("unknown-header.msgpack"), Does.Contain("cultcache.store.v9"));
}
[Test]
public void SingleFileRefusesExtraRecordSlotNamingTheRecord()
{
var message = Refusal("extra-slot-full-payload.msgpack");
Assert.That(message, Does.Contain("item:anvil"));
Assert.That(message, Does.Contain(ItemSchemaId));
}
[Test]
public void SingleFileRefusesVariantStoreByVersionOrRecord()
{
Assert.That(Refusal("variant-v2.msgpack"),
Does.Contain("cultcache.store.v2").Or.Contain("item:anvil-big"));
}
[Test]
public void SnapshotWithoutAFormatVersionIsRefusedNotDefaultedToV1()
{
// [nil, [], []]
var bytes = new byte[] { 0x93, 0xc0, 0x90, 0x90 };
Assert.That(() => CultDocumentMessagePackSerialization.DeserializeSnapshot(bytes),
Throws.TypeOf<NotSupportedException>().With.Message.Contains("format version"));
}
[Test]
public void V1StoreWrittenAtTheBaseCommitStillDecodesByteForByte()
{
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(VectorPath("v1-base.msgpack")));
CultDocumentMessagePackSerialization.RequireSingleFileFormat(snapshot);
Assert.That(snapshot.FormatVersion, Is.EqualTo("cultcache.store.v1"));
Assert.That(snapshot.Records.Select(record => record.Key), Is.EqualTo(new[] { "alpha", "beta" }));
Assert.That(snapshot.Records[0].Payload, Is.EqualTo(new byte[] { 0x92, 0xa5, (byte)'a', (byte)'l', (byte)'p', (byte)'h', (byte)'a', 0x01 }));
Assert.That(snapshot.Records[1].Payload, Is.EqualTo(new byte[] { 0x92, 0xa4, (byte)'b', (byte)'e', (byte)'t', (byte)'a', 0x02 }));
}
// Format strings taken from DirectoryMessagePackBackingStore.cs at 0db1fe5.
[TestCase("cultcache.store.v1")]
[TestCase("cultcache.store.v1.directory")]
[TestCase("cultcache.store.v2.directory-indexed")]
[TestCase("cultcache.store.v3.directory-immutable-pages")]
public void DirectoryStoreRefusesNonV4Manifest(string formatVersion)
{
var root = Path.Combine(Path.GetTempPath(), $"cultlib-refuse-{Guid.NewGuid():N}");
var manifest = Path.Combine(root, "store.cc");
Directory.CreateDirectory(root);
try
{
var bytes = CultDocumentMessagePackSerialization.SerializeSnapshot(new CultPersistedStoreSnapshot
{
FormatVersion = formatVersion,
SchemaCatalog = Array.Empty<CultSchemaCatalogEntry>(),
Records = Array.Empty<CultPersistedRecord>()
});
File.WriteAllBytes(manifest, bytes);
using var cache = new CultCache();
Assert.That(() => cache.AddBackingStore(new DirectoryMessagePackBackingStore(manifest)),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains(formatVersion));
Assert.That(cache.BackingStores, Is.Empty);
Assert.That(File.ReadAllBytes(manifest), Is.EqualTo(bytes));
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
}
}