Tests: the ref-list gate compares schema ids after one write at both trees; the C2a writer takes a fixed clock
The 1.0.60 ref-list summary is taken after attach, an upsert of alpha, beta and the list reversed, and a flush, at 2.0.1 and at head, and names records by schema id (the restamp-on-write ruling makes them equal). C2aGateWriter stamps every record with one instant and compares the committed vectors byte for byte, the directory store as relative path and bytes with lock files left out. The committed vectors are regenerated in the next commit, so TheCommittedVectorsAreWhatC2aWrites is red at this commit; it builds. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
338035eacb
commit
fd9bbaf4a4
3 changed files with 84 additions and 48 deletions
|
|
@ -44,9 +44,14 @@ namespace GameCult.Caching.Tests
|
|||
.Concat(deck.Moves.Select(move => move.Id))
|
||||
.ToArray();
|
||||
|
||||
// The one instant every record is stamped with, so the bytes this writes are the same on every run and the committed vectors
|
||||
// can be compared to them byte for byte. Regenerate the vectors with CULTLIB_GATE_OUT (see C2aStamps...) if C2a's format moves.
|
||||
private static readonly DateTimeOffset StoredAt = new(2026, 10, 27, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static void Write(string path, bool directory)
|
||||
{
|
||||
using var cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = Registry, UseDirectoryStore = directory });
|
||||
cache.Clock = () => StoredAt;
|
||||
cache.Commit(batch => batch.Upsert(typeof(ElementIdTests.IdDeck), Deck(), new CultRecordKey("gate")));
|
||||
}
|
||||
|
||||
|
|
@ -70,14 +75,13 @@ namespace GameCult.Caching.Tests
|
|||
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)));
|
||||
}
|
||||
// Every file under root by relative path and bytes; a store's lock files hold no state and are not compared.
|
||||
private static (string Path, byte[] Bytes)[] Tree(string root) =>
|
||||
Directory.GetFiles(root, "*", SearchOption.AllDirectories)
|
||||
.Where(file => !file.EndsWith(".lock", StringComparison.Ordinal))
|
||||
.Select(file => (Path.GetRelativePath(root, file).Replace(Path.DirectorySeparatorChar, '/'), File.ReadAllBytes(file)))
|
||||
.OrderBy(entry => entry.Item1, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
private string _temp = string.Empty;
|
||||
|
||||
|
|
@ -119,20 +123,18 @@ namespace GameCult.Caching.Tests
|
|||
public void TheCommittedVectorsAreWhatC2aWrites()
|
||||
{
|
||||
var live = Path.Combine(_temp, "live");
|
||||
Directory.CreateDirectory(live);
|
||||
var liveDirectory = Path.Combine(live, DirectoryName);
|
||||
Directory.CreateDirectory(liveDirectory);
|
||||
Write(Path.Combine(live, SingleName), directory: false);
|
||||
Write(Path.Combine(live, ManifestName), directory: true);
|
||||
Write(Path.Combine(liveDirectory, 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))));
|
||||
Assert.That(File.ReadAllBytes(Path.Combine(Vectors(), SingleName)), Is.EqualTo(File.ReadAllBytes(Path.Combine(live, SingleName))), SingleName);
|
||||
|
||||
// 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));
|
||||
var committed = Tree(Path.Combine(Vectors(), DirectoryName));
|
||||
var written = Tree(liveDirectory);
|
||||
Assert.That(committed.Select(entry => entry.Path), Is.EqualTo(written.Select(entry => entry.Path)), DirectoryName);
|
||||
for (var i = 0; i < committed.Length; i++)
|
||||
Assert.That(committed[i].Bytes, Is.EqualTo(written[i].Bytes), committed[i].Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,9 +107,10 @@ 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. A record is named by its schema name, not its id: a flush of an untouched record carries its
|
||||
// persisted id forward at this tree and restamps it at 2.0.1, so the id after PushAll is not what "opens alike" is about.
|
||||
// old-reader-refusal.sh gate csharp names in CULTLIB_GATE_OLD_STORE. It attaches the store, upserts alpha, beta and the list
|
||||
// reversed, flushes, rereads the file and prints the REFLIST-SUMMARY line OldReaderRefListProbe prints at 2.0.1 after the same
|
||||
// write; the gate diffs them, so the two must stay the same code. Ids are compared, not names: the write is what makes a record's
|
||||
// schema id the one its type owns, and a record left under its persisted id is a type that cannot write it.
|
||||
[Test]
|
||||
public async Task A_1_0_60_Written_Store_Opens_Like_2_0_1()
|
||||
{
|
||||
|
|
@ -118,20 +119,35 @@ namespace GameCult.Caching.Tests
|
|||
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"));
|
||||
string[] warnings;
|
||||
using (var cache = new CultCache())
|
||||
{
|
||||
var store = new SingleFileMessagePackBackingStore(copy);
|
||||
cache.AddBackingStore(store);
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
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 holder = cache.GetAll<RefListHolder>().Single();
|
||||
Assert.That(holder.Items.Select(item => item.Key.Value), Is.EqualTo(new[] { "alpha", "beta" }));
|
||||
|
||||
// The write both runtimes make: the targets as they are, the holder with its list reversed.
|
||||
foreach (var target in cache.GetAll<RefListTarget>().ToArray()) await cache.UpsertAsync(target, cache.TryGetHandle(target));
|
||||
holder.Items = holder.Items.AsEnumerable().Reverse().ToArray();
|
||||
await cache.UpsertAsync(holder, cache.TryGetHandle(holder));
|
||||
await cache.FlushAsync();
|
||||
}
|
||||
|
||||
store.PushAll();
|
||||
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(copy));
|
||||
var records = snapshot.Records.OrderBy(record => record.Key, StringComparer.Ordinal).Select(record => record.Key + "=" + snapshot.SchemaCatalog.Where(entry => entry.SchemaId == record.SchemaId).Select(entry => entry.SchemaName).SingleOrDefault()).ToArray();
|
||||
TestContext.Out.WriteLine("REFLIST-SUMMARY " + System.Text.Json.JsonSerializer.Serialize(new { records, items, warnings, header = snapshot.FormatVersion }));
|
||||
var records = snapshot.Records.OrderBy(record => record.Key, StringComparer.Ordinal).Select(record => record.Key + "=" + record.SchemaId).ToArray();
|
||||
var listId = snapshot.Records.Single(record => record.Key == "list").SchemaId;
|
||||
var slot = snapshot.SchemaCatalog.Single(entry => entry.SchemaId == listId).Members.Single(member => member.MemberName == "Items");
|
||||
using var reopened = new CultCache();
|
||||
reopened.AddBackingStore(new SingleFileMessagePackBackingStore(copy));
|
||||
await reopened.PullAllBackingStoresAsync();
|
||||
var items = reopened.GetAll<RefListHolder>().Single().Items.Select(item => item.Key.Value).ToArray();
|
||||
Assert.That(items, Is.EqualTo(new[] { "beta", "alpha" }));
|
||||
Assert.That(reopened.Get<RefListTarget>(reopened.GetAll<RefListHolder>().Single().Items[0].Key)!.Name, Is.EqualTo("beta"));
|
||||
TestContext.Out.WriteLine("REFLIST-SUMMARY " + System.Text.Json.JsonSerializer.Serialize(new { records, items, itemsSlot = new { slot.IsReference, slot.TargetSchemaName }, warnings, header = snapshot.FormatVersion }));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ using System.Threading.Tasks;
|
|||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using NUnit.Framework;
|
||||
using RefListHolder = GameCult.Caching.Tests.RefListDriftTests.RefListHolder;
|
||||
using RefListTarget = GameCult.Caching.Tests.RefListDriftTests.RefListTarget;
|
||||
|
||||
namespace GameCult.Caching.Tests
|
||||
{
|
||||
|
|
@ -16,6 +18,7 @@ namespace GameCult.Caching.Tests
|
|||
// types are 2.0.1's own RefListDriftTests.RefListTarget and RefListHolder.
|
||||
public class OldReaderRefListProbe
|
||||
{
|
||||
// RefListDriftTests.A_1_0_60_Written_Store_Opens_Like_2_0_1 is this test's body at head; the two must stay the same code.
|
||||
[Test]
|
||||
public async Task A_1_0_60_Written_Store_Attaches_As_Compatible_Drift()
|
||||
{
|
||||
|
|
@ -23,24 +26,39 @@ namespace GameCult.Caching.Tests
|
|||
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<RefListDriftTests.RefListHolder>().Single().Items.Select(item => item.Key.Value).ToArray();
|
||||
Assert.That(items, Is.EqualTo(new[] { "alpha", "beta" }));
|
||||
string[] warnings;
|
||||
using (var cache = new CultCache())
|
||||
{
|
||||
var store = new SingleFileMessagePackBackingStore(copy);
|
||||
cache.AddBackingStore(store);
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
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 holder = cache.GetAll<RefListHolder>().Single();
|
||||
Assert.That(holder.Items.Select(item => item.Key.Value), Is.EqualTo(new[] { "alpha", "beta" }));
|
||||
|
||||
// The write both runtimes make: the targets as they are, the holder with its list reversed.
|
||||
foreach (var target in cache.GetAll<RefListTarget>().ToArray()) await cache.UpsertAsync(target, cache.TryGetHandle(target));
|
||||
holder.Items = holder.Items.AsEnumerable().Reverse().ToArray();
|
||||
await cache.UpsertAsync(holder, cache.TryGetHandle(holder));
|
||||
await cache.FlushAsync();
|
||||
}
|
||||
|
||||
store.PushAll();
|
||||
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(copy));
|
||||
var records = snapshot.Records.OrderBy(record => record.Key, StringComparer.Ordinal).Select(record => record.Key + "=" + snapshot.SchemaCatalog.Where(entry => entry.SchemaId == record.SchemaId).Select(entry => entry.SchemaName).SingleOrDefault()).ToArray();
|
||||
TestContext.Out.WriteLine("REFLIST-SUMMARY " + JsonSerializer.Serialize(new { records, items, warnings, header = snapshot.FormatVersion }));
|
||||
var records = snapshot.Records.OrderBy(record => record.Key, StringComparer.Ordinal).Select(record => record.Key + "=" + record.SchemaId).ToArray();
|
||||
var listId = snapshot.Records.Single(record => record.Key == "list").SchemaId;
|
||||
var slot = snapshot.SchemaCatalog.Single(entry => entry.SchemaId == listId).Members.Single(member => member.MemberName == "Items");
|
||||
using var reopened = new CultCache();
|
||||
reopened.AddBackingStore(new SingleFileMessagePackBackingStore(copy));
|
||||
await reopened.PullAllBackingStoresAsync();
|
||||
var items = reopened.GetAll<RefListHolder>().Single().Items.Select(item => item.Key.Value).ToArray();
|
||||
Assert.That(items, Is.EqualTo(new[] { "beta", "alpha" }));
|
||||
Assert.That(reopened.Get<RefListTarget>(reopened.GetAll<RefListHolder>().Single().Items[0].Key)!.Name, Is.EqualTo("beta"));
|
||||
TestContext.Out.WriteLine("REFLIST-SUMMARY " + System.Text.Json.JsonSerializer.Serialize(new { records, items, itemsSlot = new { slot.IsReference, slot.TargetSchemaName }, warnings, header = snapshot.FormatVersion }));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue