Skip to content

Commit cfb5053

Browse files
committed
Add comprehensive unit tests for PG.StarWarsGame.Engine.FileSystem and include test project in solution
1 parent 3b58ea8 commit cfb5053

12 files changed

Lines changed: 909 additions & 17 deletions

ModVerify.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<Project Path="modules/ModdingToolBase/src/Updater/ExternalUpdater.Core/ExternalUpdater.Core.csproj" />
1818
</Folder>
1919
<Folder Name="/PetroglyphTools/">
20+
<Project Path="src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem.Test/PG.StarWarsGame.Engine.FileSystem.Test.csproj" />
2021
<Project Path="src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem/PG.StarWarsGame.Engine.FileSystem.csproj" />
2122
<Project Path="src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj" />
2223
<Project Path="src/PetroglyphTools/PG.StarWarsGame.Files.ALO/PG.StarWarsGame.Files.ALO.csproj" />
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
using System.IO;
3+
using AnakinRaW.CommonUtilities.FileSystem;
4+
using PG.StarWarsGame.Engine.Utilities;
5+
using Xunit;
6+
7+
namespace PG.StarWarsGame.Engine.FileSystem.Test.IO;
8+
9+
public partial class PetroglyphFileSystemTests
10+
{
11+
[Theory]
12+
#if Windows
13+
[InlineData("a", "b", "a\\b")]
14+
[InlineData("a/", "b", "a/b")]
15+
[InlineData("a\\", "b", "a\\b")]
16+
[InlineData("", "b", "b")]
17+
[InlineData("a", "", "a")]
18+
[InlineData("/", "b", "/b")]
19+
[InlineData("a", "/b", "/b")]
20+
[InlineData("a", "\\b", "\\b")]
21+
[InlineData("a/b", "c/d", "a/b\\c/d")]
22+
[InlineData("a\\b", "c\\d", "a\\b\\c\\d")]
23+
[InlineData("a/b/", "c/d", "a/b/c/d")]
24+
[InlineData("a\\b\\", "c\\d", "a\\b\\c\\d")]
25+
#else
26+
[InlineData("a", "b", "a/b")]
27+
[InlineData("a/", "b", "a/b")]
28+
[InlineData("a\\", "b", "a\\b")]
29+
[InlineData("", "b", "b")]
30+
[InlineData("a", "", "a")]
31+
[InlineData("/", "b", "/b")]
32+
[InlineData("a", "/b", "/b")]
33+
[InlineData("a", "\\b", "\\b")]
34+
[InlineData("a/b", "c/d", "a/b/c/d")]
35+
[InlineData("a\\b", "c\\d", "a\\b/c\\d")]
36+
[InlineData("a/b/", "c/d", "a/b/c/d")]
37+
[InlineData("a\\b\\", "c\\d", "a\\b\\c\\d")]
38+
#endif
39+
public void CombinePath(string pathA, string pathB, string expected)
40+
{
41+
var result = _pgFileSystem.CombinePath(pathA, pathB);
42+
Assert.Equal(expected, result);
43+
#if Windows
44+
Assert.Equal(Path.Combine(pathA, pathB), result);
45+
#endif
46+
}
47+
48+
[Theory]
49+
#if Windows
50+
[InlineData("a", "b", "a\\b")]
51+
[InlineData("a/", "b", "a/b")]
52+
[InlineData("a\\", "b", "a\\b")]
53+
[InlineData("", "b", "b")]
54+
[InlineData("a", "", "a")]
55+
[InlineData("/", "b", "/b")]
56+
[InlineData("a", "/b", "a/b")]
57+
[InlineData("a", "\\b", "a\\b")]
58+
[InlineData("a/b", "c/d", "a/b\\c/d")]
59+
[InlineData("a\\b", "c\\d", "a\\b\\c\\d")]
60+
#else
61+
[InlineData("a", "b", "a/b")]
62+
[InlineData("a/", "b", "a/b")]
63+
[InlineData("a\\", "b", "a\\b")]
64+
[InlineData("", "b", "b")]
65+
[InlineData("a", "", "a")]
66+
[InlineData("/", "b", "/b")]
67+
[InlineData("a", "/b", "a/b")]
68+
[InlineData("a", "\\b", "a\\b")]
69+
[InlineData("a/b", "c/d", "a/b/c/d")]
70+
[InlineData("a\\b", "c\\d", "a\\b/c\\d")]
71+
#endif
72+
public void JoinPath(string path1, string path2, string expected)
73+
{
74+
var vsb = new ValueStringBuilder();
75+
_pgFileSystem.JoinPath(path1.AsSpan(), path2.AsSpan(), ref vsb);
76+
var result = vsb.ToString();
77+
Assert.Equal(expected, result);
78+
#if Windows
79+
Assert.Equal(result, _fileSystem.Path.Join(path1, path2));
80+
#endif
81+
}
82+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
using System;
2+
using System.IO;
3+
using System.Runtime.InteropServices;
4+
using PG.StarWarsGame.Engine.Utilities;
5+
using Xunit;
6+
7+
namespace PG.StarWarsGame.Engine.FileSystem.Test.IO;
8+
9+
public partial class PetroglyphFileSystemTests
10+
{
11+
[Fact]
12+
public void FileExists_EmptyGameDirectory_Works()
13+
{
14+
var tempFile = Path.GetTempFileName();
15+
try
16+
{
17+
var vsb = new ValueStringBuilder();
18+
var exists = _pgFileSystem.FileExists(tempFile.AsSpan(), ref vsb, ReadOnlySpan<char>.Empty);
19+
Assert.True(exists);
20+
Assert.Equal(tempFile, vsb.ToString());
21+
}
22+
finally
23+
{
24+
if (File.Exists(tempFile))
25+
File.Delete(tempFile);
26+
}
27+
}
28+
29+
[Fact]
30+
public void FileExists_FileExists()
31+
{
32+
var tempFile = Path.GetTempFileName();
33+
try
34+
{
35+
var vsb = new ValueStringBuilder();
36+
var exists = _pgFileSystem.FileExists(tempFile.AsSpan(), ref vsb, string.Empty.AsSpan());
37+
Assert.True(exists);
38+
Assert.Equal(tempFile, vsb.ToString());
39+
}
40+
finally
41+
{
42+
if (File.Exists(tempFile))
43+
File.Delete(tempFile);
44+
}
45+
}
46+
47+
[Fact]
48+
public void FileExists_FileDoesNotExist()
49+
{
50+
var tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
51+
var vsb = new ValueStringBuilder();
52+
var exists = _pgFileSystem.FileExists(tempFile.AsSpan(), ref vsb, string.Empty.AsSpan());
53+
Assert.False(exists);
54+
}
55+
56+
[Fact]
57+
public void FileExists_RelativePath()
58+
{
59+
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
60+
Directory.CreateDirectory(tempDir);
61+
var tempFile = Path.Combine(tempDir, "test.txt");
62+
File.WriteAllText(tempFile, "test");
63+
try
64+
{
65+
var vsb = new ValueStringBuilder();
66+
var exists = _pgFileSystem.FileExists("test.txt".AsSpan(), ref vsb, tempDir.AsSpan());
67+
Assert.True(exists);
68+
69+
// On Windows, JoinPath might use backslashes.
70+
// PetroglyphFileSystem.JoinPath uses _underlyingFileSystem.Path.DirectorySeparatorChar if no separator is present.
71+
// Since _fileSystem is RealFileSystem, it will be \ on Windows.
72+
var expected = Path.Combine(tempDir, "test.txt");
73+
Assert.Equal(expected, vsb.ToString());
74+
}
75+
finally
76+
{
77+
Directory.Delete(tempDir, true);
78+
}
79+
}
80+
81+
[Theory]
82+
[InlineData("test.txt", "TEST.txt")]
83+
[InlineData("dir/test.txt", "DIR/TEST.txt")]
84+
[InlineData("a/b/c.txt", "A/B/C.txt")]
85+
[InlineData("A/B/C.txt", "a/b/c.txt")]
86+
[InlineData("a/B/c.txt", "A/b/C.txt")]
87+
public void FileExists_CaseInsensitive(string inputPath, string actualPathOnDisk)
88+
{
89+
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
90+
Directory.CreateDirectory(tempDir);
91+
92+
var fullPathOnDisk = Path.Combine(tempDir, actualPathOnDisk.Replace('/', Path.DirectorySeparatorChar));
93+
var fullPathOnDiskDir = Path.GetDirectoryName(fullPathOnDisk);
94+
if (fullPathOnDiskDir != null)
95+
Directory.CreateDirectory(fullPathOnDiskDir);
96+
97+
File.WriteAllText(fullPathOnDisk, "test");
98+
99+
try
100+
{
101+
var vsb = new ValueStringBuilder();
102+
// On Windows, CreateFile is case-insensitive by default.
103+
// On Linux, FileExistsCaseInsensitive handles it.
104+
var exists = _pgFileSystem.FileExists(inputPath.AsSpan(), ref vsb, tempDir.AsSpan());
105+
Assert.True(exists);
106+
107+
var resultPath = vsb.ToString();
108+
109+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
110+
{
111+
// Windows CreateFile doesn't change the path in the string builder to the actual case-sensitive path if it just found it.
112+
// It stays as what was passed to it (with gameDirectory joined).
113+
var expected = _fileSystem.Path.Combine(tempDir, inputPath);
114+
Assert.Equal(expected, resultPath);
115+
}
116+
else
117+
{
118+
// On Linux, FileExistsCaseInsensitive DOES update the string builder:
119+
// stringBuilder.Length = 0;
120+
// stringBuilder.Append(file);
121+
// It should be the exact path on disk.
122+
Assert.Equal(fullPathOnDisk, resultPath);
123+
}
124+
}
125+
finally
126+
{
127+
Directory.Delete(tempDir, true);
128+
}
129+
}
130+
131+
[Fact]
132+
public void FileExists_GameDirectory_WithTrailingSeparator()
133+
{
134+
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()) + Path.DirectorySeparatorChar;
135+
Directory.CreateDirectory(tempDir);
136+
var tempFile = Path.Combine(tempDir, "test.txt");
137+
File.WriteAllText(tempFile, "test");
138+
try
139+
{
140+
var vsb = new ValueStringBuilder();
141+
var exists = _pgFileSystem.FileExists("test.txt".AsSpan(), ref vsb, tempDir.AsSpan());
142+
Assert.True(exists);
143+
Assert.Equal(tempFile, vsb.ToString());
144+
}
145+
finally
146+
{
147+
Directory.Delete(tempDir, true);
148+
}
149+
}
150+
151+
[Theory]
152+
#if Windows
153+
[InlineData("C:\\test.txt", true)]
154+
[InlineData("/test.txt", false)] // On Windows, /test.txt is NOT fully qualified (it's root-relative to current drive)
155+
[InlineData("\\test.txt", false)]
156+
#else
157+
[InlineData("/test.txt", true)]
158+
[InlineData("C:\\test.txt", false)] // On Linux, C:\ is not a root
159+
#endif
160+
[InlineData("test.txt", false)]
161+
public void IsPathFullyQualified_Exists_Internal(string path, bool expected)
162+
{
163+
// This method is internal/private, but we can indirectly test it through FileExists or use reflection if we want to be explicit.
164+
// Actually, FileExists calls it.
165+
// If it's fully qualified, it doesn't join with gameDirectory.
166+
167+
var gameDir = "Z:\\non-existent-dir";
168+
var vsb = new ValueStringBuilder();
169+
_pgFileSystem.FileExists(path.AsSpan(), ref vsb, gameDir.AsSpan());
170+
171+
var resultPath = vsb.ToString();
172+
if (expected)
173+
{
174+
Assert.StartsWith(path, resultPath);
175+
Assert.DoesNotContain(gameDir, resultPath);
176+
}
177+
else
178+
{
179+
Assert.Contains(gameDir, resultPath);
180+
}
181+
}
182+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using AnakinRaW.CommonUtilities.FileSystem;
5+
using Xunit;
6+
7+
namespace PG.StarWarsGame.Engine.FileSystem.Test.IO;
8+
9+
public partial class PetroglyphFileSystemTests
10+
{
11+
public static TheoryData<string, string> TestData_GetFileName => new()
12+
{
13+
{ ".", "." },
14+
{ "..", ".." },
15+
{ "file", "file" },
16+
{ "file.", "file." },
17+
{ "file.exe", "file.exe" },
18+
{ " . ", " . " },
19+
{ " .. ", " .. " },
20+
{ "fi le", "fi le" },
21+
{ Path.Combine("baz", "file.exe"), "file.exe" },
22+
{ Path.Combine("baz", "file.exe") + "/", "" },
23+
{ Path.Combine("bar", "baz", "file.exe"), "file.exe" },
24+
{ Path.Combine("bar", "baz", "file.exe") + "\\", "" },
25+
26+
{ "foo\\bar/file.exe", "file.exe" },
27+
{ "foo/bar\\file.exe", "file.exe" },
28+
};
29+
30+
[Theory, MemberData(nameof(TestData_GetFileName))]
31+
public void GetFileName_Span(string path, string expected)
32+
{
33+
PathAssert.Equal(expected.AsSpan(), _pgFileSystem.GetFileName(path.AsSpan()));
34+
Assert.Equal(expected, _pgFileSystem.GetFileName(path));
35+
}
36+
37+
public static IEnumerable<object[]> TestData_GetFileName_Volume()
38+
{
39+
yield return [":", ":"];
40+
yield return [".:", ".:"];
41+
yield return [".:.", ".:."]; // Not a valid drive letter
42+
yield return ["file:", "file:"];
43+
yield return [":file", ":file"];
44+
yield return ["file:exe", "file:exe"];
45+
yield return ["baz\\file:exe", "file:exe"];
46+
yield return ["bar/baz/file:exe", "file:exe"];
47+
}
48+
49+
[Theory, MemberData(nameof(TestData_GetFileName_Volume))]
50+
public void GetFileName_Volume(string path, string expected)
51+
{
52+
// We used to break on ':' on Windows. This is a valid file name character for alternate data streams.
53+
// Additionally, the character can show up on unix volumes mounted to Windows.
54+
#if !NETFRAMEWORK
55+
Assert.Equal(expected, Path.GetFileName(path));
56+
Assert.Equal(expected, _pgFileSystem.GetFileName(path));
57+
#if Windows
58+
Assert.Equal(_pgFileSystem.GetFileName(path), _fileSystem.Path.GetFileName(path.AsSpan()));
59+
#endif
60+
#endif
61+
62+
PathAssert.Equal(expected.AsSpan(), _pgFileSystem.GetFileName(path.AsSpan()));
63+
}
64+
65+
public static TheoryData<string, string> TestData_GetFileNameWithoutExtension => new()
66+
{
67+
{ "", "" },
68+
{ "file", "file" },
69+
{ "file.exe", "file" },
70+
{ "bar\\baz/file.exe", "file" },
71+
{ "bar/baz\\file.exe", "file" },
72+
{ Path.Combine("bar", "baz") + "\\", "" },
73+
{ Path.Combine("bar", "baz") + "/", "" },
74+
};
75+
76+
[Theory, MemberData(nameof(TestData_GetFileNameWithoutExtension))]
77+
public void GetFileNameWithoutExtension_Span(string path, string expected)
78+
{
79+
PathAssert.Equal(expected.AsSpan(), _pgFileSystem.GetFileNameWithoutExtension(path.AsSpan()));
80+
Assert.Equal(expected, _pgFileSystem.GetFileNameWithoutExtension(path));
81+
#if Windows
82+
Assert.Equal(_pgFileSystem.GetFileName(path), _fileSystem.Path.GetFileName(path.AsSpan()));
83+
#endif
84+
}
85+
86+
[Theory,
87+
InlineData(null, null, null),
88+
InlineData(null, "exe", null),
89+
InlineData("", "", ""),
90+
InlineData("file.exe", null, "file"),
91+
InlineData("file.exe", "", "file."),
92+
InlineData("file", "exe", "file.exe"),
93+
InlineData("file", ".exe", "file.exe"),
94+
InlineData("file.txt", "exe", "file.exe"),
95+
InlineData("file.txt", ".exe", "file.exe"),
96+
InlineData("file.txt.bin", "exe", "file.txt.exe"),
97+
InlineData("dir/file.t", "exe", "dir/file.exe"),
98+
InlineData("dir\\file.t", "exe", "dir\\file.exe"),
99+
InlineData("dir/file.exe", "t", "dir/file.t"),
100+
InlineData("dir\\file.exe", "t", "dir\\file.t"),
101+
InlineData("dir/file", "exe", "dir/file.exe"),
102+
InlineData("dir\\file", "exe", "dir\\file.exe")]
103+
public void ChangeExtension(string? path, string? newExtension, string? expected)
104+
{
105+
Assert.Equal(expected, _pgFileSystem.ChangeExtension(path, newExtension));
106+
107+
#if Windows
108+
Assert.Equal(_pgFileSystem.ChangeExtension(path, newExtension), _fileSystem.Path.ChangeExtension(path, newExtension));
109+
#endif
110+
}
111+
}

0 commit comments

Comments
 (0)