Skip to content

Commit d568119

Browse files
**feat: add comprehensive unit tests and fix GameFileParser GDI parsing**
- Added test projects with coverage for `FileItem`, `GameFileParser`, `FileExtensions`, `PathUtils`, and `ArchiveService`. - Fixed GDI filename parsing with spaces by updating slice from `parts[4..^3]` to `parts[4..^1]`. - Improved `FileItem.DisplaySize` formatting with `CultureInfo.InvariantCulture`. - Added missing file checks and warning logs in `ArchiveService`. - Updated solution file with test project configuration.
1 parent 6e12307 commit d568119

12 files changed

Lines changed: 617 additions & 4 deletions
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
using System.IO.Compression;
2+
using System.Reflection;
3+
using BatchConvertToCHD.Services;
4+
5+
namespace BatchConvertToCHD.Tests;
6+
7+
public class ArchiveServiceTests : IDisposable
8+
{
9+
private readonly string _tempDir;
10+
11+
public ArchiveServiceTests()
12+
{
13+
_tempDir = Path.Combine(Path.GetTempPath(), $"ArchiveServiceTests_{Guid.NewGuid():N}");
14+
Directory.CreateDirectory(_tempDir);
15+
}
16+
17+
public void Dispose()
18+
{
19+
try
20+
{
21+
if (Directory.Exists(_tempDir))
22+
{
23+
Directory.Delete(_tempDir, true);
24+
}
25+
}
26+
catch
27+
{
28+
// ignore cleanup errors
29+
}
30+
31+
GC.SuppressFinalize(this);
32+
}
33+
34+
[Fact]
35+
public async Task ExtractArchiveAsyncMissingFileReturnsFailureAndLogsWarning()
36+
{
37+
var service = new ArchiveService("maxcso.exe", false);
38+
var missingPath = Path.Combine(_tempDir, "missing.7z");
39+
var tempDir = Path.Combine(_tempDir, "extract");
40+
var logs = new List<string>();
41+
42+
var result = await service.ExtractArchiveAsync(missingPath, tempDir, logs.Add, CancellationToken.None);
43+
44+
Assert.False(result.Success);
45+
Assert.Contains(logs, static msg => msg.Contains("WARNING", StringComparison.OrdinalIgnoreCase) && msg.Contains("missing", StringComparison.OrdinalIgnoreCase));
46+
Assert.Contains("File not found", result.ErrorMessage);
47+
}
48+
49+
[Fact]
50+
public async Task ExtractArchiveAsyncUnsupportedExtensionReturnsFailure()
51+
{
52+
var service = new ArchiveService("maxcso.exe", false);
53+
var filePath = Path.Combine(_tempDir, "test.tar");
54+
File.WriteAllText(filePath, "dummy");
55+
var tempDir = Path.Combine(_tempDir, "extract");
56+
57+
var result = await service.ExtractArchiveAsync(filePath, tempDir, static _ => { }, CancellationToken.None);
58+
59+
Assert.False(result.Success);
60+
Assert.Contains("Unsupported archive type", result.ErrorMessage);
61+
}
62+
63+
[Fact]
64+
public async Task ExtractArchiveAsyncValidZipExtractsFiles()
65+
{
66+
var service = new ArchiveService("maxcso.exe", false);
67+
var zipPath = Path.Combine(_tempDir, "test.zip");
68+
var tempDir = Path.Combine(_tempDir, "extract");
69+
Directory.CreateDirectory(tempDir);
70+
71+
// Create a zip with an ISO file inside
72+
await using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create))
73+
{
74+
var entry = archive.CreateEntry("game.iso");
75+
await using (var stream = entry.Open())
76+
{
77+
stream.WriteByte(0x01);
78+
}
79+
}
80+
81+
var result = await service.ExtractArchiveAsync(zipPath, tempDir, static _ => { }, CancellationToken.None);
82+
83+
Assert.True(result.Success);
84+
Assert.Single(result.FilePaths);
85+
Assert.EndsWith("game.iso", result.FilePaths[0], StringComparison.OrdinalIgnoreCase);
86+
}
87+
88+
[Fact]
89+
public async Task ExtractArchiveAsyncValidZipNoPrimaryFilesReturnsFailure()
90+
{
91+
var service = new ArchiveService("maxcso.exe", false);
92+
var zipPath = Path.Combine(_tempDir, "test.zip");
93+
var tempDir = Path.Combine(_tempDir, "extract");
94+
Directory.CreateDirectory(tempDir);
95+
96+
// Create a zip with a txt file (not a primary target)
97+
await using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create))
98+
{
99+
var entry = archive.CreateEntry("readme.txt");
100+
await using (var stream = entry.Open())
101+
{
102+
stream.WriteByte(0x01);
103+
}
104+
}
105+
106+
var result = await service.ExtractArchiveAsync(zipPath, tempDir, static _ => { }, CancellationToken.None);
107+
108+
Assert.False(result.Success);
109+
Assert.Contains("No supported primary files found", result.ErrorMessage);
110+
}
111+
112+
[Fact]
113+
public async Task ExtractCsoAsyncMaxCsoNotAvailableReturnsFailure()
114+
{
115+
var service = new ArchiveService("maxcso.exe", false);
116+
var tempIso = Path.Combine(_tempDir, "out.iso");
117+
118+
var result = await service.ExtractCsoAsync("input.cso", tempIso, _tempDir, static _ => { }, static _ => { }, CancellationToken.None);
119+
120+
Assert.False(result.Success);
121+
Assert.Contains("maxcso.exe is not available", result.ErrorMessage);
122+
}
123+
124+
[Fact]
125+
public void DisposeDoesNotThrow()
126+
{
127+
var service = new ArchiveService("maxcso.exe", false);
128+
var exception = Record.Exception(service.Dispose);
129+
Assert.Null(exception);
130+
}
131+
132+
[Fact]
133+
public void ExtractArchiveWithFallbackMissingFileThrowsFileNotFoundWithoutFallback()
134+
{
135+
var method = typeof(ArchiveService).GetMethod("ExtractArchiveWithFallback", BindingFlags.NonPublic | BindingFlags.Static);
136+
Assert.NotNull(method);
137+
138+
// Construct the generic method with a concrete archive type that satisfies the constraints
139+
var genericMethod = method.MakeGenericMethod(typeof(SharpCompress.Archives.Zip.ZipArchive));
140+
141+
var missingPath = Path.Combine(_tempDir, $"missing_{Guid.NewGuid():N}.7z");
142+
var outputDir = Path.Combine(_tempDir, "out");
143+
Directory.CreateDirectory(outputDir);
144+
var logs = new List<string>();
145+
146+
var ex = Record.Exception(() =>
147+
{
148+
genericMethod.Invoke(
149+
null,
150+
[
151+
missingPath,
152+
outputDir,
153+
(Action<string>)(logs.Add),
154+
".7z",
155+
(Func<Stream, SharpCompress.Archives.Zip.ZipArchive>)(static _ => throw new InvalidOperationException("Should not reach here")),
156+
CancellationToken.None
157+
]);
158+
});
159+
160+
Assert.NotNull(ex);
161+
var inner = ex is TargetInvocationException tie ? tie.InnerException : ex;
162+
Assert.IsType<FileNotFoundException>(inner);
163+
Assert.Contains(logs, static msg => msg.Contains("Skipping fallback", StringComparison.OrdinalIgnoreCase));
164+
}
165+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0-windows</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="coverlet.collector" Version="6.0.4" />
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
13+
<PackageReference Include="xunit" Version="2.9.3" />
14+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<Using Include="Xunit" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="..\BatchConvertToCHD\BatchConvertToCHD.csproj" />
23+
</ItemGroup>
24+
25+
</Project>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using BatchConvertToCHD.Utilities;
2+
3+
namespace BatchConvertToCHD.Tests;
4+
5+
public class FileExtensionsTests
6+
{
7+
[Theory]
8+
[InlineData(".zip", true)]
9+
[InlineData(".7z", true)]
10+
[InlineData(".rar", true)]
11+
[InlineData(".iso", false)]
12+
[InlineData(".cue", false)]
13+
public void ArchiveExtensionsSetContainsExpectedValues(string ext, bool expected)
14+
{
15+
Assert.Equal(expected, FileExtensions.ArchiveExtensionsSet.Contains(ext));
16+
}
17+
18+
[Theory]
19+
[InlineData(".cue", true)]
20+
[InlineData(".iso", true)]
21+
[InlineData(".img", true)]
22+
[InlineData(".gdi", true)]
23+
[InlineData(".chd", false)]
24+
[InlineData(".zip", false)]
25+
public void PrimaryTargetExtensionsSetContainsExpectedValues(string ext, bool expected)
26+
{
27+
Assert.Equal(expected, FileExtensions.PrimaryTargetExtensionsSet.Contains(ext));
28+
}
29+
30+
[Fact]
31+
public void AllSupportedInputExtensionsForConversionSetIsCaseInsensitive()
32+
{
33+
Assert.Contains(".ISO", FileExtensions.AllSupportedInputExtensionsForConversionSet);
34+
Assert.Contains(".7Z", FileExtensions.AllSupportedInputExtensionsForConversionSet);
35+
Assert.Contains(".ZIP", FileExtensions.AllSupportedInputExtensionsForConversionSet);
36+
}
37+
38+
[Fact]
39+
public void ChdExtensionIsCorrect()
40+
{
41+
Assert.Equal(".chd", FileExtensions.Chd);
42+
}
43+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using BatchConvertToCHD.Models;
2+
3+
namespace BatchConvertToCHD.Tests;
4+
5+
public class FileItemTests
6+
{
7+
[Fact]
8+
public void IsSelectedDefaultValueIsTrue()
9+
{
10+
var item = new FileItem();
11+
Assert.True(item.IsSelected);
12+
}
13+
14+
[Fact]
15+
public void IsSelectedPropertyChangedFiresEvent()
16+
{
17+
var item = new FileItem();
18+
var fired = false;
19+
item.PropertyChanged += (_, e) =>
20+
{
21+
if (e.PropertyName == nameof(FileItem.IsSelected))
22+
{
23+
fired = true;
24+
}
25+
};
26+
27+
item.IsSelected = false;
28+
29+
Assert.True(fired);
30+
}
31+
32+
[Fact]
33+
public void IsSelectedSameValueDoesNotFireEvent()
34+
{
35+
var item = new FileItem { IsSelected = true };
36+
var fired = false;
37+
item.PropertyChanged += (_, _) => { fired = true; };
38+
39+
item.IsSelected = true;
40+
41+
Assert.False(fired);
42+
}
43+
44+
[Fact]
45+
public void FileNamePropertyChangedFiresEvent()
46+
{
47+
var item = new FileItem();
48+
var fired = false;
49+
item.PropertyChanged += (_, e) =>
50+
{
51+
if (e.PropertyName == nameof(FileItem.FileName))
52+
{
53+
fired = true;
54+
}
55+
};
56+
57+
item.FileName = "test.iso";
58+
59+
Assert.True(fired);
60+
}
61+
62+
[Fact]
63+
public void FullPathPropertyChangedFiresEvent()
64+
{
65+
var item = new FileItem();
66+
var fired = false;
67+
item.PropertyChanged += (_, e) =>
68+
{
69+
if (e.PropertyName == nameof(FileItem.FullPath))
70+
{
71+
fired = true;
72+
}
73+
};
74+
75+
item.FullPath = @"C:\test.iso";
76+
77+
Assert.True(fired);
78+
}
79+
80+
[Fact]
81+
public void FileSizeSetsDisplaySize()
82+
{
83+
var item = new FileItem();
84+
var fired = false;
85+
item.PropertyChanged += (_, e) =>
86+
{
87+
if (e.PropertyName == nameof(FileItem.DisplaySize))
88+
{
89+
fired = true;
90+
}
91+
};
92+
93+
item.FileSize = 1536; // 1.5 KB
94+
95+
Assert.True(fired);
96+
Assert.Equal("1.5 KB", item.DisplaySize);
97+
}
98+
99+
[Theory]
100+
[InlineData(0, "0 B")]
101+
[InlineData(512, "512 B")]
102+
[InlineData(1024, "1 KB")]
103+
[InlineData(1536, "1.5 KB")]
104+
[InlineData(1048576, "1 MB")]
105+
[InlineData(1073741824, "1 GB")]
106+
public void DisplaySizeFormatsCorrectly(long bytes, string expected)
107+
{
108+
var item = new FileItem
109+
{
110+
// Set to a non-zero value first to ensure the setter runs even when testing 0
111+
FileSize = 1
112+
};
113+
item.FileSize = bytes;
114+
Assert.Equal(expected, item.DisplaySize);
115+
}
116+
}

0 commit comments

Comments
 (0)