Skip to content

Commit 4d8b4d5

Browse files
committed
add tests for all fs strategies, default asset loading chain and platform independent file lookup (case and separator insensitive)
1 parent 52abfc3 commit 4d8b4d5

10 files changed

Lines changed: 372 additions & 97 deletions

src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RepositoryLookupSetup.cs renamed to src/PetroglyphTools/PG.StarWarsGame.Engine.Test/CaseInsensitivityFixture.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,15 @@
55
namespace PG.StarWarsGame.Engine.Test;
66

77
/// <summary>
8-
/// Per-repository scaffolding for the inherited lookup-insensitivity test.
9-
/// Each derived test class returns one describing which <see cref="IRepository"/> facet it exercises,
10-
/// how to populate the virtual game with fixtures that facet can resolve, and the lookup keys that
11-
/// should return the seeded filesystem-backed and MEG-backed content.
8+
/// A test fixture for verifying that repositories resolve requests in a case-insensitive manner.
129
/// </summary>
13-
/// <param name="PopulateGame">Callback that writes fixtures into the virtual game's <c>WithGame</c> origin.</param>
10+
/// <param name="PopulateGame">Callback that writes fixtures into the virtual game's <c>ConfigureGame</c> origin.</param>
1411
/// <param name="SelectRepository">Picks the repository facet under test from the constructed <see cref="IGameRepository"/>.</param>
1512
/// <param name="FilesystemLookup">Lookup key the repository should resolve to the filesystem-backed fixture.</param>
1613
/// <param name="FilesystemContent">Expected content at <paramref name="FilesystemLookup"/>.</param>
1714
/// <param name="MegLookup">Lookup key the repository should resolve to the MEG-backed fixture.</param>
1815
/// <param name="MegContent">Expected content at <paramref name="MegLookup"/>.</param>
19-
public sealed record RepositoryLookupSetup(
16+
public sealed record CaseInsensitivityFixture(
2017
Action<IRepoOriginWriter> PopulateGame,
2118
Func<IGameRepository, IRepository> SelectRepository,
2219
string FilesystemLookup,

src/PetroglyphTools/PG.StarWarsGame.Engine.Test/EngineRepositoryTestBase.cs

Lines changed: 152 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,68 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.IO;
34
using System.IO.Abstractions;
45
using System.Text;
5-
using AnakinRaW.CommonUtilities.Hashing;
6-
using AnakinRaW.CommonUtilities.Testing;
76
using AnakinRaW.CommonUtilities.Testing.Extensions;
8-
using Microsoft.Extensions.DependencyInjection;
9-
using PG.Commons;
107
using PG.StarWarsGame.Engine.ErrorReporting;
118
using PG.StarWarsGame.Engine.IO;
9+
using PG.StarWarsGame.Engine.IO.Repositories;
1210
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;
1711
using Testably.Abstractions;
1812
using Xunit;
1913

2014
namespace PG.StarWarsGame.Engine.Test;
2115

2216
/// <summary>
2317
/// 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.
2718
/// </summary>
28-
public abstract class EngineRepositoryTestBase : TestBaseWithFileSystem
19+
public abstract class EngineRepositoryTestBase : EngineTestBase
2920
{
30-
/// <summary>The engine targeted by this test class. Each concrete leaf class declares its engine here.</summary>
21+
/// <summary>Gets the engine targeted by this test class.</summary>
3122
protected abstract GameEngineType Engine { get; }
3223

33-
protected override void SetupServices(IServiceCollection serviceCollection)
34-
{
35-
base.SetupServices(serviceCollection);
36-
37-
serviceCollection.AddSingleton<IHashingService>(sp => new HashingService(sp));
24+
/// <summary>
25+
/// Builds a <see cref="CaseInsensitivityFixture"/> instance that provides test data and logic
26+
/// for verifying case and separator insensitivity in repository lookups.
27+
/// </summary>
28+
/// <remarks>
29+
/// Path casing and separators automatically randomized by the underlying test logic.
30+
/// </remarks>
31+
/// <returns>
32+
/// A <see cref="CaseInsensitivityFixture"/> containing the configuration and data
33+
/// necessary for case insensitivity tests.
34+
/// </returns>
35+
protected abstract CaseInsensitivityFixture BuildCaseInsensitivityFixture();
3836

39-
serviceCollection.SupportMTD();
40-
serviceCollection.SupportMEG();
41-
serviceCollection.SupportALO();
42-
serviceCollection.SupportXML();
43-
PetroglyphCommons.ContributeServices(serviceCollection);
44-
PetroglyphEngineServiceContribution.ContributeServices(serviceCollection);
45-
}
37+
/// <summary>
38+
/// Builds the <see cref="RepositoryPriorityFixture"/> describing the asset this test class resolves
39+
/// through the loading chain, exercised by the cross-origin priority tests.
40+
/// </summary>
41+
protected abstract RepositoryPriorityFixture BuildPriorityFixture();
4642

47-
protected override IFileSystem CreateFileSystem()
43+
/// <summary>
44+
/// The repository origins in descending lookup priority for this test class's <see cref="Engine"/>.
45+
/// </summary>
46+
private IReadOnlyList<RepositoryLayer> ExpectedLoadOrder => Engine switch
47+
{
48+
GameEngineType.Foc =>
49+
[
50+
RepositoryLayer.Mod,
51+
RepositoryLayer.Game,
52+
RepositoryLayer.MasterMeg,
53+
RepositoryLayer.Fallback,
54+
],
55+
GameEngineType.Eaw =>
56+
[
57+
RepositoryLayer.Mod,
58+
RepositoryLayer.Game,
59+
RepositoryLayer.Fallback,
60+
RepositoryLayer.MasterMeg,
61+
],
62+
_ => throw new ArgumentOutOfRangeException()
63+
};
64+
65+
protected sealed override IFileSystem CreateFileSystem()
4866
{
4967
// Real file system is required to test integration
5068
// with PG.StarWarsGame.Engine.FileSystem
@@ -60,19 +78,18 @@ protected VirtualGameRepoBuilder CreateBuilder()
6078
/// <summary>Constructs an <see cref="IGameRepository"/> for this test class's <see cref="Engine"/>.</summary>
6179
/// <remarks>The returned repository is sealed against further MEG modifications, matching the engine-init lifecycle.</remarks>
6280
protected IGameRepository CreateRepository(VirtualGameRepo repo)
63-
=> CreateRepository(Engine, repo, errorReporter: null);
81+
{
82+
return CreateRepository(Engine, repo, errorReporter: null);
83+
}
6484

6585
/// <summary>Constructs an <see cref="IGameRepository"/> for this test class's <see cref="Engine"/> with a custom error reporter
6686
/// to observe init-time assertions (e.g. <see cref="EngineAssertKind.FileNotFound"/> for missing patches).</summary>
6787
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);
88+
{
89+
return CreateRepository(Engine, repo, errorReporter);
90+
}
7491

75-
private IGameRepository CreateRepository(GameEngineType engine, VirtualGameRepo repo, IGameEngineErrorReporter? errorReporter)
92+
private GameRepository CreateRepository(GameEngineType engine, VirtualGameRepo repo, IGameEngineErrorReporter? errorReporter)
7693
{
7794
if (repo == null)
7895
throw new ArgumentNullException(nameof(repo));
@@ -94,59 +111,123 @@ protected static string ReadAll(Stream stream)
94111
using (var reader = new StreamReader(stream, Encoding.UTF8))
95112
return reader.ReadToEnd();
96113
}
114+
115+
public static TheoryData<PetroglyphFileSystemStrategy> SupportedFileSystemStrategies()
116+
{
117+
var data = new TheoryData<PetroglyphFileSystemStrategy>();
118+
foreach (var strategy in PetroglyphFileSystemTestHelpers.SupportedForCurrentOS())
119+
data.Add(strategy);
120+
return data;
121+
}
97122

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()
123+
[Theory]
124+
[MemberData(nameof(SupportedFileSystemStrategies))]
125+
public void Lookup_IsCaseAndSeparatorInsensitive_AcrossFilesystemAndMeg(PetroglyphFileSystemStrategy strategy)
128126
{
129-
var setup = GetLookupSetup();
127+
var fixture = BuildCaseInsensitivityFixture();
130128

131129
using var virt = CreateBuilder()
132-
.WithGame(setup.PopulateGame)
130+
.ConfigureGame(fixture.PopulateGame)
133131
.Build();
134132
var gameRepo = CreateRepository(virt);
135-
var repoUnderTest = setup.SelectRepository(gameRepo);
133+
gameRepo.PGFileSystem.ApplyStrategy(strategy);
134+
var repoUnderTest = fixture.SelectRepository(gameRepo);
136135

137136
var separatorRandom = new Random(42);
138137
for (var i = 0; i < 32; i++)
139138
{
140-
var fsVariant = JitterSeparators(string.ShuffleCasing(setup.FilesystemLookup), separatorRandom);
141-
var megVariant = JitterSeparators(string.ShuffleCasing(setup.MegLookup), separatorRandom);
139+
var fsVariant = JitterSeparators(string.ShuffleCasing(fixture.FilesystemLookup), separatorRandom);
140+
var megVariant = JitterSeparators(string.ShuffleCasing(fixture.MegLookup), separatorRandom);
142141

143142
Assert.True(repoUnderTest.FileExists(fsVariant), $"Filesystem variant '{fsVariant}' should resolve.");
144143
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)));
144+
Assert.Equal(fixture.FilesystemContent, ReadAll(repoUnderTest.OpenFile(fsVariant)));
145+
Assert.Equal(fixture.MegContent, ReadAll(repoUnderTest.OpenFile(megVariant)));
147146
}
148147
}
149148

