Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.IO.Abstractions;
using System.Runtime.InteropServices;
using PG.StarWarsGame.Engine.IO;
using PG.StarWarsGame.Engine.IO.FileExistStrategies;
using PG.StarWarsGame.Engine.Utilities;
using Testably.Abstractions;
using Xunit;
Expand All @@ -20,6 +21,21 @@ protected override IFileSystem CreateFileSystem()

protected abstract override void ConfigureStrategy(PetroglyphFileSystem fs);

/// <summary>
/// Constructs a fresh, undisposed instance of the strategy under test, so generic suite
/// tests (<see cref="Dispose_CalledTwice_DoesNotThrow"/>) can exercise it directly without
/// fighting the <see cref="PetroglyphFileSystem"/>'s ownership of the active strategy.
/// </summary>
private protected abstract FileExistsStrategy CreateStrategyForDisposeTest();

[Fact]
public void Dispose_CalledTwice_DoesNotThrow()
{
var strategy = CreateStrategyForDisposeTest();
strategy.Dispose();
strategy.Dispose();
}

protected virtual void AssertResolvedPath(string expectedOnDiskPath, string actualResult)
{
var expected = expectedOnDiskPath.Replace('\\', FileSystem.Path.DirectorySeparatorChar).Replace('/', FileSystem.Path.DirectorySeparatorChar);
Expand Down Expand Up @@ -53,10 +69,6 @@ protected string NewTempDir()
return dir;
}

// ---------------------------------------------------------------------------------------------
// Shared tests — every strategy must satisfy.
// ---------------------------------------------------------------------------------------------

[Theory]
[InlineData("/gameDir")]
[InlineData(null)]
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System;
using PG.StarWarsGame.Engine.IO;
using PG.StarWarsGame.Engine.IO.FileExistStrategies;
using PG.StarWarsGame.Engine.Utilities;
using Xunit;

namespace PG.StarWarsGame.Engine.FileSystem.Test.IO.FileExistStrategies;

