Skip to content

Commit 8b5695a

Browse files
Inject IConsole into ValidatorRunner and add console output tests
Agent-Logs-Url: https://github.com/304NotModified/SLNX-validator/sessions/2ef89553-2469-46d8-8587-2c62c427fa11 Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
1 parent 9aef6d8 commit 8b5695a

7 files changed

Lines changed: 141 additions & 9 deletions

File tree

src/SLNX-validator/IConsole.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace JulianVerdurmen.SlnxValidator;
2+
3+
internal interface IStandardStreamWriter
4+
{
5+
void Write(string value);
6+
}
7+
8+
internal interface IConsole
9+
{
10+
IStandardStreamWriter Out { get; }
11+
IStandardStreamWriter Error { get; }
12+
}

src/SLNX-validator/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public static async Task<int> Main(string[] args)
8181
var services = new ServiceCollection()
8282
.AddSlnxValidator()
8383
.AddSingleton<SlnxCollector>()
84+
.AddSingleton<IConsole>(new SystemConsole())
8485
.AddSingleton<ValidatorRunner>()
8586
.BuildServiceProvider();
8687

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace JulianVerdurmen.SlnxValidator;
2+
3+
internal sealed class SystemConsole : IConsole
4+
{
5+
public IStandardStreamWriter Out { get; } = new ConsoleStreamWriter(Console.Out);
6+
public IStandardStreamWriter Error { get; } = new ConsoleStreamWriter(Console.Error);
7+
8+
private sealed class ConsoleStreamWriter(TextWriter writer) : IStandardStreamWriter
9+
{
10+
public void Write(string value) => writer.Write(value);
11+
}
12+
}

src/SLNX-validator/ValidatorRunner.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
namespace JulianVerdurmen.SlnxValidator;
99