149+
#region Default loading chain priority
150+
151+
[Theory]
152+
[MemberData(nameof(SupportedFileSystemStrategies))]
153+
public void Priority_ResolvesAccordingToEngineLoadOrder(PetroglyphFileSystemStrategy strategy)
154+
{
155+
var fixture = BuildPriorityFixture();
156+
var order = ExpectedLoadOrder;
157+
158+
// Sliding 'top' down the list makes each origin,
159+
// in turn, the highest-priority one holding the file (Example for FOC):
160+
// top = mod -> all four origins hold it -> mod must win
161+
// top = game -> game, MEG, fallback hold it -> game must win (mod is empty)
162+
// top = MEG -> MEG, fallback hold it -> MEG must win (mod, game empty)
163+
// top = fallback -> only fallback holds it -> fallback wins
164+
for (var top = 0; top < order.Count; top++)
165+
{
166+
var builder = CreateBuilder();
167+
for (var i = top; i < order.Count; i++)
168+
WriteLayer(builder, order[i], fixture.ResolvablePath, order[i].ToString());
169+
170+
using var repo = builder.Build();
171+
var gameRepo = CreateRepository(repo);
172+
gameRepo.PGFileSystem.ApplyStrategy(strategy);
173+
var repoUnderTest = fixture.SelectRepository(gameRepo);
174+
175+
var winner = order[top];
176+
Assert.Equal(winner.ToString(), ReadAll(repoUnderTest.OpenFile(fixture.ResolvablePath)));
177+
}
178+
}
179+
180+
private static void WriteLayer(VirtualGameRepoBuilder builder, RepositoryLayer layer, string path, string content)
181+
{
182+
switch (layer)
183+
{
184+
case RepositoryLayer.Mod:
185+
builder.WithMod("Mod", w => w.Write(path, content));
186+
break;
187+
case RepositoryLayer.Game:
188+
builder.ConfigureGame(g => g.Write(path, content));
189+
break;
190+
case RepositoryLayer.MasterMeg:
191+
builder.ConfigureGame(g => g.WriteMeg("Data/Patch.meg", meg => meg.Add(path, content)));
192+
break;
193+
case RepositoryLayer.Fallback:
194+
builder.WithFallbackGame(f => f.Write(path, content));
195+
break;
196+
default:
197+
throw new ArgumentOutOfRangeException(nameof(layer), layer, null);
198+
}
199+
}
200+
201+
[Fact]
202+
public void Priority_ModDeclarationOrderIsRespected()
203+
{
204+
var fixture = BuildPriorityFixture();
205+
206+
using var repo = CreateBuilder()
207+
.WithMod("ModA", m => m.Write(fixture.ResolvablePath, "A"))
208+
.WithMod("ModB", m => m.Write(fixture.ResolvablePath, "B"))
209+
.Build();
210+
var repoUnderTest = fixture.SelectRepository(CreateRepository(repo));
211+
212+
Assert.Equal("A", ReadAll(repoUnderTest.OpenFile(fixture.ResolvablePath)));
213+
}
214+
215+
[Fact]
216+
public void Priority_FallbackDeclarationOrderIsRespected()
217+
{
218+
var fixture = BuildPriorityFixture();
219+
220+
using var repo = CreateBuilder()
221+
.WithFallback("FallbackA", w => w.Write(fixture.ResolvablePath, "A"))
222+
.WithFallback("FallbackB", w => w.Write(fixture.ResolvablePath, "B"))
223+
.Build();
224+
var repoUnderTest = fixture.SelectRepository(CreateRepository(repo));
225+
226+
Assert.Equal("A", ReadAll(repoUnderTest.OpenFile(fixture.ResolvablePath)));
227+
}
228+
229+
#endregion
230+
150231
private static string JitterSeparators(string path, Random random)
151232
{
152233
var chars = path.ToCharArray();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using AnakinRaW.CommonUtilities.Hashing;
2+
using AnakinRaW.CommonUtilities.Testing;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using PG.Commons;
5+
using PG.StarWarsGame.Files.ALO;
6+
using PG.StarWarsGame.Files.MEG;
7+
using PG.StarWarsGame.Files.MTD;
8+
using PG.StarWarsGame.Files.XML;
9+
10+
namespace PG.StarWarsGame.Engine.Test;
11+
12+
/// <summary>
13+
/// Represents a base class for engine-bound tests, providing the necessary service
14+
/// registrations for constructing game repositories and related components.
15+
/// </summary>
16+
public abstract class EngineTestBase : TestBaseWithFileSystem
17+
{
18+
protected override void SetupServices(IServiceCollection serviceCollection)
19+
{
20+
base.SetupServices(serviceCollection);
21+
serviceCollection.AddSingleton<IHashingService>(sp => new HashingService(sp));
22+
23+
serviceCollection.SupportMTD();
24+
serviceCollection.SupportMEG();
25+
serviceCollection.SupportALO();
26+
serviceCollection.SupportXML();
27+
PetroglyphCommons.ContributeServices(serviceCollection);
28+
PetroglyphEngineServiceContribution.ContributeServices(serviceCollection);
29+
}
30+
}
Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,40 @@
11
using System;
22
using PG.StarWarsGame.Engine.ErrorReporting;
33
using PG.StarWarsGame.Engine.IO;
4+
using PG.StarWarsGame.Engine.IO.Repositories;
5+
using PG.StarWarsGame.Engine.Testing;
46
using Xunit;
57

68
namespace PG.StarWarsGame.Engine.Test.IO;
79

8-
public abstract class GameRepositoryFactoryTests : EngineRepositoryTestBase
10+
/// <summary>
11+
/// Tests <see cref="GameRepositoryFactory"/> engine dispatch in isolation. The factory is engine-agnostic
12+
/// infrastructure, so it is a standalone test class rather than an engine-bound repository test.
13+
/// </summary>
14+
public class GameRepositoryFactoryTests : EngineTestBase
915
{
16+
private GameRepository Create(GameEngineType engine, VirtualGameRepo repo)
17+
{
18+
var factory = new GameRepositoryFactory(ServiceProvider);
19+
return factory.Create(engine, repo.GameLocations, new GameEngineErrorReporterWrapper(null));
20+
}
21+
1022
[Fact]
11-
public void Create_ReportsMatchingEngineType()
23+
public void Create_Foc_ReturnsFocGameRepositoryWithMatchingEngineType()
1224
{
13-
using var repo = CreateBuilder().Build();
14-
var gameRepo = CreateRepository(repo);
25+
using var repo = new VirtualGameRepoBuilder(ServiceProvider).Build();
26+
27+
var gameRepo = Create(GameEngineType.Foc, repo);
1528

16-
Assert.Equal(Engine, gameRepo.EngineType);
29+
Assert.IsType<FocGameRepository>(gameRepo);
30+
Assert.Equal(GameEngineType.Foc, gameRepo.EngineType);
1731
}
1832

1933
[Fact]
20-
public void Create_EawEngine_ThrowsNotImplemented()
34+
public void Create_Eaw_ThrowsNotImplemented()
2135
{
22-
// Cross-engine: assert the factory rejects EaW today, regardless of the current engine.
23-
// Once EaW lands this test should be removed (or relocated to the EaW concrete class as
24-
// a "returns EawGameRepository" assertion).
25-
using var repo = CreateBuilder().Build();
26-
var factory = new GameRepositoryFactory(ServiceProvider);
36+
using var repo = new VirtualGameRepoBuilder(ServiceProvider).Build();
2737

28-
Assert.Throws<NotImplementedException>(() =>
29-
factory.Create(GameEngineType.Eaw, repo.GameLocations, new GameEngineErrorReporterWrapper(null)));
38+
Assert.Throws<NotImplementedException>(() => Create(GameEngineType.Eaw, repo));
3039
}
3140
}

0 commit comments

Comments
 (0)