/// <summary>
/// Tests every <see cref="VirtualFileExistsStrategyBase"/>-derived strategy must satisfy:
/// per-directory snapshotting, no delegation for in-tree paths, delegation for out-of-tree
/// paths, and missing-directory handling.
/// </summary>
public abstract class VirtualFileExistsStrategyBaseTests : FileExistsStrategyTestBase
{
/// <summary>
/// Switch the active strategy on <paramref name="fs"/> to the strategy under test, with
/// <paramref name="underlying"/> as the fallback for outside-game-directory lookups.
/// </summary>
private protected abstract void ConfigureStrategy(PetroglyphFileSystem fs, FileExistsStrategy underlying);

[Fact]
public void FileExists_RepeatedCallsSameDirectory_BothResolveFromSnapshot()
{
var dir = NewTempDir();
var dataDir = FileSystem.Path.Combine(dir, "Mods", "Test", "Data", "Xml");
FileSystem.Directory.CreateDirectory(dataDir);
var foo = FileSystem.Path.Combine(dataDir, "foo.xml");
var bar = FileSystem.Path.Combine(dataDir, "bar.xml");
FileSystem.File.WriteAllText(foo, "1");
FileSystem.File.WriteAllText(bar, "2");

var sb1 = new ValueStringBuilder();
Assert.True(PgFileSystem.FileExists("MODS/TEST/DATA/XML/FOO.XML".AsSpan(), ref sb1, dir.AsSpan()));
AssertResolvedPath(foo, sb1.ToString());

var sb2 = new ValueStringBuilder();
Assert.True(PgFileSystem.FileExists("mods/test/data/xml/BAR.XML".AsSpan(), ref sb2, dir.AsSpan()));
AssertResolvedPath(bar, sb2.ToString());
}

[Fact]
public void FileExists_MissingDirectoryUnderGameRoot_RemainsMissing()
{
var dir = NewTempDir();
FileSystem.Directory.CreateDirectory(FileSystem.Path.Combine(dir, "Mods", "Test", "Data", "Xml"));
FileSystem.File.WriteAllText(FileSystem.Path.Combine(dir, "Mods", "Test", "Data", "Xml", "foo.xml"), "1");

Assert.False(FileExists("MODS/TEST/DATA/OTHER/foo.xml".AsSpan(), dir.AsSpan()));
Assert.False(FileExists("mods/test/data/other/bar.xml".AsSpan(), dir.AsSpan()));
}

[Fact]
public void FileExists_PathOutsideGameDirectory_DelegatesToUnderlying()
{
var root = NewTempDir();
var gameDir = FileSystem.Path.Combine(root, "game");
var outsideDir = FileSystem.Path.Combine(root, "outside");
FileSystem.Directory.CreateDirectory(gameDir);
FileSystem.Directory.CreateDirectory(outsideDir);
var file = FileSystem.Path.Combine(outsideDir, "FILE.TXT");
FileSystem.File.WriteAllText(file, "x");

var tracking = new TrackingFileExistsStrategy(FileSystem) { ReturnValue = true, ResolvedPath = file };
ConfigureStrategy(PgFileSystem, tracking);

var sb = new ValueStringBuilder();
Assert.True(PgFileSystem.FileExists(file.AsSpan(), ref sb, gameDir.AsSpan()));
AssertResolvedPath(file, sb.ToString());

Assert.Equal(1, tracking.CallCount);
}

[Fact]
public void FileExists_PathUnderGameDirectory_DoesNotDelegate()
{
var dir = NewTempDir();
var dataDir = FileSystem.Path.Combine(dir, "Data");
FileSystem.Directory.CreateDirectory(dataDir);
FileSystem.File.WriteAllText(FileSystem.Path.Combine(dataDir, "foo.xml"), "x");

var tracking = new TrackingFileExistsStrategy(FileSystem);
ConfigureStrategy(PgFileSystem, tracking);

Assert.True(FileExists("Data/foo.xml".AsSpan(), dir.AsSpan()));
Assert.Equal(0, tracking.CallCount);
}

[Fact]
public void FileExists_RepeatedLookupInSnapshottedDirectory_DoesNotDelegate()
{
var dir = NewTempDir();
var dataDir = FileSystem.Path.Combine(dir, "Data");
FileSystem.Directory.CreateDirectory(dataDir);
FileSystem.File.WriteAllText(FileSystem.Path.Combine(dataDir, "foo.xml"), "x");
FileSystem.File.WriteAllText(FileSystem.Path.Combine(dataDir, "bar.xml"), "y");

var tracking = new TrackingFileExistsStrategy(FileSystem);
ConfigureStrategy(PgFileSystem, tracking);

Assert.True(FileExists("Data/foo.xml".AsSpan(), dir.AsSpan()));
Assert.True(FileExists("Data/bar.xml".AsSpan(), dir.AsSpan()));
Assert.False(FileExists("Data/missing.xml".AsSpan(), dir.AsSpan()));

Assert.Equal(0, tracking.CallCount);
}

[Fact]
public void FileExists_MissingSubdirectoryUnderGameRoot_DoesNotDelegate()
{
var dir = NewTempDir();
FileSystem.Directory.CreateDirectory(FileSystem.Path.Combine(dir, "Data"));

var tracking = new TrackingFileExistsStrategy(FileSystem);
ConfigureStrategy(PgFileSystem, tracking);

Assert.False(FileExists("Data/Other/foo.xml".AsSpan(), dir.AsSpan()));
Assert.False(FileExists("Data/Other/bar.xml".AsSpan(), dir.AsSpan()));

Assert.Equal(0, tracking.CallCount);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.IO;
using PG.StarWarsGame.Engine.IO;
using PG.StarWarsGame.Engine.Utilities;
using PG.StarWarsGame.Engine.IO.FileExistStrategies;
using Xunit;

namespace PG.StarWarsGame.Engine.FileSystem.Test.IO.FileExistStrategies;
Expand All @@ -22,122 +21,29 @@ protected override void ConfigureStrategy(PetroglyphFileSystem fs)
=> fs.UseVirtualStrategy();
}

public abstract class VirtualFileExistsStrategyTests : FileExistsStrategyTestBase
public abstract class VirtualFileExistsStrategyTests : VirtualFileExistsStrategyBaseTests
{
[Fact]
public void FileExists_RepeatedCallsSameDirectory_BothResolveFromSnapshot()
{
var dir = NewTempDir();
var dataDir = Path.Combine(dir, "Mods", "Test", "Data", "Xml");
Directory.CreateDirectory(dataDir);
var foo = Path.Combine(dataDir, "foo.xml");
var bar = Path.Combine(dataDir, "bar.xml");
File.WriteAllText(foo, "1");
File.WriteAllText(bar, "2");

var sb1 = new ValueStringBuilder();
Assert.True(PgFileSystem.FileExists("MODS/TEST/DATA/XML/FOO.XML".AsSpan(), ref sb1, dir.AsSpan()));
AssertResolvedPath(foo, sb1.ToString());

var sb2 = new ValueStringBuilder();
Assert.True(PgFileSystem.FileExists("mods/test/data/xml/BAR.XML".AsSpan(), ref sb2, dir.AsSpan()));
AssertResolvedPath(bar, sb2.ToString());
}
private protected override void ConfigureStrategy(PetroglyphFileSystem fs, FileExistsStrategy underlying)
=> fs.UseVirtualStrategy(underlying);

[Fact]
public void FileExists_MissingDirectoryUnderGameRoot_RemainsMissing()
{
var dir = NewTempDir();
Directory.CreateDirectory(Path.Combine(dir, "Mods", "Test", "Data", "Xml"));
File.WriteAllText(Path.Combine(dir, "Mods", "Test", "Data", "Xml", "foo.xml"), "1");

Assert.False(FileExists("MODS/TEST/DATA/OTHER/foo.xml".AsSpan(), dir.AsSpan()));
Assert.False(FileExists("mods/test/data/other/bar.xml".AsSpan(), dir.AsSpan()));
}
private protected override FileExistsStrategy CreateStrategyForDisposeTest()
=> new VirtualFileExistsStrategy(FileSystem, new WineFileExistsStrategy(FileSystem));

[Fact]
public void FileExists_AfterFirstResolve_SnapshotServesSubsequentLookups()
{
var dir = NewTempDir();
var dataDir = Path.Combine(dir, "Data");
Directory.CreateDirectory(dataDir);
var file = Path.Combine(dataDir, "foo.xml");
File.WriteAllText(file, "x");
var dataDir = FileSystem.Path.Combine(dir, "Data");
FileSystem.Directory.CreateDirectory(dataDir);
var file = FileSystem.Path.Combine(dataDir, "foo.xml");
FileSystem.File.WriteAllText(file, "x");

Assert.True(FileExists("DATA/foo.xml".AsSpan(), dir.AsSpan()));

File.Delete(file);
FileSystem.File.Delete(file);

// Non-live strategy: snapshot is taken once and serves all subsequent lookups even if
// the file is deleted on disk. The live variant overrides this behavior.
Assert.True(FileExists("DATA/foo.xml".AsSpan(), dir.AsSpan()));
}

[Fact]
public void FileExists_PathOutsideGameDirectory_DelegatesToUnderlying()
{
var root = NewTempDir();
var gameDir = Path.Combine(root, "game");
var outsideDir = Path.Combine(root, "outside");
Directory.CreateDirectory(gameDir);
Directory.CreateDirectory(outsideDir);
var file = Path.Combine(outsideDir, "FILE.TXT");
File.WriteAllText(file, "x");

var tracking = new TrackingFileExistsStrategy(FileSystem) { ReturnValue = true, ResolvedPath = file };
PgFileSystem.UseVirtualStrategy(tracking);

var sb = new ValueStringBuilder();
Assert.True(PgFileSystem.FileExists(file.AsSpan(), ref sb, gameDir.AsSpan()));
AssertResolvedPath(file, sb.ToString());

Assert.Equal(1, tracking.CallCount);
}

[Fact]
public void FileExists_PathUnderGameDirectory_DoesNotDelegate()
{
var dir = NewTempDir();
var dataDir = Path.Combine(dir, "Data");
Directory.CreateDirectory(dataDir);
File.WriteAllText(Path.Combine(dataDir, "foo.xml"), "x");

var tracking = new TrackingFileExistsStrategy(FileSystem);
PgFileSystem.UseVirtualStrategy(tracking);

Assert.True(FileExists("Data/foo.xml".AsSpan(), dir.AsSpan()));
Assert.Equal(0, tracking.CallCount);
}

[Fact]
public void FileExists_RepeatedLookupInSnapshottedDirectory_DoesNotDelegate()
{
var dir = NewTempDir();
var dataDir = Path.Combine(dir, "Data");
Directory.CreateDirectory(dataDir);
File.WriteAllText(Path.Combine(dataDir, "foo.xml"), "x");
File.WriteAllText(Path.Combine(dataDir, "bar.xml"), "y");

var tracking = new TrackingFileExistsStrategy(FileSystem);
PgFileSystem.UseVirtualStrategy(tracking);

Assert.True(FileExists("Data/foo.xml".AsSpan(), dir.AsSpan()));
Assert.True(FileExists("Data/bar.xml".AsSpan(), dir.AsSpan()));
Assert.False(FileExists("Data/missing.xml".AsSpan(), dir.AsSpan()));

Assert.Equal(0, tracking.CallCount);
}

[Fact]
public void FileExists_MissingSubdirectoryUnderGameRoot_DoesNotDelegate()
{
var dir = NewTempDir();
Directory.CreateDirectory(Path.Combine(dir, "Data"));

var tracking = new TrackingFileExistsStrategy(FileSystem);
PgFileSystem.UseVirtualStrategy(tracking);

Assert.False(FileExists("Data/Other/foo.xml".AsSpan(), dir.AsSpan()));
Assert.False(FileExists("Data/Other/bar.xml".AsSpan(), dir.AsSpan()));

Assert.Equal(0, tracking.CallCount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.IO.Abstractions;
using AnakinRaW.CommonUtilities.Testing.Attributes;
using Microsoft.Extensions.DependencyInjection;
using PG.StarWarsGame.Engine.IO;
using PG.StarWarsGame.Engine.Utilities;
using Testably.Abstractions.Testing;
using Xunit;

namespace PG.StarWarsGame.Engine.FileSystem.Test.IO.FileExistStrategies;

/// <summary>
/// Exercises the snapshot path with the filesystem root (<c>/</c>) as the game directory.
/// Real-disk fixtures cannot create files at <c>/</c> without root privileges, so this uses
/// a Linux-simulated <see cref="MockFileSystem"/>. Only meaningful for the non-live variant —
/// the live variant's <see cref="System.IO.FileSystemWatcher"/> binds to the real OS, not the mock.
/// </summary>
public sealed class VirtualFileExistsStrategy_RootGameDirectoryTests
{
[PlatformSpecificFact(TestPlatformIdentifier.Linux)]
public void FileExists_GameDirectoryIsFilesystemRoot_ResolvesFromSnapshot()
{
var mockFs = new MockFileSystem();
mockFs.File.WriteAllText("/foo.xml", "x");

var pgFs = NewPgFs(mockFs);
var tracking = new TrackingFileExistsStrategy(mockFs);
pgFs.UseVirtualStrategy(tracking);

var sb = new ValueStringBuilder();
Assert.True(pgFs.FileExists("/foo.xml".AsSpan(), ref sb, "/".AsSpan()));
Assert.Equal("/foo.xml", sb.ToString());

// Lookup is under the game directory, so it must resolve from the snapshot, not delegate.
Assert.Equal(0, tracking.CallCount);
}

[PlatformSpecificFact(TestPlatformIdentifier.Linux)]
public void FileExists_GameDirectoryIsFilesystemRoot_MissingFile_ReportsFalseWithoutDelegating()
{
var mockFs = new MockFileSystem();
mockFs.File.WriteAllText("/foo.xml", "x");

var pgFs = NewPgFs(mockFs);
var tracking = new TrackingFileExistsStrategy(mockFs);
pgFs.UseVirtualStrategy(tracking);

var sb = new ValueStringBuilder();
Assert.False(pgFs.FileExists("/missing.xml".AsSpan(), ref sb, "/".AsSpan()));
Assert.Equal(0, tracking.CallCount);
}

private static PetroglyphFileSystem NewPgFs(IFileSystem fileSystem)
{
var sc = new ServiceCollection();
sc.AddSingleton(fileSystem);
return new PetroglyphFileSystem(sc.BuildServiceProvider());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Runtime.InteropServices;
using PG.StarWarsGame.Engine.IO;
using PG.StarWarsGame.Engine.IO.FileExistStrategies;
using Xunit;

namespace PG.StarWarsGame.Engine.FileSystem.Test.IO.FileExistStrategies;
Expand All @@ -17,4 +18,11 @@ protected override void ConfigureStrategy(PetroglyphFileSystem fs)
Assert.Skip("Windows strategy requires a Windows host.");
fs.UseWindowsStrategy();
}

private protected override FileExistsStrategy CreateStrategyForDisposeTest()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Assert.Skip("Windows strategy requires a Windows host.");
return new WindowsFileExistsStrategy(FileSystem);
}
}
Loading
Loading