using System; using System.IO; using System.Runtime.CompilerServices; using Xunit; using XWorld.Server.Host; namespace XWorld.Server.Host.Tests { // 同 (gameId,version) 磁盘内容变化(覆盖重发)→ Registry 自动失效重载; // 旧房间打完旧代码(退役条目引用归零即卸载),新房间用新代码。 public class RegistryContentInvalidationTests : IDisposable { private string _root; private string CopyRps1ToTempRoot() { string root = Path.Combine(Path.GetTempPath(), "games_" + Guid.NewGuid().ToString("N")); string dst = Path.Combine(root, "rps", "1"); Directory.CreateDirectory(dst); string src = Path.Combine(TestGames.Root, "rps", "1"); foreach (var f in Directory.GetFiles(src)) File.Copy(f, Path.Combine(dst, Path.GetFileName(f))); _root = root; return root; } public void Dispose() { if (_root != null) { try { Directory.Delete(_root, true); } catch { } } } // 改变内容指纹(game.json 在指纹范围内;追加空白不影响 JSON 解析) private static void TouchContent(string root) { string gameJson = Path.Combine(root, "rps", "1", "game.json"); File.AppendAllText(gameJson, " "); } [Fact] public void SameContent_ReturnsCachedInstance() { string root = CopyRps1ToTempRoot(); var reg = new GameModuleRegistry(new GameModuleLoader(), root); var a = reg.Acquire("rps", 1); var b = reg.Acquire("rps", 1); Assert.Same(a, b); Assert.Equal(2, reg.RefCount("rps", 1)); } [Fact] public void ContentChange_AfterRefZero_ReloadsNewInstance() { string root = CopyRps1ToTempRoot(); var reg = new GameModuleRegistry(new GameModuleLoader(), root); var a = reg.Acquire("rps", 1); reg.Release(a); // 引用归零,未淘汰 → 保留缓存 TouchContent(root); var b = reg.Acquire("rps", 1); Assert.NotSame(a, b); // 内容变了 → 新模块实例 Assert.Equal(1, reg.RefCount("rps", 1)); Assert.True(reg.IsLoaded("rps", 1)); } [Fact] public void ContentChange_WhileOldActive_OldAndNewCoexist_ReleaseBothCorrect() { string root = CopyRps1ToTempRoot(); var reg = new GameModuleRegistry(new GameModuleLoader(), root); var oldM = reg.Acquire("rps", 1); // 旧房间在打 TouchContent(root); var newM = reg.Acquire("rps", 1); // 新房间 → 新内容 Assert.NotSame(oldM, newM); Assert.Equal(1, reg.RefCount("rps", 1)); // RefCount 只统计当前条目 reg.Release(oldM); // 旧房间结束:退役条目按实例识别释放,不抛 reg.Release(newM); Assert.Equal(0, reg.RefCount("rps", 1)); Assert.True(reg.IsLoaded("rps", 1)); // 当前条目未被淘汰 → 保留复用 } [Fact] public void ContentChange_RetiredModule_UnloadsAndReclaims() { string root = CopyRps1ToTempRoot(); var weak = AcquireChangeReleaseOld(root); for (int i = 0; i < 12 && weak.IsAlive; i++) { GC.Collect(); GC.WaitForPendingFinalizers(); } Assert.False(weak.IsAlive, "退役模块未被回收 —— 存在泄漏引用"); } // 独立方法确保局部强引用随返回消失(同 AlcUnloadTests 套路) [MethodImpl(MethodImplOptions.NoInlining)] private static WeakReference AcquireChangeReleaseOld(string root) { var reg = new GameModuleRegistry(new GameModuleLoader(), root); var oldM = reg.Acquire("rps", 1); TouchContent(root); var newM = reg.Acquire("rps", 1); // oldM 退役 var weak = new WeakReference(oldM); reg.Release(oldM); // 退役 + 归零 → 卸载 reg.Release(newM); return weak; } [Fact] public void MissingDir_ServesCachedEntry() { string root = CopyRps1ToTempRoot(); var reg = new GameModuleRegistry(new GameModuleLoader(), root); var a = reg.Acquire("rps", 1); string dir = Path.Combine(root, "rps", "1"); string away = dir + ".away"; Directory.Move(dir, away); // 模拟覆盖交换的瞬间窗口 try { var b = reg.Acquire("rps", 1); // 指纹算不出 → 用缓存兜底 Assert.Same(a, b); } finally { Directory.Move(away, dir); } } } }