GameCult.Thing.Contract.Tests: a store with patched type name, id and hash is refused on reopen
Replaces the false comment that reopening never compares type names with the end-to-end pin: id, content hash and a member type name moved together are refused; the same patch without the type name opens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4f87e5c5bf
commit
a9f8b2c242
1 changed files with 63 additions and 4 deletions
|
|
@ -59,10 +59,6 @@ public sealed class NameSetTests
|
||||||
}
|
}
|
||||||
|
|
||||||
// A persisted entry equal in id and content hash resolves without comparison, so the entries below move both.
|
// A persisted entry equal in id and content hash resolves without comparison, so the entries below move both.
|
||||||
// CultCache compares a persisted catalog with the package's names where it resolves a persisted schema id that is not
|
|
||||||
// its own (ResolvePersistedSchema). Reopening a single-file store does not call it: a store whose recorded names were
|
|
||||||
// patched (id and Int64 both) opens and reads at b3bd8dd5, so the comparison is exercised on the entry the store
|
|
||||||
// would record.
|
|
||||||
[Test]
|
[Test]
|
||||||
public void APersistedCatalogWhoseTypeNameDiffersIsRefusedByTheComparison()
|
public void APersistedCatalogWhoseTypeNameDiffersIsRefusedByTheComparison()
|
||||||
{
|
{
|
||||||
|
|
@ -79,4 +75,67 @@ public sealed class NameSetTests
|
||||||
Assert.Throws<InvalidOperationException>(() => Wire.Registry.ResolvePersistedSchema(listed.SchemaId, new[] { listed }),
|
Assert.Throws<InvalidOperationException>(() => Wire.Registry.ResolvePersistedSchema(listed.SchemaId, new[] { listed }),
|
||||||
"a store written when Assets was a List is refused");
|
"a store written when Assets was a List is refused");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int Patch(byte[] bytes, string from, string to)
|
||||||
|
{
|
||||||
|
var f = System.Text.Encoding.UTF8.GetBytes(from);
|
||||||
|
var t = System.Text.Encoding.UTF8.GetBytes(to);
|
||||||
|
Assert.That(t, Has.Length.EqualTo(f.Length));
|
||||||
|
var count = 0;
|
||||||
|
for (var i = 0; i + f.Length <= bytes.Length; i++)
|
||||||
|
{
|
||||||
|
if (!bytes.AsSpan(i, f.Length).SequenceEqual(f)) continue;
|
||||||
|
t.CopyTo(bytes, i);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Moved(string hex) => hex[..^1] + (hex[^1] == '0' ? '1' : '0');
|
||||||
|
|
||||||
|
// CultCache reopens a store by schema id; an id it does not own is resolved by name and compared member by member.
|
||||||
|
// A store whose recorded id and content hash name another schema and whose type name was changed is refused, while
|
||||||
|
// the same patch without the type name opens: the refusal is the type-name comparison, end to end.
|
||||||
|
[Test]
|
||||||
|
public void AStoreWhoseRecordedTypeNameDiffersIsRefusedOnReopen()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), $"thing-names-{Guid.NewGuid():N}.cc");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = Wire.Registry }))
|
||||||
|
{
|
||||||
|
cache.Commit(b => b.Upsert(Builders.FullAssetCatalog()));
|
||||||
|
cache.FlushAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry = Wire.Registry.GetRequired(typeof(AssetCatalog)).ToCatalogEntry();
|
||||||
|
var written = File.ReadAllBytes(path);
|
||||||
|
|
||||||
|
byte[] Patched(bool typeName)
|
||||||
|
{
|
||||||
|
var bytes = (byte[])written.Clone();
|
||||||
|
Assert.That(Patch(bytes, entry.SchemaId, Moved(entry.SchemaId)), Is.GreaterThan(0), "the schema id was recorded");
|
||||||
|
Assert.That(Patch(bytes, entry.ContentHash, Moved(entry.ContentHash)), Is.GreaterThan(0), "the content hash was recorded");
|
||||||
|
if (typeName) Assert.That(Patch(bytes, "System.Int64", "System.Int32"), Is.GreaterThan(0), "a member type name was recorded");
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Reopen(byte[] bytes)
|
||||||
|
{
|
||||||
|
File.WriteAllBytes(path, bytes);
|
||||||
|
using var cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = Wire.Registry, ReadOnly = true });
|
||||||
|
return cache.GetAll<AssetCatalog>().Count();
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.That(Reopen(written), Is.EqualTo(1), "the unpatched store reopens");
|
||||||
|
Assert.That(Reopen(Patched(typeName: false)), Is.EqualTo(1), "a moved id and hash with the same names reopens");
|
||||||
|
var error = Assert.Catch(() => Reopen(Patched(typeName: true)))!;
|
||||||
|
Assert.That(Wire.Flatten(error), Does.Contain("changed type").And.Contain("System.Int32").And.Contain("System.Int64"));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue