Skip to content

Commit 692d3bd

Browse files
Add test project with CI integration and internal API access
Added ImageSetToCBZ.Tests with xUnit tests for settings validation, CBZ creation, and log file output. Updated solution and project files to include the test project and grant internal access via InternalsVisibleTo. Enhanced CI workflow to run tests before build. No changes to main application logic.
1 parent 10c9ceb commit 692d3bd

9 files changed

Lines changed: 379 additions & 10 deletions

File tree

.github/workflows/build-and-release.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,23 @@ permissions:
88
contents: write
99

1010
jobs:
11+
test:
12+
name: Test
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- uses: actions/setup-dotnet@v4
19+
with:
20+
dotnet-version: '10.0.x'
21+
22+
- name: Run tests
23+
run: dotnet test ImageSetToCBZ.Tests/ImageSetToCBZ.Tests.csproj -c Release --verbosity normal
24+
1125
build:
1226
name: Build / ${{ matrix.rid }}
27+
needs: test
1328
runs-on: ${{ matrix.os }}
1429
strategy:
1530
matrix:
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using ImageSetToCBZ;
2+
3+
namespace ImageSetToCBZ.Tests;
4+
5+
public class ConvertSettingsTests : IDisposable
6+
{
7+
private readonly string _tempDir = Directory.CreateTempSubdirectory("cbz_settings_test_").FullName;
8+
9+
public void Dispose() => Directory.Delete(_tempDir, recursive: true);
10+
11+
[Fact]
12+
public void Validate_NonExistentInput_ReturnsError()
13+
{
14+
var settings = new ConvertSettings { Input = Path.Combine(_tempDir, "does_not_exist") };
15+
Assert.False(settings.Validate().Successful);
16+
}
17+
18+
[Fact]
19+
public void Validate_MirrorWithoutBatch_ReturnsError()
20+
{
21+
var settings = new ConvertSettings { Input = _tempDir, Mirror = true, Batch = false, Output = _tempDir };
22+
Assert.False(settings.Validate().Successful);
23+
}
24+
25+
[Fact]
26+
public void Validate_MirrorWithoutOutput_ReturnsError()
27+
{
28+
var settings = new ConvertSettings { Input = _tempDir, Mirror = true, Batch = true, Output = null };
29+
Assert.False(settings.Validate().Successful);
30+
}
31+
32+
[Fact]
33+
public void Validate_ValidSingleMode_ReturnsSuccess()
34+
{
35+
var settings = new ConvertSettings { Input = _tempDir };
36+
Assert.True(settings.Validate().Successful);
37+
}
38+
39+
[Fact]
40+
public void Validate_ValidBatchMode_ReturnsSuccess()
41+
{
42+
var settings = new ConvertSettings { Input = _tempDir, Batch = true };
43+
Assert.True(settings.Validate().Successful);
44+
}
45+
46+
[Fact]
47+
public void Validate_ValidMirrorMode_ReturnsSuccess()
48+
{
49+
var settings = new ConvertSettings { Input = _tempDir, Batch = true, Mirror = true, Output = _tempDir };
50+
Assert.True(settings.Validate().Successful);
51+
}
52+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
using ImageSetToCBZ;
2+
using System.IO.Compression;
3+
4+
namespace ImageSetToCBZ.Tests;
5+
6+
public class CreateCbzTests : IDisposable
7+
{
8+
private readonly string _root = Directory.CreateTempSubdirectory("cbz_create_test_").FullName;
9+
private readonly string _inputDir;
10+
private readonly string _outputDir;
11+
12+
public CreateCbzTests()
13+
{
14+
_inputDir = Directory.CreateDirectory(Path.Combine(_root, "input")).FullName;
15+
_outputDir = Directory.CreateDirectory(Path.Combine(_root, "output")).FullName;
16+
}
17+
18+
public void Dispose() => Directory.Delete(_root, recursive: true);
19+
20+
// -------------------------------------------------------------------------
21+
// Helpers
22+
// -------------------------------------------------------------------------
23+
24+
private static void Touch(string dir, params string[] names)
25+
{
26+
foreach (var name in names)
27+
File.WriteAllBytes(Path.Combine(dir, name), []);
28+
}
29+
30+
private string CbzPath(string? prepend = null)
31+
{
32+
var dirName = Path.GetFileName(_inputDir);
33+
var cbzName = prepend is null ? $"{dirName}.cbz" : $"{prepend} {dirName}.cbz";
34+
return Path.Combine(_outputDir, cbzName);
35+
}
36+
37+
private CbzTaskResult Run(bool renamePages = false, bool overwrite = false, string? prepend = null,
38+
CompressionLevel compression = CompressionLevel.NoCompression)
39+
=> ConvertCommand.CreateCbz(_inputDir, _outputDir, prepend, compression, renamePages, overwrite);
40+
41+
// -------------------------------------------------------------------------
42+
// Skipped cases
43+
// -------------------------------------------------------------------------
44+
45+
[Fact]
46+
public void EmptyFolder_ReturnsSkipped()
47+
{
48+
var result = Run();
49+
Assert.Equal(CreateResult.Skipped, result.Status);
50+
}
51+
52+
[Fact]
53+
public void NonImageFilesOnly_ReturnsSkipped()
54+
{
55+
Touch(_inputDir, "readme.txt", "data.json", "archive.zip");
56+
var result = Run();
57+
Assert.Equal(CreateResult.Skipped, result.Status);
58+
}
59+
60+
[Fact]
61+
public void ExistingFile_OverwriteFalse_ReturnsSkippedWithReason()
62+
{
63+
Touch(_inputDir, "001.jpg");
64+
Run(overwrite: true);
65+
66+
var result = Run(overwrite: false);
67+
Assert.Equal(CreateResult.Skipped, result.Status);
68+
Assert.Equal("already exists", result.ErrorMessage);
69+
}
70+
71+
// -------------------------------------------------------------------------
72+
// Success cases
73+
// -------------------------------------------------------------------------
74+
75+
[Fact]
76+
public void WithImages_ReturnsSuccess()
77+
{
78+
Touch(_inputDir, "001.jpg", "002.png");
79+
var result = Run();
80+
Assert.Equal(CreateResult.Success, result.Status);
81+
Assert.Equal(2, result.ImageCount);
82+
}
83+
84+
[Fact]
85+
public void WithImages_CreatesArchiveOnDisk()
86+
{
87+
Touch(_inputDir, "001.jpg");
88+
Run();
89+
Assert.True(File.Exists(CbzPath()));
90+
}
91+
92+
[Fact]
93+
public void WithImages_ArchiveContainsAllImages()
94+
{
95+
Touch(_inputDir, "001.jpg", "002.png", "003.gif");
96+
Run();
97+
using var archive = ZipFile.OpenRead(CbzPath());
98+
Assert.Equal(3, archive.Entries.Count);
99+
}
100+
101+
[Fact]
102+
public void NonImageFilesAreExcludedFromArchive()
103+
{
104+
Touch(_inputDir, "001.jpg", "notes.txt", "002.png");
105+
Run();
106+
using var archive = ZipFile.OpenRead(CbzPath());
107+
Assert.Equal(2, archive.Entries.Count);
108+
Assert.DoesNotContain(archive.Entries, e => e.Name == "notes.txt");
109+
}
110+
111+
[Fact]
112+
public void AllSupportedExtensions_AreIncluded()
113+
{
114+
Touch(_inputDir, "a.jpg", "b.jpeg", "c.png", "d.gif", "e.webp", "f.bmp", "g.tiff", "h.tif", "i.avif");
115+
var result = Run();
116+
Assert.Equal(9, result.ImageCount);
117+
}
118+
119+
[Fact]
120+
public void Prepend_CbzFileNameStartsWithPrepend()
121+
{
122+
Touch(_inputDir, "001.jpg");
123+
var result = Run(prepend: "Vol.1");
124+
Assert.StartsWith("Vol.1 ", result.CbzFileName);
125+
}
126+
127+
[Fact]
128+
public void ExistingFile_OverwriteTrue_Succeeds()
129+
{
130+
Touch(_inputDir, "001.jpg");
131+
Run(overwrite: true);
132+
var result = Run(overwrite: true);
133+
Assert.Equal(CreateResult.Success, result.Status);
134+
}
135+
136+
// -------------------------------------------------------------------------
137+
// Page renaming
138+
// -------------------------------------------------------------------------
139+
140+
[Fact]
141+
public void RenamePages_False_PreservesOriginalNames()
142+
{
143+
Touch(_inputDir, "001.jpg", "002.png");
144+
Run(renamePages: false);
145+
using var archive = ZipFile.OpenRead(CbzPath());
146+
var names = archive.Entries.Select(e => e.Name).ToHashSet();
147+
Assert.Contains("001.jpg", names);
148+
Assert.Contains("002.png", names);
149+
}
150+
151+
[Fact]
152+
public void RenamePages_True_EntriesFollowPageNNPattern()
153+
{
154+
Touch(_inputDir, "001.jpg", "002.png");
155+
Run(renamePages: true);
156+
using var archive = ZipFile.OpenRead(CbzPath());
157+
var names = archive.Entries.Select(e => e.Name).OrderBy(x => x).ToList();
158+
Assert.Equal("page_01.jpg", names[0]);
159+
Assert.Equal("page_02.png", names[1]);
160+
}
161+
162+
[Fact]
163+
public void RenamePages_TenImages_PadsToTwoDigits()
164+
{
165+
Touch(_inputDir, Enumerable.Range(1, 10).Select(i => $"{i:D3}.jpg").ToArray());
166+
Run(renamePages: true);
167+
using var archive = ZipFile.OpenRead(CbzPath());
168+
Assert.All(archive.Entries, e => Assert.Matches(@"^page_\d{2}\.jpg$", e.Name));
169+
}
170+
171+
[Fact]
172+
public void RenamePages_HundredImages_PadsToThreeDigits()
173+
{
174+
Touch(_inputDir, Enumerable.Range(1, 100).Select(i => $"{i:D3}.jpg").ToArray());
175+
Run(renamePages: true);
176+
using var archive = ZipFile.OpenRead(CbzPath());
177+
Assert.All(archive.Entries, e => Assert.Matches(@"^page_\d{3}\.jpg$", e.Name));
178+
}
179+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
global using Xunit;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
12+
<PackageReference Include="xunit.v3" Version="3.2.2" />
13+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
14+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
15+
<PrivateAssets>all</PrivateAssets>
16+
</PackageReference>
17+
<PackageReference Include="coverlet.collector" Version="8.0.1">
18+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
19+
<PrivateAssets>all</PrivateAssets>
20+
</PackageReference>
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<ProjectReference Include="..\ImageSetToCBZ\ImageSetToCBZ.csproj" />
25+
</ItemGroup>
26+
27+
</Project>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using ImageSetToCBZ;
2+
3+
namespace ImageSetToCBZ.Tests;
4+
5+
public class WriteLogFileTests : IDisposable
6+
{
7+
private readonly string _tempDir = Directory.CreateTempSubdirectory("cbz_log_test_").FullName;
8+
9+
public void Dispose() => Directory.Delete(_tempDir, recursive: true);
10+
11+
private static IReadOnlyList<CbzTaskResult> MakeResults(
12+
int succeeded = 0, int skipped = 0, int failed = 0)
13+
{
14+
var list = new List<CbzTaskResult>();
15+
for (var i = 0; i < succeeded; i++) list.Add(new(CreateResult.Success, $"ok_{i}.cbz", 5, null));
16+
for (var i = 0; i < skipped; i++) list.Add(new(CreateResult.Skipped, $"skip_{i}.cbz", 0, "no images found"));
17+
for (var i = 0; i < failed; i++) list.Add(new(CreateResult.Failed, $"fail_{i}.cbz", 0, "access denied"));
18+
return list;
19+
}
20+
21+
private string SingleLog() => Directory.GetFiles(_tempDir, "*.log").Single();
22+
23+
// -------------------------------------------------------------------------
24+
25+
[Fact]
26+
public void CreatesLogFile_InOutputDirectory()
27+
{
28+
ConvertCommand.WriteLogFile(_tempDir, "Single", @"C:\input", MakeResults(succeeded: 1));
29+
Assert.Single(Directory.GetFiles(_tempDir, "*.log"));
30+
}
31+
32+
[Fact]
33+
public void LogFile_HasTimestampedName()
34+
{
35+
ConvertCommand.WriteLogFile(_tempDir, "Single", @"C:\input", MakeResults(succeeded: 1));
36+
var name = Path.GetFileName(SingleLog());
37+
Assert.Matches(@"^ImageSetToCBZ_\d{8}_\d{6}\.log$", name);
38+
}
39+
40+
[Fact]
41+
public void LogFile_ContainsModeLabel()
42+
{
43+
ConvertCommand.WriteLogFile(_tempDir, "Batch (Mirror)", @"C:\input", MakeResults(succeeded: 1));
44+
Assert.Contains("Batch (Mirror)", File.ReadAllText(SingleLog()));
45+
}
46+
47+
[Fact]
48+
public void LogFile_ContainsInputPath()
49+
{
50+
ConvertCommand.WriteLogFile(_tempDir, "Single", @"C:\my\comics", MakeResults(succeeded: 1));
51+
Assert.Contains(@"C:\my\comics", File.ReadAllText(SingleLog()));
52+
}
53+
54+
[Fact]
55+
public void LogFile_ContainsPerEntryResults()
56+
{
57+
var results = MakeResults(succeeded: 1, skipped: 1, failed: 1);
58+
ConvertCommand.WriteLogFile(_tempDir, "Batch", @"C:\input", results);
59+
var content = File.ReadAllText(SingleLog());
60+
Assert.Contains("[OK ]", content);
61+
Assert.Contains("[SKIP]", content);
62+
Assert.Contains("[FAIL]", content);
63+
}
64+
65+
[Fact]
66+
public void LogFile_SummaryCountsAreCorrect()
67+
{
68+
ConvertCommand.WriteLogFile(_tempDir, "Batch", @"C:\input", MakeResults(succeeded: 3, skipped: 1, failed: 2));
69+
var content = File.ReadAllText(SingleLog());
70+
Assert.Contains("Created : 3", content);
71+
Assert.Contains("Skipped : 1", content);
72+
Assert.Contains("Failed : 2", content);
73+
}
74+
75+
[Fact]
76+
public void BlockedOutputPath_DoesNotThrow()
77+
{
78+
// Place a file where WriteLogFile would try to create a directory,
79+
// forcing an IOException inside the method that must be swallowed.
80+
var blockedPath = Path.Combine(_tempDir, "blocked");
81+
File.WriteAllText(blockedPath, "");
82+
83+
var ex = Record.Exception(() =>
84+
ConvertCommand.WriteLogFile(blockedPath, "Single", @"C:\input", MakeResults(succeeded: 1)));
85+
86+
Assert.Null(ex);
87+
}
88+
}

ImageSetToCBZ.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
<Solution>
22
<Project Path="ImageSetToCBZ/ImageSetToCBZ.csproj" />
3+
<Project Path="ImageSetToCBZ.Tests/ImageSetToCBZ.Tests.csproj" />
34
</Solution>

0 commit comments

Comments
 (0)