Repository navigation
Expand file tree
/
Copy pathProcessRunnerTests.cs
More file actions
164 lines (147 loc) · 6.63 KB
/
Copy pathProcessRunnerTests.cs
File metadata and controls
164 lines (147 loc) · 6.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
using XISOSharp.Models;
namespace XISOSharp.Tests;
/// <summary>
/// Unit tests for <see cref="ProcessRunner"/> and <see cref="ProcessRunResult"/>:
/// start failures, output capture, line callbacks, exit codes, cancellation, and timeouts.
/// </summary>
public class ProcessRunnerTests
{
private static (string FileName, string[] Args) ShellCommand(string command) =>
OperatingSystem.IsWindows()
? ("cmd.exe", ["/c", command])
: ("sh", ["-c", command]);
private static (string FileName, string[] Args) LongRunningCommand() =>
OperatingSystem.IsWindows()
? ("cmd.exe", ["/c", "ping -n 30 127.0.0.1"])
: ("sh", ["-c", "sleep 30"]);
/// <summary>Dumps a file's raw bytes to stdout (cmd <c>type</c> / <c>cat</c>).</summary>
private static (string FileName, string[] Args) RawDumpCommand(string path) =>
OperatingSystem.IsWindows()
? ("cmd.exe", ["/c", "type", path])
: ("cat", [path]);
/// <summary>Verifies the default values of a fresh <see cref="ProcessRunResult"/>.</summary>
[Fact]
public void ProcessRunResult_Defaults()
{
ProcessRunResult result = new();
Assert.Equal(0, result.ExitCode);
Assert.Equal(string.Empty, result.StandardOutput);
Assert.Equal(string.Empty, result.StandardError);
}
/// <summary>Verifies a real process run captures stdout and reports exit code 0.</summary>
[Fact]
public async Task RunAsync_DotnetVersion_ReturnsZeroWithStdout()
{
// The suite churns the process-wide working directory in other
// collections, which can transiently break an external `dotnet` start
// (for example when the inherited CWD is mid-delete). Retry so this
// test measures ProcessRunner's capture, not the environment.
ProcessRunResult result = default!;
for (int attempt = 0; attempt < 3; attempt++)
{
result = await ProcessRunner.RunAsync("dotnet", ["--version"]);
if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.StandardOutput))
{
break;
}
}
Assert.Equal(0, result.ExitCode);
Assert.False(string.IsNullOrWhiteSpace(result.StandardOutput));
}
/// <summary>Verifies stdout and stderr are captured and both reach the line callback.</summary>
[Fact]
public async Task RunAsync_CapturesStdoutStderrAndLines()
{
(string file, string[] args) = ShellCommand("echo hello & echo err 1>&2");
List<string> lines = [];
ProcessRunResult result = await ProcessRunner.RunAsync(file, args, onLine: lines.Add);
Assert.Equal(0, result.ExitCode);
Assert.Contains("hello", result.StandardOutput);
Assert.Contains("err", result.StandardError);
Assert.Contains(lines, l => l.Contains("hello", StringComparison.Ordinal));
Assert.Contains(lines, l => l.Contains("err", StringComparison.Ordinal));
}
/// <summary>
/// Verifies the optional output encoding decodes legacy Latin-1 bytes
/// (extract-xiso output) instead of mangling them into U+FFFD.
/// </summary>
[Fact]
public async Task RunAsync_Latin1OutputEncoding_DecodesHighBytes()
{
// Raw bytes for "café" in Latin-1: c a f 0xE9.
string latin1File = Path.Combine(Path.GetTempPath(), $"xiso_latin1_{Guid.NewGuid():N}.bin");
await File.WriteAllBytesAsync(latin1File, [0x63, 0x61, 0x66, 0xE9]);
try
{
(string file, string[] args) = RawDumpCommand(latin1File);
ProcessRunResult result = await ProcessRunner.RunAsync(file, args,
outputEncoding: System.Text.Encoding.Latin1);
Assert.Equal(0, result.ExitCode);
Assert.Contains("caf\u00E9", result.StandardOutput, StringComparison.Ordinal);
}
finally
{
File.Delete(latin1File);
}
}
/// <summary>Verifies a non-zero exit code is propagated unchanged.</summary>
[Fact]
public async Task RunAsync_NonZeroExit_IsReported()
{
(string file, string[] args) = ShellCommand("exit 7");
ProcessRunResult result = await ProcessRunner.RunAsync(file, args);
Assert.Equal(7, result.ExitCode);
}
/// <summary>Verifies a missing executable yields exit code -1 with the error message.</summary>
[Fact]
public async Task RunAsync_MissingExecutable_ReturnsMinusOne()
{
ProcessRunResult result = await ProcessRunner.RunAsync("xisosharp-no-such-executable-xyz", []);
Assert.Equal(-1, result.ExitCode);
Assert.False(string.IsNullOrWhiteSpace(result.StandardError));
}
/// <summary>Verifies a null file name is rejected.</summary>
[Fact]
public async Task RunAsync_NullFileName_Throws()
{
await Assert.ThrowsAnyAsync<ArgumentException>(() => ProcessRunner.RunAsync(null!, []));
}
/// <summary>Verifies an empty file name is rejected.</summary>
[Fact]
public async Task RunAsync_EmptyFileName_Throws()
{
await Assert.ThrowsAnyAsync<ArgumentException>(() => ProcessRunner.RunAsync(string.Empty, []));
}
/// <summary>Verifies a null argument list is rejected.</summary>
[Fact]
public async Task RunAsync_NullArgs_Throws()
{
await Assert.ThrowsAsync<ArgumentNullException>(() => ProcessRunner.RunAsync("dotnet", null!));
}
/// <summary>Verifies a pre-canceled token aborts the run with an operation-canceled error.</summary>
[Fact]
public async Task RunAsync_PreCanceledToken_ThrowsOperationCanceled()
{
(string file, string[] args) = LongRunningCommand();
using CancellationTokenSource cts = new();
cts.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
ProcessRunner.RunAsync(file, args, cancellationToken: cts.Token));
}
/// <summary>Verifies an expired timeout kills the process and throws <see cref="TimeoutException"/>.</summary>
[Fact]
public async Task RunAsync_Timeout_ThrowsTimeoutException()
{
(string file, string[] args) = LongRunningCommand();
await Assert.ThrowsAsync<TimeoutException>(() =>
ProcessRunner.RunAsync(file, args, timeout: TimeSpan.FromMilliseconds(300)));
}
/// <summary>Verifies <see cref="Timeout.InfiniteTimeSpan"/> disables the timeout path.</summary>
[Fact]
public async Task RunAsync_InfiniteTimeout_WaitsForCompletion()
{
(string file, string[] args) = ShellCommand("exit 0");
ProcessRunResult result = await ProcessRunner.RunAsync(file, args, timeout: Timeout.InfiniteTimeSpan);
Assert.Equal(0, result.ExitCode);
}
}