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>
This commit is contained in:
parent
e06d5a2e8c
commit
de78734f4d
10 changed files with 696 additions and 100 deletions
138
tests/GameCult.Caching.Tests/C2aGateWriter.cs
Normal file
138
tests/GameCult.Caching.Tests/C2aGateWriter.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GameCult.Caching.Tests
|
||||
{
|
||||
// The writer half of the C2a format gate. It writes a record that holds element ids through CultCacheMessagePack.Create, once
|
||||
// as a single-file store and once as a directory store, and asserts the headers C2a stamps. old-reader-refusal.sh gate runs it
|
||||
// with CULTLIB_GATE_OUT set, so the 2.0.1 reader probes the very bytes this tree writes; the committed vectors are one such run.
|
||||
public class C2aGateWriter
|
||||
{
|
||||
private const string SingleName = "c2a-written-ids.msgpack";
|
||||
private const string DirectoryName = "c2a-written-ids-directory";
|
||||
private const string ManifestName = "store.cc";
|
||||
private const string DirectoryHeader = "cultcache.store.v5.directory-content-addressed-pages";
|
||||
|
||||
private static readonly CultDocumentRegistry Registry = CultDocumentRegistry.ForTypes(new[] { typeof(ElementIdTests.IdDeck) });
|
||||
|
||||
// Every id is the author's, so the written bytes and the decoded ids do not depend on minting.
|
||||
private static readonly string[] Ids = { "00000000c2a1", "00000000c2a2", "00000000c2a3", "00000000c2a4" };
|
||||
|
||||
private static ElementIdTests.IdDeck Deck() => new()
|
||||
{
|
||||
Name = "gate",
|
||||
Reels =
|
||||
{
|
||||
new ElementIdTests.IdReel
|
||||
{
|
||||
Label = "reel",
|
||||
Id = Ids[0],
|
||||
Marks = { new ElementIdTests.IdMark { Text = "m", Id = Ids[1] }, new ElementIdTests.IdMark { Text = "m", Id = Ids[2] } }
|
||||
}
|
||||
},
|
||||
Moves = { new ElementIdTests.IdSlash { Reach = 1, Id = Ids[3] } }
|
||||
};
|
||||
|
||||
private static string[] IdsOf(ElementIdTests.IdDeck deck) =>
|
||||
deck.Reels.Select(reel => reel.Id)
|
||||
.Concat(deck.Reels.SelectMany(reel => reel.Marks.Select(mark => mark.Id)))
|
||||
.Concat(deck.Moves.Select(move => move.Id))
|
||||
.ToArray();
|
||||
|
||||
private static void Write(string path, bool directory)
|
||||
{
|
||||
using var cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = Registry, UseDirectoryStore = directory });
|
||||
cache.Commit(batch => batch.Upsert(typeof(ElementIdTests.IdDeck), Deck(), new CultRecordKey("gate")));
|
||||
}
|
||||
|
||||
private static string HeaderOf(string path) =>
|
||||
CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).FormatVersion;
|
||||
|
||||
private static string[] ReadIds(string path)
|
||||
{
|
||||
using var cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = Registry });
|
||||
return IdsOf(cache.Get<ElementIdTests.IdDeck>(new CultRecordKey("gate"))!);
|
||||
}
|
||||
|
||||
private static string Vectors()
|
||||
{
|
||||
for (var dir = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); dir != null; dir = dir.Parent)
|
||||
{
|
||||
var vectors = Path.Combine(dir.FullName, "tests", "vectors", "document-variants-c2a");
|
||||
if (Directory.Exists(vectors)) return vectors;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException($"tests/vectors/document-variants-c2a not found above {TestContext.CurrentContext.TestDirectory}.");
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
private string _temp = string.Empty;
|
||||
|
||||
[SetUp]
|
||||
public void CreateTemp()
|
||||
{
|
||||
_temp = Path.Combine(Path.GetTempPath(), $"cultlib-c2a-gate-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_temp);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void DeleteTemp()
|
||||
{
|
||||
if (Directory.Exists(_temp))
|
||||
Directory.Delete(_temp, recursive: true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void C2aStampsV3OnASingleFileStoreAndV5OnADirectoryStoreThatHoldIds()
|
||||
{
|
||||
var output = Environment.GetEnvironmentVariable("CULTLIB_GATE_OUT");
|
||||
var root = string.IsNullOrEmpty(output) ? _temp : output;
|
||||
Directory.CreateDirectory(root);
|
||||
var single = Path.Combine(root, SingleName);
|
||||
var directory = Path.Combine(root, DirectoryName);
|
||||
File.Delete(single);
|
||||
if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true);
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
Write(single, directory: false);
|
||||
Write(Path.Combine(directory, ManifestName), directory: true);
|
||||
|
||||
Assert.That(HeaderOf(single), Is.EqualTo(CultPersistedStoreSnapshot.FormatV3));
|
||||
Assert.That(HeaderOf(Path.Combine(directory, ManifestName)), Is.EqualTo(DirectoryHeader));
|
||||
Assert.That(ReadIds(single), Is.EqualTo(Ids));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheCommittedVectorsAreWhatC2aWrites()
|
||||
{
|
||||
var live = Path.Combine(_temp, "live");
|
||||
Directory.CreateDirectory(live);
|
||||
Write(Path.Combine(live, SingleName), directory: false);
|
||||
Write(Path.Combine(live, ManifestName), directory: true);
|
||||
|
||||
var committedSingle = Path.Combine(Vectors(), SingleName);
|
||||
var committedManifest = Path.Combine(Vectors(), DirectoryName, ManifestName);
|
||||
Assert.That(HeaderOf(committedSingle), Is.EqualTo(HeaderOf(Path.Combine(live, SingleName))));
|
||||
Assert.That(HeaderOf(committedManifest), Is.EqualTo(HeaderOf(Path.Combine(live, ManifestName))));
|
||||
|
||||
// The committed bytes are read through a copy: opening a store may take its lock beside it.
|
||||
var copy = Path.Combine(_temp, "copy");
|
||||
CopyTree(Vectors(), copy);
|
||||
Assert.That(ReadIds(Path.Combine(copy, SingleName)), Is.EqualTo(ReadIds(Path.Combine(live, SingleName))));
|
||||
Assert.That(ReadIds(Path.Combine(copy, DirectoryName, ManifestName)), Is.EqualTo(Ids));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -106,6 +106,38 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
}
|
||||
|
||||
// The store the 1.0.60 writer wrote (tests/vectors/ref-list-drift/v1-0-60-written.cc, made by OldWriterRefListProbe), or the one
|
||||
// old-reader-refusal.sh gate csharp names in CULTLIB_GATE_OLD_STORE. The line it prints is the one OldReaderRefListProbe prints
|
||||
// at 2.0.1; the gate diffs them.
|
||||
[Test]
|
||||
public async Task A_1_0_60_Written_Store_Opens_Like_2_0_1()
|
||||
{
|
||||
var source = Environment.GetEnvironmentVariable("CULTLIB_GATE_OLD_STORE");
|
||||
var copy = Path.Combine(Path.GetTempPath(), $"reflist-1060-{Guid.NewGuid():N}.cc");
|
||||
File.Copy(string.IsNullOrEmpty(source) ? VectorPath("v1-0-60-written.cc") : source, copy);
|
||||
try
|
||||
{
|
||||
using var cache = new CultCache();
|
||||
var store = new SingleFileMessagePackBackingStore(copy);
|
||||
cache.AddBackingStore(store);
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
var warnings = store.LastSchemaMigrationReports.SelectMany(report => report.Warnings).Select(warning => warning.Code).Distinct().OrderBy(code => code, StringComparer.Ordinal).ToArray();
|
||||
Assert.That(warnings, Does.Contain("gained_reference_semantics"));
|
||||
var items = cache.GetAll<RefListHolder>().Single().Items.Select(item => item.Key.Value).ToArray();
|
||||
Assert.That(items, Is.EqualTo(new[] { "alpha", "beta" }));
|
||||
Assert.That(cache.Get<RefListTarget>(cache.GetAll<RefListHolder>().Single().Items[0].Key)!.Name, Is.EqualTo("alpha"));
|
||||
|
||||
store.PushAll();
|
||||
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(copy));
|
||||
var records = snapshot.Records.OrderBy(record => record.Key, StringComparer.Ordinal).Select(record => record.Key + "=" + record.SchemaId).ToArray();
|
||||
TestContext.Out.WriteLine("REFLIST-SUMMARY " + System.Text.Json.JsonSerializer.Serialize(new { records, items, warnings, header = snapshot.FormatVersion }));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(copy);
|
||||
}
|
||||
}
|
||||
|
||||
private static CultSchemaMigrationReport Resolve(Action<CultSchemaMemberCatalogEntry> persistedItems)
|
||||
{
|
||||
var catalog = CultDocumentRegistry.Shared.GetRequired<RefListHolder>().ToCatalogEntry();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
#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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
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.
|
||||
// It attaches the store the 1.0.60 writer produced and prints the REFLIST-SUMMARY line that
|
||||
// RefListDriftTests.A_1_0_60_Written_Store_Opens_Like_2_0_1 prints at head; 'gate csharp' diffs the two lines.
|
||||
public class OldReaderRefListProbe
|
||||
{
|
||||
[CultDocument("tests.reflist_target", "tests.reflist_target.v1")]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
internal sealed class RefListTarget
|
||||
{
|
||||
[Key(0)]
|
||||
[CultName]
|
||||
public string Name = string.Empty;
|
||||
}
|
||||
|
||||
[CultDocument("tests.reflist_holder", "tests.reflist_holder.v1")]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
internal sealed class RefListHolder
|
||||
{
|
||||
[Key(0)]
|
||||
[CultName]
|
||||
public string Name = string.Empty;
|
||||
|
||||
[Key(1)]
|
||||
public CultRecordRef<RefListTarget>[] Items = Array.Empty<CultRecordRef<RefListTarget>>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task A_1_0_60_Written_Store_Attaches_As_Compatible_Drift()
|
||||
{
|
||||
var copy = Path.Combine(Path.GetTempPath(), $"reflist-gate-{Guid.NewGuid():N}.cc");
|
||||
File.Copy(Path.Combine(Environment.GetEnvironmentVariable("CULTLIB_GATE_OUT")!, "v1-0-60-ref-list.cc"), copy);
|
||||
try
|
||||
{
|
||||
using var cache = new CultCache();
|
||||
var store = new SingleFileMessagePackBackingStore(copy);
|
||||
cache.AddBackingStore(store);
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
var warnings = store.LastSchemaMigrationReports.SelectMany(report => report.Warnings).Select(warning => warning.Code).Distinct().OrderBy(code => code, StringComparer.Ordinal).ToArray();
|
||||
Assert.That(warnings, Does.Contain("gained_reference_semantics"));
|
||||
var items = cache.GetAll<RefListHolder>().Single().Items.Select(item => item.Key.Value).ToArray();
|
||||
Assert.That(items, Is.EqualTo(new[] { "alpha", "beta" }));
|
||||
|
||||
store.PushAll();
|
||||
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(copy));
|
||||
var records = snapshot.Records.OrderBy(record => record.Key, StringComparer.Ordinal).Select(record => record.Key + "=" + record.SchemaId).ToArray();
|
||||
TestContext.Out.WriteLine("REFLIST-SUMMARY " + JsonSerializer.Serialize(new { records, items, warnings, header = snapshot.FormatVersion }));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GameCult.Caching.Tests
|
||||
{
|
||||
// Copied into a checkout of the C0 merge (e382bb4) by old-reader-refusal.sh. Not part of this tree's suite.
|
||||
public class OldReaderV3Probe
|
||||
{
|
||||
[Test]
|
||||
public void ACopyOfV3BaseIsRefusedByHeader()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"cultlib-oldreader-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(root);
|
||||
try
|
||||
{
|
||||
var file = Path.Combine(root, "store.msgpack");
|
||||
File.Copy(Path.Combine(Environment.GetEnvironmentVariable("CULTLIB_ROOT")!, "tests", "vectors", "document-variants-c2a", "v3-base.msgpack"), file);
|
||||
using var cache = new CultCache();
|
||||
var error = Assert.Catch(() => cache.AddBackingStore(new SingleFileMessagePackBackingStore(file)));
|
||||
Assert.That(error, Is.Not.Null);
|
||||
TestContext.Out.WriteLine("OLD-READER csharp refused: " + error!.Message);
|
||||
Assert.That(error.ToString(), Does.Contain("cultcache.store.v3"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,15 +4,42 @@ import { tmpdir } from "node:os";
|
|||
import { join, resolve } from "node:path";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { documentPayloadFromBytes } from "../src/document";
|
||||
import { SingleFileMessagePackBackingStore } from "../src/single-file-messagepack-backing-store";
|
||||
|
||||
// Copied into a checkout of the C0 merge (e382bb4) by old-reader-refusal.sh. Not part of this tree's suite.
|
||||
test("old TypeScript reader refuses the v3 element-id store by header", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "cultcache-oldreader-"));
|
||||
const file = join(dir, "store.msgpack");
|
||||
await writeFile(file, await readFile(resolve(__dirname, "../../../../tests/vectors/document-variants-c2a/v3-base.msgpack")));
|
||||
await assert.rejects(() => new SingleFileMessagePackBackingStore(file).pullAll(), (error: Error) => {
|
||||
console.log("OLD-READER typescript refused: " + error.message);
|
||||
// Copied into a checkout of cultcache-rs-v0.3.1 (02cf04b5) by old-reader-refusal.sh. Not part of this tree's suite.
|
||||
// The vector is the store C2aGateWriter wrote through CultCacheMessagePack.Create.
|
||||
const vector = resolve(__dirname, "../../../../tests/vectors/document-variants-c2a/c2a-written-ids.msgpack");
|
||||
const envelope = { key: "probe", type: "probe", payload: documentPayloadFromBytes(new Uint8Array([0x80])), storedAt: "2026-10-22T00:00:00Z" };
|
||||
|
||||
async function copyOfTheVector(): Promise<{ file: string; bytes: Buffer }> {
|
||||
const bytes = await readFile(vector);
|
||||
const file = join(await mkdtemp(join(tmpdir(), "cultcache-oldreader-")), "store.msgpack");
|
||||
await writeFile(file, bytes);
|
||||
return { file, bytes };
|
||||
}
|
||||
|
||||
function refusesV3(label: string) {
|
||||
return (error: Error) => {
|
||||
console.log(`OLD-READER typescript refused ${label}: ` + error.message);
|
||||
return /cultcache\.store\.v3/u.test(error.message);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
test("old TypeScript reader refuses the C2a element-id store at read", async () => {
|
||||
const { file, bytes } = await copyOfTheVector();
|
||||
await assert.rejects(() => new SingleFileMessagePackBackingStore(file).pullAll(), refusesV3("pullAll"));
|
||||
assert.equal(Buffer.compare(await readFile(file), bytes), 0);
|
||||
});
|
||||
|
||||
test("old TypeScript reader refuses a per-record push to the C2a element-id store", async () => {
|
||||
const { file, bytes } = await copyOfTheVector();
|
||||
await assert.rejects(() => new SingleFileMessagePackBackingStore(file).push(envelope), refusesV3("push"));
|
||||
assert.equal(Buffer.compare(await readFile(file), bytes), 0);
|
||||
});
|
||||
|
||||
test("old TypeScript reader refuses a whole-store pushAll over the C2a element-id store", async () => {
|
||||
const { file, bytes } = await copyOfTheVector();
|
||||
await assert.rejects(() => new SingleFileMessagePackBackingStore(file).pushAll([envelope]), refusesV3("pushAll"));
|
||||
assert.equal(Buffer.compare(await readFile(file), bytes), 0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,53 @@
|
|||
// Copied into a checkout of the C0 merge (e382bb4) by old-reader-refusal.sh. Not part of this tree's suite.
|
||||
use cultcache_rs::{CacheBackingStore, SingleFileMessagePackBackingStore};
|
||||
// Copied into a checkout of cultcache-rs-v0.3.1 (02cf04b5) by old-reader-refusal.sh. Not part of this tree's suite.
|
||||
// The vector is the store C2aGateWriter wrote through CultCacheMessagePack.Create.
|
||||
use cultcache_rs::{CacheBackingStore, CultCacheEnvelope, PushAllOptions, SingleFileMessagePackBackingStore};
|
||||
|
||||
#[test]
|
||||
fn old_rust_reader_refuses_the_v3_element_id_store_by_header() {
|
||||
fn copy_of_the_vector() -> (tempfile::TempDir, std::path::PathBuf, Vec<u8>) {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let path = temp.path().join("store.msgpack");
|
||||
let vector = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../tests/vectors/document-variants-c2a/v3-base.msgpack");
|
||||
.join("../../tests/vectors/document-variants-c2a/c2a-written-ids.msgpack");
|
||||
std::fs::copy(vector, &path).unwrap();
|
||||
let error = SingleFileMessagePackBackingStore::new(&path).pull_all().unwrap_err();
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
(temp, path, bytes)
|
||||
}
|
||||
|
||||
fn envelope() -> CultCacheEnvelope {
|
||||
CultCacheEnvelope {
|
||||
key: "probe".into(),
|
||||
r#type: "probe".into(),
|
||||
payload: vec![0x80],
|
||||
stored_at: "2026-10-22T00:00:00Z".into(),
|
||||
schema_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn refused_v3(label: &str, error: anyhow::Error) {
|
||||
let message = format!("{error:#}");
|
||||
println!("OLD-READER rust refused: {message}");
|
||||
println!("OLD-READER rust refused {label}: {message}");
|
||||
assert!(message.contains("cultcache.store.v3"), "{message}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_rust_reader_refuses_the_c2a_element_id_store_at_read() {
|
||||
let (_temp, path, bytes) = copy_of_the_vector();
|
||||
refused_v3("pull_all", SingleFileMessagePackBackingStore::new(&path).pull_all().unwrap_err());
|
||||
assert_eq!(std::fs::read(&path).unwrap(), bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_rust_reader_refuses_a_per_record_push_to_the_c2a_element_id_store() {
|
||||
let (_temp, path, bytes) = copy_of_the_vector();
|
||||
refused_v3("push", SingleFileMessagePackBackingStore::new(&path).push(&envelope()).unwrap_err());
|
||||
assert_eq!(std::fs::read(&path).unwrap(), bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_rust_reader_refuses_a_whole_store_push_all_over_the_c2a_element_id_store() {
|
||||
let (_temp, path, bytes) = copy_of_the_vector();
|
||||
let error = SingleFileMessagePackBackingStore::new(&path)
|
||||
.push_all(std::slice::from_ref(&envelope()), PushAllOptions::default())
|
||||
.unwrap_err();
|
||||
refused_v3("push_all", error);
|
||||
assert_eq!(std::fs::read(&path).unwrap(), bytes);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,20 +2,39 @@ import tempfile
|
|||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from cultcache_py import SingleFileMessagePackBackingStore
|
||||
from cultcache_py import CultCacheEnvelope, SingleFileMessagePackBackingStore
|
||||
|
||||
|
||||
# Copied into a checkout of the C0 merge (e382bb4) by old-reader-refusal.sh. Not part of this tree's suite.
|
||||
# Copied into a checkout of cultcache-rs-v0.3.1 (02cf04b5) by old-reader-refusal.sh. Not part of this tree's suite.
|
||||
# The vector is the store C2aGateWriter wrote through CultCacheMessagePack.Create.
|
||||
class OldReaderV3(unittest.TestCase):
|
||||
def test_old_python_reader_refuses_the_v3_element_id_store_by_header(self) -> None:
|
||||
vector = Path(__file__).resolve().parents[3] / "tests" / "vectors" / "document-variants-c2a" / "v3-base.msgpack"
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
store_path = Path(tmp) / "store.msgpack"
|
||||
store_path.write_bytes(vector.read_bytes())
|
||||
with self.assertRaises(ValueError) as caught:
|
||||
SingleFileMessagePackBackingStore(store_path).pull_all()
|
||||
print("OLD-READER python refused:", caught.exception)
|
||||
def setUp(self) -> None:
|
||||
vector = Path(__file__).resolve().parents[3] / "tests" / "vectors" / "document-variants-c2a" / "c2a-written-ids.msgpack"
|
||||
self._tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self._tmp.cleanup)
|
||||
self.path = Path(self._tmp.name) / "store.msgpack"
|
||||
self.bytes = vector.read_bytes()
|
||||
self.path.write_bytes(self.bytes)
|
||||
self.envelope = CultCacheEnvelope(key="probe", type="probe", payload=b"\x80", stored_at="2026-10-22T00:00:00Z")
|
||||
|
||||
def refuses_v3(self, label: str, action) -> None:
|
||||
with self.assertRaises(ValueError) as caught:
|
||||
action(SingleFileMessagePackBackingStore(self.path))
|
||||
print(f"OLD-READER python refused {label}:", caught.exception)
|
||||
self.assertIn("cultcache.store.v3", str(caught.exception))
|
||||
self.assertEqual(self.path.read_bytes(), self.bytes)
|
||||
|
||||
def test_old_python_reader_refuses_the_c2a_element_id_store_at_read(self) -> None:
|
||||
self.refuses_v3("pull_all", lambda store: store.pull_all())
|
||||
|
||||
def test_old_python_reader_refuses_a_per_record_push(self) -> None:
|
||||
self.refuses_v3("push", lambda store: store.push(self.envelope))
|
||||
|
||||
def test_old_python_reader_refuses_a_whole_store_push_all(self) -> None:
|
||||
self.refuses_v3("push_all", lambda store: store.push_all([self.envelope]))
|
||||
|
||||
def test_old_python_reader_refuses_a_delete(self) -> None:
|
||||
self.refuses_v3("delete", lambda store: store.delete("probe", "probe"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,46 +1,128 @@
|
|||
#!/usr/bin/env bash
|
||||
# Proves that a reader from before element ids refuses the v3 header, per runtime. The old reader is the code at the C0
|
||||
# merge (e382bb4), which accepts only cultcache.store.v1; OLD_READER_REV overrides it (the C1 merge, for the C# reader that
|
||||
# reads v2).
|
||||
# The C2a format gate: a reader from before element ids must refuse what C2a writes, and a store the 1.0.60 writer made must
|
||||
# open alike at 2.0.1 and at the tree under test. Each old revision is a resolved SHA with its tag beside it, because the
|
||||
# verification clone holds ancestors, not tags; an environment variable overrides each.
|
||||
#
|
||||
# old-reader-refusal.sh stage
|
||||
# Run in a CultLib clone. Commits the old revision plus the v3 vector and the probes in old-reader-probes/ onto a
|
||||
# scratch ref and prints its SHA. The tree at that SHA is the old reader with one extra test.
|
||||
# old-reader-refusal.sh probe csharp|typescript|python|rust
|
||||
# Run at the root of the staged tree (a verification job given that SHA), with the runtime's toolchain on PATH.
|
||||
# Exit status zero means the old reader refused the header by name; the refusal message is printed.
|
||||
# C# OLD_READER_REV_CSHARP b8fc5eb0 cultlib-unity-v2.0.1
|
||||
# TypeScript, OLD_READER_REV_LEAF 02cf04b5 cultcache-rs-v0.3.1 (the whole-store writer fix; no cultcache-ts tag carries it)
|
||||
# Python, Rust
|
||||
# 1.0.60 writer OLD_WRITER_REV 45c2f400 cultlib-unity-v1.0.60
|
||||
#
|
||||
# old-reader-refusal.sh stage csharp|typescript|python|rust|writer
|
||||
# Run in a CultLib clone. Commits the old revision plus the C2a-written vectors and that runtime's probes onto a scratch
|
||||
# ref and prints its SHA. The tree at that SHA is the old code with extra tests ("writer" is the 1.0.60 writer).
|
||||
# old-reader-refusal.sh probe csharp|typescript|python|rust|writer
|
||||
# Run at the root of a staged tree, with the runtime's toolchain on PATH. Exit status zero means every probe passed; the
|
||||
# refusal messages are printed.
|
||||
# old-reader-refusal.sh gate csharp|typescript|python|rust
|
||||
# Run in a CultLib clone at the revision under test: stage, then probe. For csharp, C2aGateWriter first writes the live
|
||||
# v3 and v5 stores into $GATE_OUT (default /tmp/gate) and the 2.0.1 probes read them beside the committed vectors; the
|
||||
# 1.0.60 writer then writes a ref-list store there, 2.0.1 and the tree under test both open it, and their REFLIST-SUMMARY
|
||||
# lines must be identical.
|
||||
set -euo pipefail
|
||||
|
||||
here="tests/vectors/document-variants-c2a"
|
||||
self="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
|
||||
OLD_READER_REV_CSHARP="${OLD_READER_REV_CSHARP:-b8fc5eb0}" # cultlib-unity-v2.0.1
|
||||
OLD_READER_REV_LEAF="${OLD_READER_REV_LEAF:-02cf04b5}" # cultcache-rs-v0.3.1
|
||||
OLD_WRITER_REV="${OLD_WRITER_REV:-45c2f400}" # cultlib-unity-v1.0.60
|
||||
|
||||
usage() { echo "usage: $0 stage|probe|gate csharp|typescript|python|rust (writer for stage and probe)" >&2; exit 2; }
|
||||
|
||||
revision_for() {
|
||||
case "$1" in
|
||||
csharp) echo "$OLD_READER_REV_CSHARP" ;;
|
||||
typescript|python|rust) echo "$OLD_READER_REV_LEAF" ;;
|
||||
writer) echo "$OLD_WRITER_REV" ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
}
|
||||
|
||||
stage() {
|
||||
local runtime="$1" root rev old sha
|
||||
root="$(git rev-parse --show-toplevel)"
|
||||
rev="$(revision_for "$runtime")"
|
||||
old="$(mktemp -d)/old"
|
||||
git -C "$root" worktree add --detach "$old" "$rev" >/dev/null
|
||||
mkdir -p "$old/$here"
|
||||
case "$runtime" in
|
||||
csharp)
|
||||
cp "$root/$here/c2a-written-ids.msgpack" "$old/$here/"
|
||||
cp -r "$root/$here/c2a-written-ids-directory" "$old/$here/"
|
||||
cp "$root/$here/old-reader-probes/OldReaderGateProbe.cs" "$root/$here/old-reader-probes/OldReaderRefListProbe.cs" "$old/tests/GameCult.Caching.Tests/" ;;
|
||||
typescript)
|
||||
cp "$root/$here/c2a-written-ids.msgpack" "$old/$here/"
|
||||
cp "$root/$here/old-reader-probes/old-reader-v3.test.ts" "$old/packages/cultcache-ts/test/" ;;
|
||||
python)
|
||||
cp "$root/$here/c2a-written-ids.msgpack" "$old/$here/"
|
||||
cp "$root/$here/old-reader-probes/test_old_reader_v3.py" "$old/packages/cultcache-py/tests/" ;;
|
||||
rust)
|
||||
cp "$root/$here/c2a-written-ids.msgpack" "$old/$here/"
|
||||
mkdir -p "$old/packages/cultcache-rs/tests"
|
||||
cp "$root/$here/old-reader-probes/old_reader_v3.rs" "$old/packages/cultcache-rs/tests/" ;;
|
||||
writer)
|
||||
cp "$root/$here/old-writer-probes/OldWriterRefListProbe.cs" "$old/tests/GameCult.Caching.Tests/" ;;
|
||||
esac
|
||||
git -C "$old" add -A
|
||||
git -C "$old" -c user.name=old-reader-refusal -c user.email=old-reader-refusal@localhost commit -q -m "$runtime probe over $rev"
|
||||
sha="$(git -C "$old" rev-parse HEAD)"
|
||||
git -C "$root" update-ref "refs/old-reader-probe/$sha" "$sha"
|
||||
git -C "$root" worktree remove --force "$old"
|
||||
echo "$sha"
|
||||
}
|
||||
|
||||
probe() {
|
||||
case "$1" in
|
||||
csharp) CULTLIB_ROOT="$PWD" dotnet test tests/GameCult.Caching.Tests --filter "FullyQualifiedName~OldReaderGateProbe|FullyQualifiedName~OldReaderRefListProbe" --logger "console;verbosity=detailed" ;;
|
||||
writer) dotnet test tests/GameCult.Caching.Tests --filter FullyQualifiedName~OldWriterRefListProbe --logger "console;verbosity=detailed" ;;
|
||||
typescript)
|
||||
npm install --no-audit --no-fund >/dev/null 2>&1
|
||||
cd packages/cultcache-ts
|
||||
node ../../node_modules/typescript/bin/tsc -p tsconfig.json
|
||||
node ../../node_modules/typescript/bin/tsc -p tsconfig.test.json
|
||||
node --test dist-test/test/old-reader-v3.test.js ;;
|
||||
python) PYTHONPATH=packages/cultcache-py/src python3 packages/cultcache-py/tests/test_old_reader_v3.py -v ;;
|
||||
rust) cd packages/cultcache-rs && cargo test --test old_reader_v3 -- --nocapture ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Stage a runtime's probes, then run them in a worktree of the staged commit.
|
||||
probe_staged() {
|
||||
local sha tree
|
||||
sha="$(stage "$1")"
|
||||
tree="$(mktemp -d)/staged"
|
||||
git worktree add --detach "$tree" "$sha" >/dev/null
|
||||
(cd "$tree" && "$self" probe "$1")
|
||||
git worktree remove --force "$tree"
|
||||
}
|
||||
|
||||
summary_of() { grep -o 'REFLIST-SUMMARY .*' "$1" | sort -u; }
|
||||
|
||||
gate() {
|
||||
case "$1" in
|
||||
csharp)
|
||||
local out="${GATE_OUT:-/tmp/gate}"
|
||||
rm -rf "$out"; mkdir -p "$out"
|
||||
export CULTLIB_GATE_OUT="$out"
|
||||
dotnet test tests/GameCult.Caching.Tests --filter FullyQualifiedName~C2aGateWriter --logger "console;verbosity=detailed"
|
||||
probe_staged writer
|
||||
probe_staged csharp 2>&1 | tee "$out/reader.log"
|
||||
CULTLIB_GATE_OLD_STORE="$out/v1-0-60-ref-list.cc" dotnet test tests/GameCult.Caching.Tests \
|
||||
--filter "FullyQualifiedName~RefListDriftTests.A_1_0_60_Written_Store_Opens_Like_2_0_1" --logger "console;verbosity=detailed" 2>&1 | tee "$out/head.log"
|
||||
summary_of "$out/reader.log" > "$out/reader.summary"
|
||||
summary_of "$out/head.log" > "$out/head.summary"
|
||||
test "$(wc -l < "$out/reader.summary")" -eq 1 && test "$(wc -l < "$out/head.summary")" -eq 1
|
||||
cat "$out/reader.summary" "$out/head.summary"
|
||||
diff "$out/reader.summary" "$out/head.summary" && echo "GATE csharp: the 2.0.1 and head REFLIST-SUMMARY lines are identical" ;;
|
||||
typescript|python|rust) probe_staged "$1" ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
stage)
|
||||
root="$(git rev-parse --show-toplevel)"
|
||||
here="tests/vectors/document-variants-c2a"
|
||||
old="$(mktemp -d)/old"
|
||||
git -C "$root" worktree add --detach "$old" "${OLD_READER_REV:-e382bb4}" >/dev/null
|
||||
mkdir -p "$old/$here"
|
||||
cp "$root/$here/v3-base.msgpack" "$old/$here/"
|
||||
cp "$root/$here/old-reader-probes/OldReaderV3Probe.cs" "$old/tests/GameCult.Caching.Tests/"
|
||||
cp "$root/$here/old-reader-probes/old-reader-v3.test.ts" "$old/packages/cultcache-ts/test/"
|
||||
cp "$root/$here/old-reader-probes/test_old_reader_v3.py" "$old/packages/cultcache-py/tests/"
|
||||
mkdir -p "$old/packages/cultcache-rs/tests"
|
||||
cp "$root/$here/old-reader-probes/old_reader_v3.rs" "$old/packages/cultcache-rs/tests/"
|
||||
git -C "$old" add -A
|
||||
git -C "$old" commit -q -m "old-reader probe over ${OLD_READER_REV:-e382bb4}"
|
||||
sha="$(git -C "$old" rev-parse HEAD)"
|
||||
git -C "$root" update-ref "refs/old-reader-probe/$sha" "$sha"
|
||||
git -C "$root" worktree remove --force "$old"
|
||||
echo "$sha" ;;
|
||||
probe)
|
||||
case "${2:-}" in
|
||||
csharp) CULTLIB_ROOT="$PWD" dotnet test tests/GameCult.Caching.Tests --filter FullyQualifiedName~OldReaderV3Probe --logger "console;verbosity=detailed" ;;
|
||||
typescript)
|
||||
npm install --no-audit --no-fund >/dev/null 2>&1
|
||||
cd packages/cultcache-ts
|
||||
node ../../node_modules/typescript/bin/tsc -p tsconfig.json
|
||||
node ../../node_modules/typescript/bin/tsc -p tsconfig.test.json
|
||||
node --test dist-test/test/old-reader-v3.test.js ;;
|
||||
python) PYTHONPATH=packages/cultcache-py/src python3 packages/cultcache-py/tests/test_old_reader_v3.py -v ;;
|
||||
rust) cd packages/cultcache-rs && cargo test --test old_reader_v3 -- --nocapture ;;
|
||||
*) echo "usage: $0 probe csharp|typescript|python|rust" >&2; exit 2 ;;
|
||||
esac ;;
|
||||
*) echo "usage: $0 stage | probe <runtime>" >&2; exit 2 ;;
|
||||
stage) stage "${2:-}" ;;
|
||||
probe) probe "${2:-}" ;;
|
||||
gate) gate "${2:-}" ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
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-v1.0.60 (45c2f400) by old-reader-refusal.sh. Not part of this tree's suite.
|
||||
// It writes the store the 1.0.x writer produces for a list of record references (the shapes are RefListDriftTests'), and
|
||||
// is the only author of tests/vectors/ref-list-drift/v1-0-60-written.cc.
|
||||
public class OldWriterRefListProbe
|
||||
{
|
||||
[CultDocument("tests.reflist_target", "tests.reflist_target.v1")]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
internal sealed class RefListTarget
|
||||
{
|
||||
[Key(0)]
|
||||
[CultName]
|
||||
public string Name = string.Empty;
|
||||
}
|
||||
|
||||
[CultDocument("tests.reflist_holder", "tests.reflist_holder.v1")]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
internal sealed class RefListHolder
|
||||
{
|
||||
[Key(0)]
|
||||
[CultName]
|
||||
public string Name = string.Empty;
|
||||
|
||||
[Key(1)]
|
||||
public CultRecordRef<RefListTarget>[] Items = Array.Empty<CultRecordRef<RefListTarget>>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WritesAStoreWhoseRefListSlotIsAPlainValue()
|
||||
{
|
||||
var output = Environment.GetEnvironmentVariable("CULTLIB_GATE_OUT")!;
|
||||
Directory.CreateDirectory(output);
|
||||
var file = Path.Combine(output, "v1-0-60-ref-list.cc");
|
||||
File.Delete(file);
|
||||
|
||||
using var cache = new CultCache();
|
||||
var store = new SingleFileMessagePackBackingStore(file);
|
||||
cache.AddBackingStore(store);
|
||||
await cache.AddAsync(new RefListTarget { Name = "alpha" });
|
||||
await cache.AddAsync(new RefListTarget { Name = "beta" });
|
||||
await cache.AddAsync(new RefListHolder
|
||||
{
|
||||
Name = "list",
|
||||
Items = new[] { new CultRecordRef<RefListTarget>(new CultRecordKey("alpha")), new CultRecordRef<RefListTarget>(new CultRecordKey("beta")) }
|
||||
});
|
||||
store.PushAll();
|
||||
|
||||
var header = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(file)).FormatVersion;
|
||||
Assert.That(header, Is.EqualTo("cultcache.store.v1"));
|
||||
TestContext.Out.WriteLine("OLD-WRITER 1.0.60 wrote " + file + " as " + header);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue