Initial commit: Client Doc docs Server Tools

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ud18010
2026-07-10 10:24:29 +08:00
co-authored by Cursor
commit 7e35d8da31
3374 changed files with 680813 additions and 0 deletions
@@ -0,0 +1,128 @@
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); }
}
}
}