Skip to content

Commit 4da9119

Browse files
Address PR feedback: simplify IConsole, rename to FakeConsole, make console required, remove SizeCapturingStream
Agent-Logs-Url: https://github.com/304NotModified/SLNX-validator/sessions/d2fc4b14-e446-4e41-8704-97bcd27ae058 Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
1 parent 8b5695a commit 4da9119

7 files changed

Lines changed: 44 additions & 98 deletions

File tree

src/SLNX-validator/IConsole.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
namespace JulianVerdurmen.SlnxValidator;
22

3-
internal interface IStandardStreamWriter
4-
{
5-
void Write(string value);
6-
}
7-
83
internal interface IConsole
94
{
10-
IStandardStreamWriter Out { get; }
11-
IStandardStreamWriter Error { get; }
5+
Task WriteAsync(string value);
6+
Task WriteErrorAsync(string value);
127
}

src/SLNX-validator/SystemConsole.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ namespace JulianVerdurmen.SlnxValidator;
22

33
internal sealed class SystemConsole : IConsole
44
{
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-
}
5+
public Task WriteAsync(string value) => Console.Out.WriteAsync(value);
6+
public Task WriteErrorAsync(string value) => Console.Error.WriteAsync(value);
127
}

src/SLNX-validator/ValidatorRunner.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public async Task<int> RunAsync(ValidatorRunnerOptions options, CancellationToke
2323

2424
if (results.Count == 0)
2525
{
26-
console.Error.Write($"No .slnx files found for input: {options.Input}{Environment.NewLine}");
26+
await console.WriteErrorAsync($"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.Out.Write($"SonarQube report written to: {options.SonarqubeReportPath} ({size} bytes){Environment.NewLine}");
37+
await console.WriteAsync($"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.Out.Write($"SARIF report written to: {options.SarifReportPath} ({size} bytes){Environment.NewLine}");
44+
await console.WriteAsync($"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)));
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace JulianVerdurmen.SlnxValidator.Tests;
2+
3+
internal sealed class FakeConsole : IConsole
4+
{
5+
public List<string> Output { get; } = [];
6+
public List<string> ErrorOutput { get; } = [];
7+
8+
public Task WriteAsync(string value) { Output.Add(value); return Task.CompletedTask; }
9+
public Task WriteErrorAsync(string value) { ErrorOutput.Add(value); return Task.CompletedTask; }
10+
}

tests/SLNX-validator.Tests/MockFileSystem.cs

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ 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);
1110

1211
/// <summary>Create a mock with files that exist but have no specific content.</summary>
1312
public MockFileSystem(params string[] existingPaths)
@@ -35,44 +34,12 @@ public Stream CreateFile(string path)
3534
{
3635
var ms = new MemoryStream();
3736
CreatedFiles[path] = ms;
38-
return new SizeCapturingStream(ms, size => _fileSizes[path] = size);
37+
return ms;
3938
}
4039
public Stream OpenRead(string path) =>
4140
new MemoryStream(Encoding.UTF8.GetBytes(_fileContents.GetValueOrDefault(path, "")));
4241
public Task<string> ReadAllTextAsync(string path, CancellationToken cancellationToken = default) =>
4342
Task.FromResult(_fileContents.GetValueOrDefault(path, ""));
44-
public long GetFileSize(string path) =>
45-
_fileSizes.TryGetValue(path, out var size) ? size :
46-
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-
}
43+
public long GetFileSize(string path) => 0;
7744
}
7845

tests/SLNX-validator.Tests/TestConsole.cs

Lines changed: 0 additions & 21 deletions
This file was deleted.

tests/SLNX-validator.Tests/ValidatorRunnerTests.cs

Lines changed: 25 additions & 25 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, IConsole? console = null)
12+
private static ValidatorRunner CreateRunner(IFileSystem fileSystem, IConsole console, IRequiredFilesChecker? checker = 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, console ?? new TestConsole());
19+
return new ValidatorRunner(collector, sonarReporter, sarifReporter, checker, fileSystem, console);
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, IConsole? console = null)
27+
string slnxPath, string slnxContent, IConsole console, IRequiredFilesChecker? checker = 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, console ?? new TestConsole());
42+
return new ValidatorRunner(collector, sonarReporter, sarifReporter, checker, fileSystem, console);
4343
}
4444

