Caching tests: a 1.0.x store with a ref-list slot opens at 2.0 (red); reference gain is the only drift excused
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0888ce6a71
commit
3e65477f22
3 changed files with 134 additions and 0 deletions
124
tests/GameCult.Caching.Tests/RefListDriftTests.cs
Normal file
124
tests/GameCult.Caching.Tests/RefListDriftTests.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using MessagePack;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GameCult.Caching.Tests
|
||||
{
|
||||
// 1.0.x persisted a list of record references (CultRecordRef<T>[]) as a plain value slot: isReference false, no target.
|
||||
// 2.0 classifies the same slot as a reference to T. The payload is the same array of key strings, so a store written by
|
||||
// 1.0.x opens: gaining reference classification on an otherwise identical slot is compatible drift. A reference that is
|
||||
// lost, retargeted, or that arrives with a different type or cardinality stays a refusal.
|
||||
public class RefListDriftTests
|
||||
{
|
||||
[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>>();
|
||||
}
|
||||
|
||||
private static string VectorPath(string name)
|
||||
{
|
||||
for (var dir = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); dir != null; dir = dir.Parent)
|
||||
{
|
||||
var vectors = Path.Combine(dir.FullName, "tests", "vectors", "ref-list-drift");
|
||||
if (Directory.Exists(vectors)) return Path.Combine(vectors, name);
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException($"tests/vectors/ref-list-drift not found above {TestContext.CurrentContext.TestDirectory}.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task A_1_0_Store_With_A_Ref_List_Slot_Opens_And_Rewrites_Under_The_Reference_Schema()
|
||||
{
|
||||
var copy = Path.Combine(Path.GetTempPath(), $"reflist-drift-{Guid.NewGuid():N}.cc");
|
||||
File.Copy(VectorPath("old-store.cc"), copy);
|
||||
try
|
||||
{
|
||||
using var cache = new CultCache();
|
||||
cache.AddBackingStore(new SingleFileMessagePackBackingStore(copy));
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
|
||||
var holder = cache.GetAll<RefListHolder>().Single();
|
||||
Assert.That(holder.Items.Select(item => item.Key.Value), Is.EqualTo(new[] { "alpha", "beta" }));
|
||||
Assert.That(cache.Get(holder.Items[0])!.Name, Is.EqualTo("alpha"));
|
||||
|
||||
// The store is rewritten under the 2.0 schema, which now carries the reference.
|
||||
holder.Items = holder.Items.Reverse().ToArray();
|
||||
await cache.AddAsync(holder);
|
||||
using var reopened = new CultCache();
|
||||
reopened.AddBackingStore(new SingleFileMessagePackBackingStore(copy));
|
||||
await reopened.PullAllBackingStoresAsync();
|
||||
Assert.That(reopened.GetAll<RefListHolder>().Single().Items.Select(item => item.Key.Value), Is.EqualTo(new[] { "beta", "alpha" }));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(copy);
|
||||
}
|
||||
}
|
||||
|
||||
private static CultSchemaMigrationReport Resolve(Action<CultSchemaMemberCatalogEntry> persistedItems)
|
||||
{
|
||||
var catalog = CultDocumentRegistry.Shared.GetRequired<RefListHolder>().ToCatalogEntry();
|
||||
catalog.SchemaId = "persisted.tests.reflist_holder.v1";
|
||||
catalog.CompatibleSchemaIds = new[] { CultDocumentRegistry.Shared.GetRequired<RefListHolder>().SchemaId };
|
||||
persistedItems(catalog.Members.Single(member => member.Slot == 1));
|
||||
return CultDocumentRegistry.Shared.ResolvePersistedSchemaReport(catalog.SchemaId, new[] { catalog });
|
||||
}
|
||||
|
||||
private static void AsWrittenBy10(CultSchemaMemberCatalogEntry member)
|
||||
{
|
||||
member.IsReference = false;
|
||||
member.TargetSchemaName = null;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void A_Reference_Gained_On_An_Otherwise_Identical_Slot_Is_Compatible_Drift()
|
||||
{
|
||||
var report = Resolve(AsWrittenBy10);
|
||||
Assert.That(report.Kind, Is.EqualTo(CultSchemaMigrationKind.CompatibleDrift));
|
||||
Assert.That(report.Warnings.Select(warning => warning.Code), Does.Contain("gained_reference_semantics"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void A_Persisted_Reference_Retargeted_Or_Gaining_Over_A_Foreign_Target_Still_Refuses()
|
||||
{
|
||||
Assert.That(
|
||||
() => Resolve(member => member.TargetSchemaName = "tests.other_target"),
|
||||
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("changed target schema"));
|
||||
Assert.That(
|
||||
() => Resolve(member => { member.IsReference = false; member.TargetSchemaName = "tests.other_target"; }),
|
||||
Throws.TypeOf<InvalidOperationException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void A_Gained_Reference_Does_Not_Excuse_A_Changed_Type_Or_Cardinality()
|
||||
{
|
||||
Assert.That(
|
||||
() => Resolve(member => { AsWrittenBy10(member); member.TypeName = "System.String[]"; }),
|
||||
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("changed type"));
|
||||
Assert.That(
|
||||
() => Resolve(member => { AsWrittenBy10(member); member.IsMany = !member.IsMany; }),
|
||||
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("changed cardinality"));
|
||||
}
|
||||
}
|
||||
}
|
||||
10
tests/vectors/ref-list-drift/README.md
Normal file
10
tests/vectors/ref-list-drift/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# ref-list-drift vector
|
||||
|
||||
`old-store.cc` is a `cultcache.store.v1` file in the shape CultLib 1.0.x wrote for a list of record references: the
|
||||
`Items` slot (`CultRecordRef<RefListTarget>[]`) is persisted with `isReference:false` and no target schema, and its
|
||||
payload is an array of key strings. Its layout is copied from a real 1.0.x-written store (Aetheria `run.cc`, schema
|
||||
`aetheria.savedgame`, slots `Zones` and `Factions`), pruned to two schemas and three records: `alpha` and `beta`
|
||||
(`tests.reflist_target`) and `list` (`tests.reflist_holder`, `Items = [alpha, beta]`).
|
||||
|
||||
Hand-written by a script outside the runtime under test; never regenerated by CultCache. A change to this file is a
|
||||
reviewed commit.
|
||||
BIN
tests/vectors/ref-list-drift/old-store.cc
Normal file
BIN
tests/vectors/ref-list-drift/old-store.cc
Normal file
Binary file not shown.
Loading…
Reference in a new issue