Skip to content

Commit 5891854

Browse files
committed
Fix flaky test by using unique filenames
Use Guid-based filenames in AddPixelText_WithAllParameters_UsesSpecifiedValues to prevent file locking conflicts during parallel test execution
1 parent f207c50 commit 5891854

9 files changed

Lines changed: 282 additions & 39 deletions

CreatePdf.NET.Tests/DocumentTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ public async Task AddPixelText_WithAllParameters_UsesSpecifiedValues()
109109

110110
doc.AddPixelText("Test", Dye.Yellow, Dye.Blue, PixelTextSize.Large);
111111

112-
Func<Task> act = () => doc.SaveAsync("test");
112+
var fileName = $"test_{Guid.NewGuid():N}";
113+
114+
Func<Task> act = () => doc.SaveAsync(fileName);
113115
await act.Should().NotThrowAsync().ConfigureAwait(true);
114116
}
115117

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using CreatePdf.NET.Internal;
2+
3+
namespace CreatePdf.NET.Tests;
4+
5+
public class RuntimeSystemEnvironmentTests
6+
{
7+
[Fact]
8+
public void Properties_MirrorUnderlyingEnvironment()
9+
{
10+
var env = RuntimeSystemEnvironment.Instance;
11+
12+
env.IsMacOS.Should().Be(OperatingSystem.IsMacOS());
13+
env.IsWindows.Should().Be(OperatingSystem.IsWindows());
14+
env.Is64BitOperatingSystem.Should().Be(Environment.Is64BitOperatingSystem);
15+
}
16+
17+
[Fact]
18+
public void FileExists_DelegatesToFileSystem()
19+
{
20+
var temp = Path.GetTempFileName();
21+
try
22+
{
23+
RuntimeSystemEnvironment.Instance.FileExists(temp).Should().BeTrue();
24+
}
25+
finally
26+
{
27+
File.Delete(temp);
28+
}
29+
30+
RuntimeSystemEnvironment.Instance.FileExists(temp).Should().BeFalse();
31+
}
32+
}

