Skip to content

Commit 3e025fc

Browse files
committed
start implementing engine IO tests
1 parent da20144 commit 3e025fc

23 files changed

Lines changed: 1833 additions & 0 deletions

ModVerify.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<Folder Name="/PetroglyphTools/">
3939
<Project Path="src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem.Test/PG.StarWarsGame.Engine.FileSystem.Test.csproj" />
4040
<Project Path="src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem/PG.StarWarsGame.Engine.FileSystem.csproj" />
41+
<Project Path="src/PetroglyphTools/PG.StarWarsGame.Engine.Test/PG.StarWarsGame.Engine.Test.csproj" />
4142
<Project Path="src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/PG.StarWarsGame.Engine.Testing.csproj" />
4243
<Project Path="src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj" />
4344
<Project Path="src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj" />
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
using System;
2+
using System.IO;
3+
using System.IO.Abstractions;
4+
using System.Text;
5+
using AnakinRaW.CommonUtilities.Hashing;
6+
using AnakinRaW.CommonUtilities.Testing;
7+
using AnakinRaW.CommonUtilities.Testing.Extensions;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using PG.Commons;
10+
using PG.StarWarsGame.Engine.ErrorReporting;
11+
using PG.StarWarsGame.Engine.IO;
12+
using PG.StarWarsGame.Engine.Testing;
13+
using PG.StarWarsGame.Files.ALO;
14+
using PG.StarWarsGame.Files.MEG;
15+
using PG.StarWarsGame.Files.MTD;
16+
using PG.StarWarsGame.Files.XML;
17+
using Testably.Abstractions;
18+
using Xunit;
19+
20+
namespace PG.StarWarsGame.Engine.Test;
21+
22+
/// <summary>
23+
/// Base class for engine-bound repository tests.
24+
/// One concrete subclass per <see cref="GameEngineType"/> declares the engine via <see cref="Engine"/>;
25+
/// tests defined on the abstract per-category base classes (e.g. <c>GameRepositoryFileLookupTests</c>) are
26+
/// discovered through inheritance and run once per engine-specific subclass.
27+
/// </summary>
28+
public abstract class EngineRepositoryTestBase : TestBaseWithFileSystem
29+
{
30+
/// <summary>The engine targeted by this test class. Each concrete leaf class declares its engine here.</summary>
31+
protected abstract GameEngineType Engine { get; }
32+
33+
protected override void SetupServices(IServiceCollection serviceCollection)
34+
{
35+
base.SetupServices(serviceCollection);
36+
37+
serviceCollection.AddSingleton<IHashingService>(sp => new HashingService(sp));
38+
39+
serviceCollection.SupportMTD();
40+
serviceCollection.SupportMEG();
41+
serviceCollection.SupportALO();
42+
serviceCollection.SupportXML();
43+
PetroglyphCommons.ContributeServices(serviceCollection);
44+
PetroglyphEngineServiceContribution.ContributeServices(serviceCollection);
45+
}
46+
47+
protected override IFileSystem CreateFileSystem()
48+
{
49+
// Real file system is required to test integration
50+
// with PG.StarWarsGame.Engine.FileSystem
51+
return new RealFileSystem();
52+
}
53+
54+
/// <summary>Creates a builder bound to the test base's service provider.</summary>
55+
protected VirtualGameRepoBuilder CreateBuilder()
56+
{
57+
return new VirtualGameRepoBuilder(ServiceProvider);
58+
}
59+
60+
/// <summary>Constructs an <see cref="IGameRepository"/> for this test class's <see cref="Engine"/>.</summary>
61+
/// <remarks>The returned repository is sealed against further MEG modifications, matching the engine-init lifecycle.</remarks>
62+
protected IGameRepository CreateRepository(VirtualGameRepo repo)
63+
=> CreateRepository(Engine, repo, errorReporter: null);
64+
65+
/// <summary>Constructs an <see cref="IGameRepository"/> for this test class's <see cref="Engine"/> with a custom error reporter
66+
/// to observe init-time assertions (e.g. <see cref="EngineAssertKind.FileNotFound"/> for missing patches).</summary>
67+
protected IGameRepository CreateRepository(VirtualGameRepo repo, IGameEngineErrorReporter? errorReporter)
68+
=> CreateRepository(Engine, repo, errorReporter);
69+
70+
/// <summary>Constructs an <see cref="IGameRepository"/> for an explicit engine. Use only when a test must
71+
/// exercise a non-current engine (e.g. asserting factory dispatch for another engine).</summary>
72+
protected IGameRepository CreateRepository(GameEngineType engine, VirtualGameRepo repo)
73+
=> CreateRepository(engine, repo, errorReporter: null);
74+
75+
private IGameRepository CreateRepository(GameEngineType engine, VirtualGameRepo repo, IGameEngineErrorReporter? errorReporter)
76+
{
77+
if (repo == null)
78+
throw new ArgumentNullException(nameof(repo));
79+
80+
var factory = new GameRepositoryFactory(ServiceProvider);
81+
var wrapper = new GameEngineErrorReporterWrapper(errorReporter);
82+
var gameRepo = factory.Create(engine, repo.GameLocations, wrapper);
83+
gameRepo.Seal();
84+
return gameRepo;
85+
}
86+
87+
/// <summary>Reads the stream to end as UTF-8 text and disposes it. Convenient for inspecting <c>OpenFile</c> results.</summary>
88+
protected static string ReadAll(Stream stream)
89+
{
90+
if (stream == null)
91+
throw new ArgumentNullException(nameof(stream));
92+
93+
using (stream)
94+
using (var reader = new StreamReader(stream, Encoding.UTF8))
95+
return reader.ReadToEnd();
96+
}
97+
98+
/// <summary>
99+
/// Describes the repository facet under test and the fixtures used by the inherited
100+
/// <see cref="Lookup_IsCaseAndSeparatorInsensitive_AcrossFilesystemAndMeg"/> test.
101+
/// The default targets the base <see cref="IGameRepository"/>; derived classes override
102+
/// to target their specialized facet (Effects, Texture, Model).
103+
/// </summary>
104+
protected virtual RepositoryLookupSetup GetLookupSetup() => new(
105+
PopulateGame: g =>
106+
{
107+
g.Write("Data/XML/Foo.xml", "fs-content");
108+
g.WriteMeg("Data/Patch.meg", meg => meg.Add("Data/Audio/Bar.wav", "meg-content"));
109+
},
110+
SelectRepository: gameRepo => gameRepo,
111+
FilesystemLookup: "Data/XML/Foo.xml",
112+
FilesystemContent: "fs-content",
113+
MegLookup: "Data/Audio/Bar.wav",
114+
MegContent: "meg-content");
115+
116+
/// <summary>
117+
/// Sanity check that lookups through the engine layer remain case- and separator-insensitive for both
118+
/// filesystem-backed and MEG-backed files, against whichever <see cref="IRepository"/> facet the
119+
/// derived class exercises.
120+
/// </summary>
121+
/// <remarks>
122+
/// Defined on the base class so xUnit discovers it in every concrete repository-test class. Case shuffling
123+
/// uses <see cref="StringExtensions.ShuffleCasing(string)"/>; separator shuffling uses a seeded local
124+
/// <see cref="Random"/> so a separator-related failure stays reproducible.
125+
/// </remarks>
126+
[Fact]
127+
public void Lookup_IsCaseAndSeparatorInsensitive_AcrossFilesystemAndMeg()
128+
{
129+
var setup = GetLookupSetup();
130+
131+
using var virt = CreateBuilder()
132+
.WithGame(setup.PopulateGame)
133+
.Build();
134+
var gameRepo = CreateRepository(virt);
135+
var repoUnderTest = setup.SelectRepository(gameRepo);
136+
137+
var separatorRandom = new Random(42);
138+
for (var i = 0; i < 32; i++)
139+
{
140+
var fsVariant = JitterSeparators(string.ShuffleCasing(setup.FilesystemLookup), separatorRandom);
141+
var megVariant = JitterSeparators(string.ShuffleCasing(setup.MegLookup), separatorRandom);
142+
143+
Assert.True(repoUnderTest.FileExists(fsVariant), $"Filesystem variant '{fsVariant}' should resolve.");
144+
Assert.True(repoUnderTest.FileExists(megVariant), $"MEG variant '{megVariant}' should resolve.");
145+
Assert.Equal(setup.FilesystemContent, ReadAll(repoUnderTest.OpenFile(fsVariant)));
146+
Assert.Equal(setup.MegContent, ReadAll(repoUnderTest.OpenFile(megVariant)));
147+
}
148+
}
149+
150+
private static string JitterSeparators(string path, Random random)
151+
{
152+
var chars = path.ToCharArray();
153+
for (var i = 0; i < chars.Length; i++)
154+
{
155+
if (chars[i] == '/' || chars[i] == '\\')
156+
chars[i] = random.Next(2) == 0 ? '/' : '\\';
157+
}
158+
return new string(chars);
159+
}
160+
}

0 commit comments

Comments
 (0)