Packages in the tree: LFS rules for GameData/Mods, and the test fixture boots the catalog the game boots
The fixture resolves GameData/Mods through ShipModCatalog.ResolveCatalog once per test process and throws on any excluded package, so a broken first-party package fails the suite. With no GameData/Mods it is the shipped catalog, unchanged. Builds and tests not yet run on this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
594d347704
commit
cc83f5adda
4 changed files with 102 additions and 3 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
|
@ -54,6 +54,8 @@
|
|||
*.sbsar filter=lfs diff=lfs merge=lfs -text
|
||||
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
||||
GameData/*.cc filter=lfs diff=lfs merge=lfs -text
|
||||
GameData/Mods/**/*.cc filter=lfs diff=lfs merge=lfs -text
|
||||
GameData/Mods/**/*.glb filter=lfs diff=lfs merge=lfs -text
|
||||
*.a filter=lfs diff=lfs merge=lfs -text
|
||||
*AkSoundEngine filter=lfs diff=lfs merge=lfs -text
|
||||
*AkWaapiClient filter=lfs diff=lfs merge=lfs -text
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ using float3 = CultMath.float3;
|
|||
|
||||
// RunStart owns what a new run starts with, and Zone.Admit is the one admission into a zone (docs/scenarios-cut.md,
|
||||
// 2.2 and R). Every test stages into a real arena: the entrance zone of a prelude galaxy at a fixed seed, built from a
|
||||
// scratch copy of the shipped catalog and the authored settings (Assets/Resources/Settings.asset).
|
||||
// scratch copy of the catalog the game boots (TestCatalog) and the authored settings (Assets/Resources/Settings.asset).
|
||||
public sealed partial class RunStartTests : IDisposable
|
||||
{
|
||||
private const uint GalaxySeed = 1;
|
||||
|
|
@ -38,7 +38,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
var repo = FindRepoRoot();
|
||||
Directory.CreateDirectory(_root);
|
||||
var catalog = Path.Combine(_root, "Aetheria.cc");
|
||||
File.Copy(Path.Combine(repo, "GameData", "Aetheria.cc"), catalog);
|
||||
File.Copy(TestCatalog.Repo, catalog);
|
||||
|
||||
// A registry scoped to the shipped assembly's own [CultDocument] types, as RestoredHullsTests composes it, so
|
||||
// this test assembly's own documents never reach the real catalog's validation.
|
||||
|
|
@ -72,7 +72,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
.Where(t => t is { IsAbstract: false, IsInterface: false })
|
||||
.Where(t => t.GetCustomAttribute<CultDocumentAttribute>() != null));
|
||||
|
||||
private static string FindRepoRoot()
|
||||
internal static string FindRepoRoot()
|
||||
{
|
||||
for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir != null; dir = dir.Parent)
|
||||
if (File.Exists(Path.Combine(dir.FullName, "Aetheria.Shared", "Aetheria.Shared.csproj")))
|
||||
|
|
|
|||
33
tests/Aetheria.Shared.Tests/TestCatalog.cs
Normal file
33
tests/Aetheria.Shared.Tests/TestCatalog.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
// The catalog the game boots, for tests: ShipModCatalog.ResolveCatalog over GameData/Mods, the call ActionGameManager
|
||||
// makes. The game skips a broken package so a player still boots; a test never does, so an exclusion throws here and
|
||||
// a broken first-party package fails the suite instead of vanishing from it.
|
||||
internal static class TestCatalog
|
||||
{
|
||||
private static readonly Lazy<string> Shared = new Lazy<string>(() =>
|
||||
{
|
||||
var repo = RunStartTests.FindRepoRoot();
|
||||
var work = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "aetheria-testcatalog-" + Guid.NewGuid().ToString("N"))).FullName;
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => { try { Directory.Delete(work, true); } catch (IOException) { } };
|
||||
return Resolve(Path.Combine(repo, "GameData", "Aetheria.cc"), Path.Combine(repo, "GameData", "Mods"), work);
|
||||
});
|
||||
|
||||
// The repo's catalog with its first-party packages composed in, resolved once per test process.
|
||||
public static string Repo => Shared.Value;
|
||||
|
||||
public static string Resolve(string shippedCatalog, string modsRoot, string workDir)
|
||||
{
|
||||
var (catalog, excluded) = ShipModCatalog.ResolveCatalog(shippedCatalog, Path.Combine(workDir, "derived.cc"), modsRoot);
|
||||
if (excluded.Length > 0)
|
||||
throw new InvalidOperationException("First-party packages were excluded from the catalog: " +
|
||||
string.Join("; ", excluded.Select(entry => entry.Package + ": " + entry.Reason)));
|
||||
return catalog;
|
||||
}
|
||||
}
|
||||
64
tests/Aetheria.Shared.Tests/TestCatalogTests.cs
Normal file
64
tests/Aetheria.Shared.Tests/TestCatalogTests.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using GameCult.Caching;
|
||||
using Xunit;
|
||||
|
||||
// TestCatalog.Resolve: the catalog every RunStartTests partial stages from is the one the game boots.
|
||||
public sealed class TestCatalogTests : IDisposable
|
||||
{
|
||||
private readonly TempDirectory _directory = new TempDirectory();
|
||||
private string Shipped => Path.Combine(_directory.Path, "Aetheria.cc");
|
||||
private string Mods => Path.Combine(_directory.Path, "Mods");
|
||||
private string Work => Path.Combine(_directory.Path, "Work");
|
||||
|
||||
public TestCatalogTests()
|
||||
{
|
||||
Directory.CreateDirectory(Mods);
|
||||
using var cache = AetheriaStores.Open(Shipped, catalogWritable: true);
|
||||
// The test assembly registers TestCatalogGlobal (AetheriaStoresTests), so any populated catalog must hold one.
|
||||
cache.Upsert(new TestCatalogGlobal { Name = "Temperament" });
|
||||
cache.Upsert(new HullData { Name = "Wasp", Shape = ShipAuthoringTests.Fixture().Hull.Shape });
|
||||
cache.FlushAsync().Wait();
|
||||
}
|
||||
|
||||
public void Dispose() => _directory.Dispose();
|
||||
|
||||
[Fact]
|
||||
public void NoModsMeansTheShippedCatalog()
|
||||
{
|
||||
var before = File.ReadAllBytes(Shipped);
|
||||
|
||||
Assert.Equal(Shipped, TestCatalog.Resolve(Shipped, Path.Combine(_directory.Path, "absent"), Work));
|
||||
Assert.Equal(Shipped, TestCatalog.Resolve(Shipped, Mods, Work));
|
||||
|
||||
Assert.Equal(SHA256.HashData(before), SHA256.HashData(File.ReadAllBytes(Shipped)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void APackageReachesTheFixture()
|
||||
{
|
||||
ShipFixture.WritePackage(Mods, "mod.skiff");
|
||||
|
||||
var catalog = TestCatalog.Resolve(Shipped, Mods, Work);
|
||||
|
||||
Assert.NotEqual(Shipped, catalog);
|
||||
using var cache = AetheriaStores.Open(catalog);
|
||||
Assert.Equal("Skiff", cache.Get<HullData>(ShipModCatalog.HullKey("mod.skiff")).Name);
|
||||
Assert.Contains("Wasp", cache.GetAll<HullData>().Select(hull => hull.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ABrokenPackageFailsLoudly()
|
||||
{
|
||||
ShipFixture.WritePackage(Mods, "mod.good", hullName: "Good");
|
||||
ShipFixture.WritePackage(Mods, "mod.broken", hullName: "Broken");
|
||||
File.Delete(Path.Combine(Mods, "mod.broken", "skiff.glb"));
|
||||
|
||||
var error = Assert.Throws<InvalidOperationException>(() => TestCatalog.Resolve(Shipped, Mods, Work));
|
||||
|
||||
Assert.Contains("mod.broken", error.Message);
|
||||
Assert.DoesNotContain("mod.good", error.Message);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue