11using System ;
2+ using System . Collections . Generic ;
23using System . IO ;
34using System . IO . Abstractions ;
45using System . Text ;
5- using AnakinRaW . CommonUtilities . Hashing ;
6- using AnakinRaW . CommonUtilities . Testing ;
76using AnakinRaW . CommonUtilities . Testing . Extensions ;
8- using Microsoft . Extensions . DependencyInjection ;
9- using PG . Commons ;
107using PG . StarWarsGame . Engine . ErrorReporting ;
118using PG . StarWarsGame . Engine . IO ;
9+ using PG . StarWarsGame . Engine . IO . Repositories ;
1210using 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 ;
1711using Testably . Abstractions ;
1812using Xunit ;
1913
2014namespace 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 ( ) ;
0 commit comments