using Xunit; namespace MrGameEng.Mods.Tests; public sealed class ModLoaderTests : IDisposable { private readonly string _root = Directory.CreateTempSubdirectory("mrge-mods-tests-").FullName; private string ModsRoot => Path.Combine(_root, "Mods"); public void Dispose() => Directory.Delete(_root, recursive: true); /// Создаёт мод с About.json; зависимости — id модов, которые должны грузиться раньше. private void WriteMod(string id, params string[] dependencies) { var aboutDir = Path.Combine(ModsRoot, id, "About"); Directory.CreateDirectory(aboutDir); var deps = string.Join(", ", dependencies.Select(d => $"\"{d}\"")); File.WriteAllText( Path.Combine(aboutDir, "About.json"), $$"""{ "id": "{{id}}", "name": "{{id}} mod", "version": "1.0", "dependencies": [{{deps}}] }""" ); } [Fact] public void Load_OrdersDependenciesFirst_ThenAlphabetically() { WriteMod("zebra", "Core"); WriteMod("apple", "Core"); WriteMod("Core"); var mods = ModLoader.Load(ModsRoot); Assert.Equal(["Core", "apple", "zebra"], mods.Select(m => m.Id)); } [Fact] public void Load_MissingDependency_Throws() { WriteMod("orphan", "NoSuchMod"); var exception = Assert.Throws(() => ModLoader.Load(ModsRoot)); Assert.Contains("NoSuchMod", exception.Message); } [Fact] public void Load_CyclicDependency_Throws() { WriteMod("a", "b"); WriteMod("b", "a"); var exception = Assert.Throws(() => ModLoader.Load(ModsRoot)); Assert.Contains("Cyclic", exception.Message); } [Fact] public void Load_ActiveSubset_LoadsOnlyRequested() { WriteMod("Core"); WriteMod("extra", "Core"); WriteMod("unused"); var mods = ModLoader.Load(ModsRoot, ["extra", "Core"]); Assert.Equal(["Core", "extra"], mods.Select(m => m.Id)); } [Fact] public void Load_IgnoresDirectoriesWithoutAbout() { WriteMod("Core"); Directory.CreateDirectory(Path.Combine(ModsRoot, "not-a-mod")); var mod = Assert.Single(ModLoader.Load(ModsRoot)); Assert.Equal("Core", mod.Id); Assert.Equal("Core mod", mod.Info.Name); } [Fact] public void FindModsRoot_WalksUpFromNestedDirectory() { WriteMod("Core"); var nested = Path.Combine(_root, "bin", "Debug", "net8.0"); Directory.CreateDirectory(nested); Assert.Equal(ModsRoot, ModLoader.FindModsRoot(nested)); } [Fact] public void ContentTree_LaterMod_OverridesSameRelativePath() { WriteMod("Core"); WriteMod("patch", "Core"); File.WriteAllText(CreateContentFile("Core", "Textures", "things/rock.png"), "core"); File.WriteAllText(CreateContentFile("Core", "Textures", "things/tree.png"), "core"); File.WriteAllText(CreateContentFile("patch", "Textures", "things/rock.png"), "patched"); var mods = ModLoader.Load(ModsRoot); var tree = ModContentTree.Build(mods, "Textures", ".png"); Assert.Equal(2, tree.Files.Count); Assert.True(tree.TryGet("things/rock.png", out var rock)); Assert.Equal("patch", rock.Mod.Id); Assert.Equal("patched", File.ReadAllText(rock.FullPath)); Assert.True(tree.TryGet("things/tree.png", out var oak)); Assert.Equal("Core", oak.Mod.Id); } private string CreateContentFile(string modId, string folder, string relativePath) { var fullPath = Path.Combine(ModsRoot, modId, folder, relativePath); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); return fullPath; } }