Skip to content

Commit 3147625

Browse files
committed
Add integration style tests
1 parent 9b1eff8 commit 3147625

25 files changed

+460
-10
lines changed

global.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
"test:31": "dotnet r test -- --framework netcoreapp3.1",
1414
"test:5": "dotnet r test -- --framework net5.0",
1515
"test:6": "dotnet r test -- --framework net6.0",
16+
17+
"testbase": "dotnet test --no-build --logger \"trx;LogFilePrefix=tests\" --results-directory \"./.coverage\"",
18+
"test:unit": "dotnet r testbase -- --filter \"category=unit\"",
19+
"test:int": "dotnet r testbase -- --filter \"category=integration\"",
20+
1621
"pack": "dotnet pack --no-build --output \"./artifacts\"",
1722

1823
"build:release": "dotnet r build -- --configuration Release",

src/CommandBuilder.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,22 @@ internal class CommandBuilder
1111
private readonly IEnvironment _environment;
1212
private readonly Project _project;
1313
private readonly string _workingDirectory;
14+
private readonly bool _captureOutput;
1415

1516
public CommandBuilder(
1617
IConsoleWriter writer,
1718
IEnvironment environment,
1819
Project project,
19-
string workingDirectory)
20+
string workingDirectory,
21+
bool captureOutput)
2022
{
2123
if (string.IsNullOrEmpty(workingDirectory)) throw new ArgumentException($"'{nameof(workingDirectory)}' cannot be null or empty.", nameof(workingDirectory));
2224

2325
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
2426
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
2527
_project = project ?? throw new ArgumentNullException(nameof(project));
2628
_workingDirectory = workingDirectory;
29+
_captureOutput = captureOutput;
2730
}
2831

2932
public ProcessContext? ProcessContext { get; private set; }
@@ -44,6 +47,7 @@ public ICommandGroupRunner CreateGroupRunner(CancellationToken cancellationToken
4447
_environment,
4548
_project.Scripts!,
4649
ProcessContext!,
50+
_captureOutput,
4751
cancellationToken);
4852

4953
/// <summary>

src/CommandGroupRunner.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,30 @@ internal class CommandGroupRunner : ICommandGroupRunner
88
private readonly IEnvironment _environment;
99
private readonly IDictionary<string, string?> _scripts;
1010
private readonly ProcessContext _processContext;
11+
private readonly bool _captureOutput;
1112
private readonly CancellationToken _cancellationToken;
1213

1314
public CommandGroupRunner(
1415
IConsoleWriter writer,
1516
IEnvironment environment,
1617
IDictionary<string, string?> scripts,
1718
ProcessContext processContext,
19+
bool captureOutput,
1820
CancellationToken cancellationToken)
1921
{
2022
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
2123
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
2224
_scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
2325
_processContext = processContext ?? throw new ArgumentNullException(nameof(processContext));
26+
_captureOutput = captureOutput;
2427
_cancellationToken = cancellationToken;
2528
}
2629

2730
public virtual ICommandRunner BuildCommand()
2831
=> new CommandRunner(
2932
_writer,
3033
_processContext,
34+
_captureOutput,
3135
_cancellationToken);
3236

3337
public async Task<int> RunAsync(string name, string[]? scriptArgs)