CreatePdf.NET.Tests/TempFileScope.cs

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using System.Diagnostics;
2+
using CreatePdf.NET.Internal;
3+
4+
namespace CreatePdf.NET.Tests;
5+
6+
public class TesseractOcrEngineTests
7+
{
8+
private const string AppleSiliconPath = "/opt/homebrew/bin/tesseract";
9+
private const string IntelMacPath = "/usr/local/bin/tesseract";
10+
11+
[Fact]
12+
public async Task ExtractTextFromImageAsync_WhenOutputIsMissing_ThrowsFileNotFound()
13+
{
14+
var processRunner = new FakeProcessRunner();
15+
var environment = new FakeSystemEnvironment { FileExistsImpl = _ => false };
16+
var engine = new TesseractOcrEngine(environment, processRunner);
17+
18+
var act = () => engine.ExtractTextFromImageAsync(
19+
"input.png",
20+
Path.Combine(Path.GetTempPath(), "missing-output.txt"),
21+
new OcrOptions { TesseractPath = "/bin/echo" });
22+
23+
await act.Should().ThrowAsync<FileNotFoundException>()
24+
.WithMessage("OCR output file not found. Tesseract execution failed*")
25+
.ConfigureAwait(true);
26+
27+
processRunner.StartInfos.Should().HaveCount(1);
28+
processRunner.StartInfos[0].FileName.Should().Be("/bin/echo");
29+
processRunner.StartInfos[0].Arguments.Should().Contain("input.png");
30+
}
31+
32+
[Fact]
33+
public void GetPdfRasterizerExecutable_UsesExplicitConverterPath()
34+
{
35+
var engine = new TesseractOcrEngine(new FakeSystemEnvironment(), new FakeProcessRunner());
36+
var options = new OcrOptions { PdfConverterPath = "/custom/gs" };
37+
38+
engine.GetPdfRasterizerExecutable(options).Should().Be("/custom/gs");
39+
}
40+
41+
[Fact]
42+
public void GetPdfRasterizerExecutable_MacOs_ReturnsSips()
43+
{
44+
var engine = new TesseractOcrEngine(new FakeSystemEnvironment { IsMacOS = true }, new FakeProcessRunner());
45+
46+
engine.GetPdfRasterizerExecutable(new OcrOptions()).Should().Be("/usr/bin/sips");
47+
}
48+
49+
[Theory]
50+
[InlineData(true, "gswin64c")]
51+
[InlineData(false, "gswin32c")]
52+
public void GetPdfRasterizerExecutable_Windows_SelectsBitnessSpecificCommand(bool is64Bit, string expected)
53+
{
54+
var environment = new FakeSystemEnvironment { IsWindows = true, Is64BitOperatingSystem = is64Bit };
55+
var engine = new TesseractOcrEngine(environment, new FakeProcessRunner());
56+
57+
engine.GetPdfRasterizerExecutable(new OcrOptions()).Should().Be(expected);
58+
}
59+
60+
[Fact]
61+
public void GetPdfRasterizerExecutable_DefaultsToUnixCommand()
62+
{
63+
var engine = new TesseractOcrEngine(new FakeSystemEnvironment(), new FakeProcessRunner());
64+
65+
engine.GetPdfRasterizerExecutable(new OcrOptions()).Should().Be("gs");
66+
}
67+
68+
[Fact]
69+
public void GetTesseractExecutable_UsesProvidedPath()
70+
{
71+
var engine = new TesseractOcrEngine(new FakeSystemEnvironment(), new FakeProcessRunner());
72+
var options = new OcrOptions { TesseractPath = "/custom/tesseract" };
73+
74+
engine.GetTesseractExecutable(options).Should().Be("/custom/tesseract");
75+
}
76+
77+
[Fact]
78+
public void GetTesseractExecutable_MacOsPrefersAppleSiliconBinary()
79+
{
80+
var environment = new FakeSystemEnvironment
81+
{
82+
IsMacOS = true,
83+
FileExistsImpl = path => string.Equals(path, AppleSiliconPath, StringComparison.Ordinal)
84+
};
85+
var engine = new TesseractOcrEngine(environment, new FakeProcessRunner());
86+
87+
engine.GetTesseractExecutable(new OcrOptions()).Should().Be(AppleSiliconPath);
88+
}
89+
90+
[Fact]
91+
public void GetTesseractExecutable_MacOsFallsBackToIntelBinary()
92+
{
93+
var environment = new FakeSystemEnvironment
94+
{
95+
IsMacOS = true,
96+
FileExistsImpl = path => string.Equals(path, IntelMacPath, StringComparison.Ordinal)
97+
};
98+
var engine = new TesseractOcrEngine(environment, new FakeProcessRunner());
99+
100+
engine.GetTesseractExecutable(new OcrOptions()).Should().Be(IntelMacPath);
101+
}
102+
103+
[Fact]
104+
public void GetTesseractExecutable_UsesFallbackWhenNoMacBinaryFound()
105+
{
106+
var engine = new TesseractOcrEngine(
107+
new FakeSystemEnvironment { IsMacOS = true },
108+
new FakeProcessRunner());
109+
110+
engine.GetTesseractExecutable(new OcrOptions()).Should().Be("tesseract");
111+
}
112+
113+
[Fact]
114+
public void GetRasterizationArguments_MacOs_UsesSipsFormat()
115+
{
116+
var engine = new TesseractOcrEngine(new FakeSystemEnvironment { IsMacOS = true }, new FakeProcessRunner());
117+
118+
engine.GetRasterizationArguments("file.pdf", "file.png", new OcrOptions { Dpi = 150 })
119+
.Should()
120+
.Be("-s format png -s dpiHeight 150 -s dpiWidth 150 \"file.pdf\" --out \"file.png\"");
121+
}
122+
123+
[Fact]
124+
public void GetRasterizationArguments_NonMac_UsesGhostscriptFormat()
125+
{
126+
var engine = new TesseractOcrEngine(new FakeSystemEnvironment(), new FakeProcessRunner());
127+
128+
engine.GetRasterizationArguments("file.pdf", "file.png", new OcrOptions { Dpi = 200 })
129+
.Should()
130+
.Be("-dNOPAUSE -dBATCH -sDEVICE=pngalpha -dFirstPage=1 -dLastPage=1 -r200 -sOutputFile=\"file.png\" \"file.pdf\" ");
131+
}
132+
133+
private sealed class FakeSystemEnvironment : ISystemEnvironment
134+
{
135+
public bool IsMacOS { get; set; }
136+
137+
public bool IsWindows { get; set; }
138+
139+
public bool Is64BitOperatingSystem { get; set; }
140+
141+
public Func<string, bool>? FileExistsImpl { get; set; }
142+
143+
public bool FileExists(string path) => (FileExistsImpl ?? (_ => false)).Invoke(path);
144+
}
145+
146+
private sealed class FakeProcessRunner : IProcessRunner
147+
{
148+
public List<ProcessStartInfo> StartInfos { get; } = [];
149+
150+
public Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken)
151+
{
152+
StartInfos.Add(startInfo);
153+
return Task.CompletedTask;
154+
}
155+
}
156+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System.Diagnostics;
2+
3+
namespace CreatePdf.NET.Internal;
4+
5+
internal interface IProcessRunner
6+
{
7+
Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken);
8+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace CreatePdf.NET.Internal;
2+
3+
internal interface ISystemEnvironment
4+
{
5+
bool IsMacOS { get; }
6+
7+
bool IsWindows { get; }
8+
9+
bool Is64BitOperatingSystem { get; }
10+
11+
bool FileExists(string path);
12+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Diagnostics;
2+
3+
namespace CreatePdf.NET.Internal;
4+
5+
internal sealed class ProcessRunner : IProcessRunner
6+
{
7+
public static ProcessRunner Instance { get; } = new();
8+
9+
public async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken)
10+
{
11+
ArgumentNullException.ThrowIfNull(startInfo);
12+
13+
using var process = Process.Start(startInfo)!;
14+
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
15+
}
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace CreatePdf.NET.Internal;
2+
3+
internal sealed class RuntimeSystemEnvironment : ISystemEnvironment
4+
{
5+
public static RuntimeSystemEnvironment Instance { get; } = new();
6+
7+
public bool IsMacOS => OperatingSystem.IsMacOS();
8+
9+
public bool IsWindows => OperatingSystem.IsWindows();
10+
11+
public bool Is64BitOperatingSystem => Environment.Is64BitOperatingSystem;
12+
13+
public bool FileExists(string path) => File.Exists(path);
14+
}

CreatePdf.NET/Internal/TesseractOcrEngine.cs

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,33 @@ internal sealed class TesseractOcrEngine : IPdfOcrEngine
1717
private const string GsWindows32 = "gswin32c";
1818
private const string GsUnix = "gs";
1919

20+
private readonly IProcessRunner _processRunner;
21+
private readonly ISystemEnvironment _systemEnvironment;
22+
23+
internal TesseractOcrEngine()
24+
: this(RuntimeSystemEnvironment.Instance, ProcessRunner.Instance)
25+
{
26+
}
27+
28+
internal TesseractOcrEngine(ISystemEnvironment systemEnvironment, IProcessRunner processRunner)
29+
{
30+
ArgumentNullException.ThrowIfNull(systemEnvironment);
31+
ArgumentNullException.ThrowIfNull(processRunner);
32+
33+
_systemEnvironment = systemEnvironment;
34+
_processRunner = processRunner;
35+
}
36+
2037
/// <inheritdoc />
2138
public async Task RasterizePdfToPngAsync(string pdfPath, string pngPath, OcrOptions options,
2239
CancellationToken cancellationToken = default)
2340
{
24-
using var process = Process.Start(CreateProcessInfo(
25-
GetPdfRasterizerExecutable(options),
26-
GetRasterizationArguments(pdfPath, pngPath, options)))!;
27-
28-
await process.WaitForExitAsync(cancellationToken);
41+
await _processRunner.RunAsync(
42+
CreateProcessInfo(
43+
GetPdfRasterizerExecutable(options),
44+
GetRasterizationArguments(pdfPath, pngPath, options)),
45+
cancellationToken)
46+
.ConfigureAwait(false);
2947
}
3048

3149
/// <inheritdoc />
@@ -34,49 +52,50 @@ public async Task<string> ExtractTextFromImageAsync(string pngPath, string txtPa
3452
{
3553
var outputBase = txtPath[..^4];
3654

37-
using var process = Process.Start(CreateProcessInfo(
38-
GetTesseractExecutable(options),
39-
GetOcrArguments(pngPath, outputBase, options)))!;
40-
41-
await process.WaitForExitAsync(cancellationToken);
55+
await _processRunner.RunAsync(
56+
CreateProcessInfo(
57+
GetTesseractExecutable(options),
58+
GetOcrArguments(pngPath, outputBase, options)),
59+
cancellationToken)
60+
.ConfigureAwait(false);
4261

43-
if (!File.Exists(txtPath))
62+
if (!_systemEnvironment.FileExists(txtPath))
4463
throw new FileNotFoundException("OCR output file not found. Tesseract execution failed.", txtPath);
4564

46-
var text = await File.ReadAllTextAsync(txtPath, cancellationToken);
65+
var text = await File.ReadAllTextAsync(txtPath, cancellationToken).ConfigureAwait(false);
4766
return text.Trim().Replace("\n", " ").Replace("\r", " ");
4867
}
4968

50-
private static string GetPdfRasterizerExecutable(OcrOptions options)
69+
internal string GetPdfRasterizerExecutable(OcrOptions options)
5170
{
5271
if (!string.IsNullOrEmpty(options.PdfConverterPath))
5372
return options.PdfConverterPath;
5473

55-
if (OperatingSystem.IsMacOS())
74+
if (_systemEnvironment.IsMacOS)
5675
return SipsUniversalPath;
5776

58-
if (OperatingSystem.IsWindows())
59-
return Environment.Is64BitOperatingSystem ? GsWindows64 : GsWindows32;
77+
if (_systemEnvironment.IsWindows)
78+
return _systemEnvironment.Is64BitOperatingSystem ? GsWindows64 : GsWindows32;
6079

6180
return GsUnix;
6281
}
6382

64-
private static string GetTesseractExecutable(OcrOptions options)
83+
internal string GetTesseractExecutable(OcrOptions options)
6584
{
6685
if (!string.IsNullOrEmpty(options.TesseractPath))
6786
return options.TesseractPath;
6887

69-
return OperatingSystem.IsMacOS() switch
88+
return _systemEnvironment.IsMacOS switch
7089
{
71-
true when File.Exists(TesseractAppleSiliconPath) => TesseractAppleSiliconPath,
72-
true when File.Exists(TesseractIntelMacPath) => TesseractIntelMacPath,
90+
true when _systemEnvironment.FileExists(TesseractAppleSiliconPath) => TesseractAppleSiliconPath,
91+
true when _systemEnvironment.FileExists(TesseractIntelMacPath) => TesseractIntelMacPath,
7392
_ => TesseractFallback
7493
};
7594
}
7695

77-
private static string GetRasterizationArguments(string pdfPath, string pngPath, OcrOptions options)
96+
internal string GetRasterizationArguments(string pdfPath, string pngPath, OcrOptions options)
7897
{
79-
return OperatingSystem.IsMacOS()
98+
return _systemEnvironment.IsMacOS
8099
? $"-s format png -s dpiHeight {options.Dpi.ToString(CultureInfo.InvariantCulture)} -s dpiWidth {options.Dpi.ToString(CultureInfo.InvariantCulture)} \"{pdfPath}\" --out \"{pngPath}\""
81100
: $"-dNOPAUSE -dBATCH -sDEVICE=pngalpha -dFirstPage=1 -dLastPage=1 -r{options.Dpi.ToString(CultureInfo.InvariantCulture)} -sOutputFile=\"{pngPath}\" \"{pdfPath}\" ";
82101
}

0 commit comments

Comments
 (0)