4545
#region RunAsync – file resolution
@@ -48,7 +48,7 @@ private static ValidatorRunner CreateRunnerWithSlnx(
4848
public async Task RunAsync_FileNotFound_ContinueOnErrorFalse_ReturnsOne()
4949
{
5050
// Arrange
51-
var runner = CreateRunner(new MockFileSystem());
51+
var runner = CreateRunner(new MockFileSystem(), new FakeConsole());
5252

5353
// Act
5454
var exitCode = await runner.RunAsync(Options("nonexistent.slnx"), CancellationToken.None);
@@ -61,7 +61,7 @@ public async Task RunAsync_FileNotFound_ContinueOnErrorFalse_ReturnsOne()
6161
public async Task RunAsync_FileNotFound_ContinueOnErrorTrue_ReturnsZero()
6262
{
6363
// Arrange
64-
var runner = CreateRunner(new MockFileSystem());
64+
var runner = CreateRunner(new MockFileSystem(), new FakeConsole());
6565

6666
// Act
6767
var exitCode = await runner.RunAsync(Options("nonexistent.slnx", continueOnError: true), CancellationToken.None);
@@ -74,7 +74,7 @@ public async Task RunAsync_FileNotFound_ContinueOnErrorTrue_ReturnsZero()
7474
public async Task RunAsync_NoFilesFound_ContinueOnErrorFalse_ReturnsOne()
7575
{
7676
// Arrange
77-
var runner = CreateRunner(new MockFileSystem());
77+
var runner = CreateRunner(new MockFileSystem(), new FakeConsole());
7878

7979
// Act
8080
var exitCode = await runner.RunAsync(Options("src/*.slnx"), CancellationToken.None);
@@ -87,7 +87,7 @@ public async Task RunAsync_NoFilesFound_ContinueOnErrorFalse_ReturnsOne()
8787
public async Task RunAsync_NoFilesFound_ContinueOnErrorTrue_ReturnsZero()
8888
{
8989
// Arrange
90-
var runner = CreateRunner(new MockFileSystem());
90+
var runner = CreateRunner(new MockFileSystem(), new FakeConsole());
9191

9292
// Act
9393
var exitCode = await runner.RunAsync(Options("src/*.slnx", continueOnError: true), CancellationToken.None);
@@ -114,7 +114,7 @@ public async Task RunAsync_RequiredFiles_AllMatchedAndReferenced_ReturnsZero()
114114
checker.CheckInSlnx(Arg.Any<IReadOnlyList<string>>(), Arg.Any<SlnxFile>())
115115
.Returns([]);
116116

117-
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", checker);
117+
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", new FakeConsole(), checker);
118118

119119
// Act
120120
var exitCode = await runner.RunAsync(
@@ -134,7 +134,7 @@ public async Task RunAsync_RequiredFiles_NoMatchOnDisk_ReturnsOne()
134134
checker.ResolveMatchedPaths(Arg.Any<string>(), Arg.Any<string>())
135135
.Returns([]); // nothing matched on disk
136136

137-
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", checker);
137+
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", new FakeConsole(), checker);
138138

139139
// Act
140140
var exitCode = await runner.RunAsync(
@@ -155,7 +155,7 @@ public async Task RunAsync_RequiredFiles_NoMatchOnDisk_ReturnsOne()
155155
public async Task RunAsync_IgnoreAllCodes_WithErrors_ReturnsZero()
156156
{
157157
// Arrange: file with wrong extension generates SLNX002; --ignore * suppresses all codes
158-
var runner = CreateRunnerWithSlnx("test.xml", "<Solution />");
158+
var runner = CreateRunnerWithSlnx("test.xml", "<Solution />", new FakeConsole());
159159
var overrides = SeverityOverridesParser.Parse(null, null, null, null, null, ignore: "*");
160160

161161
// Act
@@ -171,7 +171,7 @@ public async Task RunAsync_IgnoreAllCodes_WithErrors_ReturnsZero()
171171
public async Task RunAsync_IgnoreSpecificCode_ThatCodeDoesNotCauseExitOne()
172172
{
173173
// Arrange: --ignore SLNX002 suppresses the InvalidExtension error
174-
var runner = CreateRunnerWithSlnx("test.xml", "<Solution />");
174+
var runner = CreateRunnerWithSlnx("test.xml", "<Solution />", new FakeConsole());
175175
var overrides = SeverityOverridesParser.Parse(null, null, null, null, null, ignore: "SLNX002");
176176

177177
// Act
@@ -187,7 +187,7 @@ public async Task RunAsync_IgnoreSpecificCode_ThatCodeDoesNotCauseExitOne()
187187
public async Task RunAsync_MinorOverrideForErrorCode_ReturnsZero()
188188
{
189189
// Arrange: --minor SLNX002 downgrades InvalidExtension to non-failing severity
190-
var runner = CreateRunnerWithSlnx("test.xml", "<Solution />");
190+
var runner = CreateRunnerWithSlnx("test.xml", "<Solution />", new FakeConsole());
191191
var overrides = SeverityOverridesParser.Parse(null, null, null, minor: "SLNX002", null, null);
192192

193193
// Act
@@ -203,7 +203,7 @@ public async Task RunAsync_MinorOverrideForErrorCode_ReturnsZero()
203203
public async Task RunAsync_InfoAllCodes_ReturnsZero()
204204
{
205205
// Arrange: --info * downgrades all codes to INFO (non-failing)
206-
var runner = CreateRunnerWithSlnx("test.xml", "<Solution />");
206+
var runner = CreateRunnerWithSlnx("test.xml", "<Solution />", new FakeConsole());
207207
var overrides = SeverityOverridesParser.Parse(null, null, null, null, info: "*", null);
208208

209209
// Act
@@ -219,7 +219,7 @@ public async Task RunAsync_InfoAllCodes_ReturnsZero()
219219
public async Task RunAsync_InfoAllCodesMajorSpecificCode_SpecificCodeCausesExitOne()
220220
{
221221
// Arrange: --info * --major SLNX002 → SLNX002 stays MAJOR (specific overrides wildcard)
222-
var runner = CreateRunnerWithSlnx("test.xml", "<Solution />");
222+
var runner = CreateRunnerWithSlnx("test.xml", "<Solution />", new FakeConsole());
223223
var overrides = SeverityOverridesParser.Parse(null, null, major: "SLNX002", null, info: "*", null);
224224

225225
// Act
@@ -235,7 +235,7 @@ public async Task RunAsync_InfoAllCodesMajorSpecificCode_SpecificCodeCausesExitO
235235
public async Task RunAsync_IgnoreAllCodesMajorSpecificCode_SpecificCodeCausesExitOne()
236236
{
237237
// Arrange: --ignore * --major SLNX002 → SLNX002 is MAJOR (specific wins over wildcard ignore)
238-
var runner = CreateRunnerWithSlnx("test.xml", "<Solution />");
238+
var runner = CreateRunnerWithSlnx("test.xml", "<Solution />", new FakeConsole());
239239
var overrides = SeverityOverridesParser.Parse(null, null, major: "SLNX002", null, null, ignore: "*");
240240

241241
// Act
@@ -255,40 +255,40 @@ public async Task RunAsync_IgnoreAllCodesMajorSpecificCode_SpecificCodeCausesExi
255255
public async Task RunAsync_NoFilesFound_WritesErrorToConsole()
256256
{
257257
// Arrange
258-
var console = new TestConsole();
259-
var runner = CreateRunner(new MockFileSystem(), console: console);
258+
var console = new FakeConsole();
259+
var runner = CreateRunner(new MockFileSystem(), console);
260260

261261
// Act
262262
await runner.RunAsync(Options("nonexistent.slnx"), CancellationToken.None);
263263

264264
// Assert
265-
console.Error.ToString().Should().Contain("No .slnx files found for input: nonexistent.slnx");
265+
console.ErrorOutput.Should().ContainMatch("*No .slnx files found for input: nonexistent.slnx*");
266266
}
267267

268268
[Test]
269269
public async Task RunAsync_SonarqubeReportPath_WritesConfirmationToConsole()
270270
{
271271
// Arrange
272272
var slnxPath = Path.GetFullPath("test.slnx");
273-
var console = new TestConsole();
274-
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", console: console);
273+
var console = new FakeConsole();
274+
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", console);
275275
var options = new ValidatorRunnerOptions(slnxPath, SonarqubeReportPath: "report.xml",
276276
ContinueOnError: false, RequiredFilesPattern: null, WorkingDirectory: ".");
277277

278278
// Act
279279
await runner.RunAsync(options, CancellationToken.None);
280280

281281
// Assert
282-
console.Out.ToString().Should().Contain("SonarQube report written to: report.xml");
282+
console.Output.Should().ContainMatch("*SonarQube report written to: report.xml*");
283283
}
284284

285285
[Test]
286286
public async Task RunAsync_SarifReportPath_WritesConfirmationToConsole()
287287
{
288288
// Arrange
289289
var slnxPath = Path.GetFullPath("test.slnx");
290-
var console = new TestConsole();
291-
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", console: console);
290+
var console = new FakeConsole();
291+
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", console);
292292
var options = new ValidatorRunnerOptions(slnxPath, SonarqubeReportPath: null,
293293
ContinueOnError: false, RequiredFilesPattern: null, WorkingDirectory: ".",
294294
SarifReportPath: "report.sarif");
@@ -297,7 +297,7 @@ public async Task RunAsync_SarifReportPath_WritesConfirmationToConsole()
297297
await runner.RunAsync(options, CancellationToken.None);
298298

299299
// Assert
300-
console.Out.ToString().Should().Contain("SARIF report written to: report.sarif");
300+
console.Output.Should().ContainMatch("*SARIF report written to: report.sarif*");
301301
}
302302

303303
#endregion

0 commit comments

Comments
 (0)