src/CommandRunner.cs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ internal class CommandRunner : ICommandRunner
66
{
77
private readonly IConsoleWriter _writer;
88
private readonly ProcessContext _processContext;
9+
private readonly bool _captureOutput;
910
private readonly CancellationToken _cancellationToken;
1011

1112
public CommandRunner(
1213
IConsoleWriter writer,
1314
ProcessContext processContext,
15+
bool captureOutput,
1416
CancellationToken cancellationToken)
1517
{
1618
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
1719
_processContext = processContext ?? throw new ArgumentNullException(nameof(processContext));
20+
_captureOutput = captureOutput;
1821
_cancellationToken = cancellationToken;
1922
}
2023

@@ -23,14 +26,26 @@ public async Task<int> RunAsync(string name, string cmd, string[]? args)
2326
_cancellationToken.ThrowIfCancellationRequested();
2427

2528
_writer.Banner(name, ArgumentBuilder.ConcatinateCommandAndArgArrayForDisplay(cmd, args));
26-
_writer.LineVerbose("Using shell: {0}", _processContext.Shell);
27-
_writer.BlankLineVerbose();
2829

2930
using (var process = new Process())
3031
{
3132
process.StartInfo.WorkingDirectory = _processContext.WorkingDirectory;
3233
process.StartInfo.FileName = _processContext.Shell;
3334

35+
StreamForwarder? outStream = null;
36+
StreamForwarder? errStream = null;
37+
Task? taskOut = null;
38+
Task? taskErr = null;
39+
40+
if (_captureOutput)
41+
{
42+
process.StartInfo.RedirectStandardOutput = true;
43+
process.StartInfo.RedirectStandardError = true;
44+
45+
outStream = new StreamForwarder().Capture();
46+
errStream = new StreamForwarder().Capture();
47+
}
48+
3449
if (_processContext.IsCmd)
3550
{
3651
process.StartInfo.Arguments = string.Concat(
@@ -46,13 +61,22 @@ public async Task<int> RunAsync(string name, string cmd, string[]? args)
4661

4762
process.Start();
4863

49-
#if NET5_0_OR_GREATER
64+
if (_captureOutput)
65+
{
66+
taskOut = outStream!.BeginReadAsync(process.StandardOutput);
67+
taskErr = errStream!.BeginReadAsync(process.StandardError);
68+
}
69+
5070
await process.WaitForExitAsync(_cancellationToken);
51-
#else
52-
await Task.CompletedTask;
5371

54-
process.WaitForExit();
55-
#endif
72+
if (_captureOutput)
73+
{
74+
await taskOut!.WaitAsync(_cancellationToken);
75+
await taskErr!.WaitAsync(_cancellationToken);
76+
77+
_writer.Line(outStream!.CapturedOutput);
78+
_writer.Error(errStream!.CapturedOutput);
79+
}
5680

5781
return process.ExitCode;
5882
}

src/Polyfills.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#pragma warning disable IDE0060 // Remove unused parameter
2+
#pragma warning disable RCS1163 // Unused parameter.
3+
4+
namespace RunScript;
5+
6+
using System.Diagnostics;
7+
8+
internal static class Polyfills
9+
{
10+
#if !NET5_0_OR_GREATER
11+
public static async Task WaitForExitAsync(this Process process, CancellationToken cancellationToken)
12+
{
13+
await Task.CompletedTask;
14+
15+
process.WaitForExit();
16+
}
17+
#endif
18+
19+
#if !NET6_0_OR_GREATER
20+
public static async Task WaitAsync(this Task task, CancellationToken cancellationToken)
21+
{
22+
await Task.CompletedTask;
23+
24+
task.Wait(cancellationToken);
25+
}
26+
#endif
27+
}

src/RunScriptCommand.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ public async Task<int> InvokeAsync(InvocationContext context)
6161
writer,
6262
_environment,
6363
project,
64-
_workingDirectory);
64+
_workingDirectory,
65+
// For now we just write to the executing shell, later we can opt to write to the log instead
66+
captureOutput: false);
6567

6668
builder.SetUpEnvironment(scriptShell);
6769

src/StreamForwarder.cs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
namespace RunScript;
2+
3+
using System.Text;
4+
5+
// https://github.com/dotnet/sdk/blob/a758a468b71e15303198506a8de1040649aa0f35/src/Cli/Microsoft.DotNet.Cli.Utils/StreamForwarder.cs
6+
internal sealed class StreamForwarder
7+
{
8+
private static readonly char[] _ignoreCharacters = { '\r' };
9+
10+
private const char FlushBuilderCharacter = '\n';
11+
12+
private StringBuilder? _builder;
13+
#pragma warning disable IDISP006 // Implement IDisposable
14+
private StringWriter? _capture;
15+
#pragma warning restore IDISP006 // Implement IDisposable
16+
private Action<string>? _writeLine;
17+
private bool _trimTrailingCapturedNewline;
18+
19+
public string? CapturedOutput
20+
{
21+
get
22+
{
23+
var capture = _capture?.GetStringBuilder()?.ToString();
24+
25+
if (_trimTrailingCapturedNewline)
26+
{
27+
capture = capture?.TrimEnd('\r', '\n');
28+
}
29+
30+
return capture;
31+
}
32+
}
33+
34+
public StreamForwarder Capture(bool trimTrailingNewline = false)
35+
{
36+
ThrowIfCaptureSet();
37+
38+
_capture?.Dispose();
39+
_capture = new StringWriter();
40+
_trimTrailingCapturedNewline = trimTrailingNewline;
41+
42+
return this;
43+
}
44+
45+
public StreamForwarder ForwardTo(Action<string> writeLine)
46+
{
47+
ThrowIfNull(writeLine);
48+
49+
ThrowIfForwarderSet();
50+
51+
_writeLine = writeLine;
52+
53+
return this;
54+
}
55+
56+
public Task BeginReadAsync(TextReader reader)
57+
=> Task.Run(() => Read(reader));
58+
59+
public void Read(TextReader reader)
60+
{
61+
if (reader is null) throw new ArgumentNullException(nameof(reader));
62+
63+
const int bufferSize = 1;
64+
65+
char currentCharacter;
66+
67+
var buffer = new char[bufferSize];
68+
_builder = new StringBuilder();
69+
70+
// Using Read with buffer size 1 to prevent looping endlessly
71+
// like we would when using Read() with no buffer
72+
while ((_ = reader.Read(buffer, 0, bufferSize)) > 0)
73+
{
74+
currentCharacter = buffer[0];
75+
76+
if (currentCharacter == FlushBuilderCharacter)
77+
{
78+
WriteBuilder();
79+
}
80+
else if (!_ignoreCharacters.Contains(currentCharacter))
81+
{
82+
_builder.Append(currentCharacter);
83+
}
84+
}
85+
86+
// Flush anything else when the stream is closed
87+
// Which should only happen if someone used console.Write
88+
if (_builder.Length > 0)
89+
{
90+
WriteBuilder();
91+
}
92+
}
93+
94+
private void WriteBuilder()
95+
{
96+
if (_builder is not null)
97+
{
98+
WriteLine(_builder.ToString());
99+
_builder.Clear();
100+
}
101+
}
102+
103+
private void WriteLine(string str)
104+
{
105+
_capture?.WriteLine(str);
106+
107+
if (_writeLine is not null)
108+
{
109+
_writeLine(str);
110+
}
111+
}
112+
113+
private void ThrowIfNull(object obj)
114+
{
115+
if (obj is null)
116+
{
117+
throw new ArgumentNullException(nameof(obj));
118+
}
119+
}
120+
121+
private void ThrowIfForwarderSet()
122+
{
123+
if (_writeLine is not null)
124+
{
125+
throw new InvalidOperationException("WriteLine forwarder set previously");
126+
}
127+
}
128+
129+
private void ThrowIfCaptureSet()
130+
{
131+
if (_capture is not null)
132+
{
133+
throw new InvalidOperationException("Already capturing stream!");
134+
}
135+
}
136+
}

test/ArgumentBuilderTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace RunScript;
22

33
// https://github.com/dotnet/sdk/blob/09b31215867d1ffe4955fd5b7cd91eb552d3632c/src/Tests/Microsoft.DotNet.Cli.Utils.Tests/ArgumentEscaperTests.cs
4+
[Trait("category", "unit")]
45
public class ArgumentBuilderTests
56
{
67
[Theory]

test/CommandBuilderTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace RunScript;
44
using System.CommandLine.IO;
55
using System.CommandLine.Rendering;
66

7+
[Trait("category", "unit")]
78
[UsesVerify]
89
public class CommandBuilderTests
910
{
@@ -122,6 +123,7 @@ private static CommandBuilder SetUpTest(bool isWindows, string? comSpec = Defaul
122123
consoleWriter,
123124
environment,
124125
project,
125-
"/test/path");
126+
"/test/path",
127+
captureOutput: true);
126128
}
127129
}

test/CommandGroupRunnerTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace RunScript;
55
using System.CommandLine.Rendering;
66
using System.Threading.Tasks;
77

8+
[Trait("category", "unit")]
89
[UsesVerify]
910
public class CommandGroupRunnerTests
1011
{
@@ -316,6 +317,7 @@ private static (TestConsole console, CommandGroupRunner groupRunner) SetUpTest(b
316317
environment,
317318
scripts,
318319
context,
320+
true,
319321
default)));
320322

321323
return (console, groupRunner);

0 commit comments

Comments
 (0)