10-
internal sealed class ValidatorRunner(SlnxCollector collector, ISonarReporter sonarReporter, ISarifReporter sarifReporter, IRequiredFilesChecker requiredFilesChecker, IFileSystem fileSystem)
10+
internal sealed class ValidatorRunner(SlnxCollector collector, ISonarReporter sonarReporter, ISarifReporter sarifReporter, IRequiredFilesChecker requiredFilesChecker, IFileSystem fileSystem, IConsole console)
1111
{
1212
public async Task<int> RunAsync(ValidatorRunnerOptions options, CancellationToken cancellationToken)
1313
{
@@ -23,7 +23,7 @@ public async Task<int> RunAsync(ValidatorRunnerOptions options, CancellationToke
2323

2424
if (results.Count == 0)
2525
{
26-
await Console.Error.WriteLineAsync($"No .slnx files found for input: {options.Input}");
26+
console.Error.Write($"No .slnx files found for input: {options.Input}{Environment.NewLine}");
2727
return options.ContinueOnError ? 0 : 1;
2828
}
2929

@@ -34,14 +34,14 @@ public async Task<int> RunAsync(ValidatorRunnerOptions options, CancellationToke
3434
{
3535
await sonarReporter.WriteReportAsync(reportResults, options.SonarqubeReportPath);
3636
var size = fileSystem.GetFileSize(options.SonarqubeReportPath);
37-
Console.WriteLine($"SonarQube report written to: {options.SonarqubeReportPath} ({size} bytes)");
37+
console.Out.Write($"SonarQube report written to: {options.SonarqubeReportPath} ({size} bytes){Environment.NewLine}");
3838
}
3939

4040
if (options.SarifReportPath is not null)
4141
{
4242
await sarifReporter.WriteReportAsync(reportResults, options.SarifReportPath);
4343
var size = fileSystem.GetFileSize(options.SarifReportPath);
44-
Console.WriteLine($"SARIF report written to: {options.SarifReportPath} ({size} bytes)");
44+
console.Out.Write($"SARIF report written to: {options.SarifReportPath} ({size} bytes){Environment.NewLine}");
4545
}
4646

4747
var hasErrors = results.Any(r => r.Errors.Any(e => options.SeverityOverrides.IsFailingError(e.Code)));

tests/SLNX-validator.Tests/MockFileSystem.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ internal sealed class MockFileSystem : IFileSystem
77
{
88
private readonly HashSet<string> _existingPaths;
99
private readonly Dictionary<string, string> _fileContents;
10+
private readonly Dictionary<string, long> _fileSizes = new(StringComparer.OrdinalIgnoreCase);
1011

1112
/// <summary>Create a mock with files that exist but have no specific content.</summary>
1213
public MockFileSystem(params string[] existingPaths)
@@ -34,12 +35,44 @@ public Stream CreateFile(string path)
3435
{
3536
var ms = new MemoryStream();
3637
CreatedFiles[path] = ms;
37-
return ms;
38+
return new SizeCapturingStream(ms, size => _fileSizes[path] = size);
3839
}
3940
public Stream OpenRead(string path) =>
4041
new MemoryStream(Encoding.UTF8.GetBytes(_fileContents.GetValueOrDefault(path, "")));
4142
public Task<string> ReadAllTextAsync(string path, CancellationToken cancellationToken = default) =>
4243
Task.FromResult(_fileContents.GetValueOrDefault(path, ""));
4344
public long GetFileSize(string path) =>
45+
_fileSizes.TryGetValue(path, out var size) ? size :
4446
CreatedFiles.TryGetValue(path, out var ms) ? ms.Length : 0;
47+
48+
/// <summary>Captures the stream length before disposing the underlying <see cref="MemoryStream"/>.</summary>
49+
private sealed class SizeCapturingStream(MemoryStream inner, Action<long> onDispose) : Stream
50+
{
51+
public override bool CanRead => inner.CanRead;
52+
public override bool CanSeek => inner.CanSeek;
53+
public override bool CanWrite => inner.CanWrite;
54+
public override long Length => inner.Length;
55+
public override long Position { get => inner.Position; set => inner.Position = value; }
56+
public override void Flush() => inner.Flush();
57+
public override int Read(byte[] buffer, int offset, int count) => inner.Read(buffer, offset, count);
58+
public override long Seek(long offset, SeekOrigin origin) => inner.Seek(offset, origin);
59+
public override void SetLength(long value) => inner.SetLength(value);
60+
public override void Write(byte[] buffer, int offset, int count) => inner.Write(buffer, offset, count);
61+
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
62+
inner.WriteAsync(buffer, offset, count, cancellationToken);
63+
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) =>
64+
inner.WriteAsync(buffer, cancellationToken);
65+
public override Task FlushAsync(CancellationToken cancellationToken) => inner.FlushAsync(cancellationToken);
66+
67+
protected override void Dispose(bool disposing)
68+
{
69+
if (disposing)
70+
{
71+
onDispose(inner.Length);
72+
inner.Dispose();
73+
}
74+
base.Dispose(disposing);
75+
}
76+
}
4577
}
78+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Text;
2+
3+
namespace JulianVerdurmen.SlnxValidator.Tests;
4+
5+
internal sealed class TestConsole : IConsole
6+
{
7+
private readonly TestStreamWriter _out = new();
8+
private readonly TestStreamWriter _error = new();
9+
10+
public IStandardStreamWriter Out => _out;
11+
public IStandardStreamWriter Error => _error;
12+
13+
private sealed class TestStreamWriter : IStandardStreamWriter
14+
{
15+
private readonly StringBuilder _sb = new();
16+
17+
public void Write(string value) => _sb.Append(value);
18+
19+
public override string ToString() => _sb.ToString();
20+
}
21+
}

tests/SLNX-validator.Tests/ValidatorRunnerTests.cs

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,22 @@ namespace JulianVerdurmen.SlnxValidator.Tests;
99

1010
public class ValidatorRunnerTests
1111
{
12-
private static ValidatorRunner CreateRunner(IFileSystem fileSystem, IRequiredFilesChecker? checker = null)
12+
private static ValidatorRunner CreateRunner(IFileSystem fileSystem, IRequiredFilesChecker? checker = null, IConsole? console = null)
1313
{
1414
checker ??= Substitute.For<IRequiredFilesChecker>();
1515
var resolver = Substitute.For<ISlnxFileResolver>();
1616
var collector = new SlnxCollector(fileSystem, resolver, Substitute.For<ISlnxValidator>(), checker);
1717
var sonarReporter = new SonarReporter(fileSystem);
1818
var sarifReporter = new SarifReporter(fileSystem);
19-
return new ValidatorRunner(collector, sonarReporter, sarifReporter, checker, fileSystem);
19+
return new ValidatorRunner(collector, sonarReporter, sarifReporter, checker, fileSystem, console ?? new TestConsole());
2020
}
2121

2222
private static ValidatorRunnerOptions Options(string input = "test.slnx",
2323
bool continueOnError = false, string? requiredFilesPattern = null) =>
2424
new(input, SonarqubeReportPath: null, continueOnError, requiredFilesPattern, WorkingDirectory: ".");
2525

2626
private static ValidatorRunner CreateRunnerWithSlnx(
27-
string slnxPath, string slnxContent, IRequiredFilesChecker? checker = null)
27+
string slnxPath, string slnxContent, IRequiredFilesChecker? checker = null, IConsole? console = null)
2828
{
2929
checker ??= Substitute.For<IRequiredFilesChecker>();
3030
var fileSystem = new MockFileSystem(new Dictionary<string, string>
@@ -39,7 +39,7 @@ private static ValidatorRunner CreateRunnerWithSlnx(
3939
var collector = new SlnxCollector(fileSystem, resolver, validator, checker);
4040
var sonarReporter = new SonarReporter(fileSystem);
4141
var sarifReporter = new SarifReporter(fileSystem);
42-
return new ValidatorRunner(collector, sonarReporter, sarifReporter, checker, fileSystem);
42+
return new ValidatorRunner(collector, sonarReporter, sarifReporter, checker, fileSystem, console ?? new TestConsole());
4343
}
4444

4545
#region RunAsync – file resolution
@@ -248,5 +248,58 @@ public async Task RunAsync_IgnoreAllCodesMajorSpecificCode_SpecificCodeCausesExi
248248
}
249249

250250
#endregion
251+
252+
#region RunAsync – console output
253+
254+
[Test]
255+
public async Task RunAsync_NoFilesFound_WritesErrorToConsole()
256+
{
257+
// Arrange
258+
var console = new TestConsole();
259+
var runner = CreateRunner(new MockFileSystem(), console: console);
260+
261+
// Act
262+
await runner.RunAsync(Options("nonexistent.slnx"), CancellationToken.None);
263+
264+
// Assert
265+
console.Error.ToString().Should().Contain("No .slnx files found for input: nonexistent.slnx");
266+
}
267+
268+
[Test]
269+
public async Task RunAsync_SonarqubeReportPath_WritesConfirmationToConsole()
270+
{
271+
// Arrange
272+
var slnxPath = Path.GetFullPath("test.slnx");
273+
var console = new TestConsole();
274+
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", console: console);
275+
var options = new ValidatorRunnerOptions(slnxPath, SonarqubeReportPath: "report.xml",
276+
ContinueOnError: false, RequiredFilesPattern: null, WorkingDirectory: ".");
277+
278+
// Act
279+
await runner.RunAsync(options, CancellationToken.None);
280+
281+
// Assert
282+
console.Out.ToString().Should().Contain("SonarQube report written to: report.xml");
283+
}
284+
285+
[Test]
286+
public async Task RunAsync_SarifReportPath_WritesConfirmationToConsole()
287+
{
288+
// Arrange
289+
var slnxPath = Path.GetFullPath("test.slnx");
290+
var console = new TestConsole();
291+
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", console: console);
292+
var options = new ValidatorRunnerOptions(slnxPath, SonarqubeReportPath: null,
293+
ContinueOnError: false, RequiredFilesPattern: null, WorkingDirectory: ".",
294+
SarifReportPath: "report.sarif");
295+
296+
// Act
297+
await runner.RunAsync(options, CancellationToken.None);
298+
299+
// Assert
300+
console.Out.ToString().Should().Contain("SARIF report written to: report.sarif");
301+
}
302+
303+
#endregion
251304
}
252305

0 commit comments

Comments
 (0)