diff --git a/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceTypeTableTests.cs b/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceTypeTableTests.cs index 0622411f7..a4aceba36 100644 --- a/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceTypeTableTests.cs +++ b/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceTypeTableTests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.IO; using Xunit; namespace Server.Tests; @@ -47,4 +49,104 @@ public class GenericEntityPersistenceTypeTableTests persistence.Unregister(); } } + + [Fact] + public void UnresolvedTableEntrySkipsItsRecordsAfterConfirmation() + { + var previousAssemblies = AssemblyHandler.Assemblies; + AssemblyHandler.Assemblies = [.. previousAssemblies ?? [], typeof(RoundTripEntity).Assembly]; + + var dir = Path.Combine(Path.GetTempPath(), $"muo-typetable-{Guid.NewGuid():N}"); + Directory.CreateDirectory(Path.Combine(dir, "TypeTable")); + + TypeTablePersistence loaded = null; + var previousIn = Console.In; + + try + { + // Hand-write a v4 idx: two table entries (one bogus), one record per type. + using (var idx = new FileBufferWriter(Path.Combine(dir, "TypeTable", "TypeTable.idx"))) + { + idx.Write(4); // version + idx.Write(2); // type table count + idx.WriteRaw(typeof(RoundTripEntity).FullName); // index 0: resolvable + idx.WriteRaw("Server.Tests.DoesNotExistAnymore"); // index 1: bogus + idx.Write(2); // record count + idx.Write((ushort)0); // record 1: real type + idx.Write(1u); // serial + idx.Write(DateTime.UtcNow.Ticks); + idx.Write(0L); // position + idx.Write(4); // length + idx.Write((ushort)1); // record 2: bogus type + idx.Write(2u); + idx.Write(DateTime.UtcNow.Ticks); + idx.Write(4L); + idx.Write(4); + } + + // GetConstructorFor prompts on the console; answer "y" (delete those types). + Console.SetIn(new StringReader("y\n")); + + loaded = new TypeTablePersistence(); + loaded.DeserializeIndexes(dir, null); + + Assert.Single(loaded.EntitiesBySerial); + Assert.True(loaded.EntitiesBySerial.ContainsKey((Serial)1u)); + Assert.True(loaded.TryGetTypeIndex(typeof(RoundTripEntity), out _)); + } + finally + { + Console.SetIn(previousIn); + loaded?.Unregister(); + AssemblyHandler.Assemblies = previousAssemblies; + Directory.Delete(dir, true); + } + } + + [Fact] + public void LegacyV3IndexesStillLoadAndHydrateTheTypeTable() + { + var previousAssemblies = AssemblyHandler.Assemblies; + AssemblyHandler.Assemblies = [.. previousAssemblies ?? [], typeof(RoundTripEntity).Assembly]; + + var dir = Path.Combine(Path.GetTempPath(), $"muo-legacyidx-{Guid.NewGuid():N}"); + Directory.CreateDirectory(Path.Combine(dir, "TypeTable")); + + TypeTablePersistence loaded = null; + + try + { + var hash = AssemblyHandler.GetTypeHash(typeof(RoundTripEntity)); + + // Hand-write a v3 idx: records carry flag + 8-byte hash, resolved via typesDb. + using (var idx = new FileBufferWriter(Path.Combine(dir, "TypeTable", "TypeTable.idx"))) + { + idx.Write(3); // version + idx.Write(1); // record count + idx.Write((byte)2); // xxHash3 flag + idx.Write(hash); + idx.Write(1u); // serial + idx.Write(DateTime.UtcNow.Ticks); + idx.Write(0L); // position + idx.Write(4); // length + } + + var typesDb = new Dictionary { [hash] = typeof(RoundTripEntity).FullName }; + + loaded = new TypeTablePersistence(); + loaded.DeserializeIndexes(dir, typesDb); + + Assert.Single(loaded.EntitiesBySerial); + Assert.True(loaded.EntitiesBySerial.ContainsKey((Serial)1u)); + + // Legacy loads must hydrate the table so the NEXT save can write v4 indexes. + Assert.True(loaded.TryGetTypeIndex(typeof(RoundTripEntity), out _)); + } + finally + { + loaded?.Unregister(); + AssemblyHandler.Assemblies = previousAssemblies; + Directory.Delete(dir, true); + } + } }