Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/SLNX-validator/IConsole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace JulianVerdurmen.SlnxValidator;

internal interface IConsole
{
Task WriteAsync(string value);
Task WriteErrorAsync(string value);
}
1 change: 1 addition & 0 deletions src/SLNX-validator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public static async Task<int> Main(string[] args)
var services = new ServiceCollection()
.AddSlnxValidator()
.AddSingleton<SlnxCollector>()
.AddSingleton<IConsole>(new SystemConsole())
.AddSingleton<ValidatorRunner>()
.BuildServiceProvider();

Expand Down
7 changes: 7 additions & 0 deletions src/SLNX-validator/SystemConsole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace JulianVerdurmen.SlnxValidator;

internal sealed class SystemConsole : IConsole
{
Comment thread
304NotModified marked this conversation as resolved.
public Task WriteAsync(string value) => Console.Out.WriteAsync(value);
public Task WriteErrorAsync(string value) => Console.Error.WriteAsync(value);
}
8 changes: 4 additions & 4 deletions src/SLNX-validator/ValidatorRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace JulianVerdurmen.SlnxValidator;

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

if (results.Count == 0)
{
await Console.Error.WriteLineAsync($"No .slnx files found for input: {options.Input}");
await console.WriteErrorAsync($"No .slnx files found for input: {options.Input}{Environment.NewLine}");
return options.ContinueOnError ? 0 : 1;
}

Expand All @@ -34,14 +34,14 @@ public async Task<int> RunAsync(ValidatorRunnerOptions options, CancellationToke
{
await sonarReporter.WriteReportAsync(reportResults, options.SonarqubeReportPath);
var size = fileSystem.GetFileSize(options.SonarqubeReportPath);
Console.WriteLine($"SonarQube report written to: {options.SonarqubeReportPath} ({size} bytes)");
await console.WriteAsync($"SonarQube report written to: {options.SonarqubeReportPath} ({size} bytes){Environment.NewLine}");
Comment thread
304NotModified marked this conversation as resolved.
Outdated
}

if (options.SarifReportPath is not null)
{
await sarifReporter.WriteReportAsync(reportResults, options.SarifReportPath);
var size = fileSystem.GetFileSize(options.SarifReportPath);
Console.WriteLine($"SARIF report written to: {options.SarifReportPath} ({size} bytes)");
await console.WriteAsync($"SARIF report written to: {options.SarifReportPath} ({size} bytes){Environment.NewLine}");
}

var hasErrors = results.Any(r => r.Errors.Any(e => options.SeverityOverrides.IsFailingError(e.Code)));
Expand Down
10 changes: 10 additions & 0 deletions tests/SLNX-validator.Tests/FakeConsole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace JulianVerdurmen.SlnxValidator.Tests;

internal sealed class FakeConsole : IConsole
{
public List<string> Output { get; } = [];
Comment thread
304NotModified marked this conversation as resolved.
Outdated
public List<string> ErrorOutput { get; } = [];

public Task WriteAsync(string value) { Output.Add(value); return Task.CompletedTask; }
public Task WriteErrorAsync(string value) { ErrorOutput.Add(value); return Task.CompletedTask; }
}
4 changes: 2 additions & 2 deletions tests/SLNX-validator.Tests/MockFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ public Stream OpenRead(string path) =>
new MemoryStream(Encoding.UTF8.GetBytes(_fileContents.GetValueOrDefault(path, "")));
public Task<string> ReadAllTextAsync(string path, CancellationToken cancellationToken = default) =>
Task.FromResult(_fileContents.GetValueOrDefault(path, ""));
public long GetFileSize(string path) =>
CreatedFiles.TryGetValue(path, out var ms) ? ms.Length : 0;
public long GetFileSize(string path) => 0;
}

85 changes: 69 additions & 16 deletions tests/SLNX-validator.Tests/ValidatorRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@ namespace JulianVerdurmen.SlnxValidator.Tests;

public class ValidatorRunnerTests
{
private static ValidatorRunner CreateRunner(IFileSystem fileSystem, IRequiredFilesChecker? checker = null)
private static ValidatorRunner CreateRunner(IFileSystem fileSystem, IConsole console, IRequiredFilesChecker? checker = null)
{
checker ??= Substitute.For<IRequiredFilesChecker>();
var resolver = Substitute.For<ISlnxFileResolver>();
var collector = new SlnxCollector(fileSystem, resolver, Substitute.For<ISlnxValidator>(), checker);
var sonarReporter = new SonarReporter(fileSystem);
var sarifReporter = new SarifReporter(fileSystem);
return new ValidatorRunner(collector, sonarReporter, sarifReporter, checker, fileSystem);
return new ValidatorRunner(collector, sonarReporter, sarifReporter, checker, fileSystem, console);
}

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

private static ValidatorRunner CreateRunnerWithSlnx(
string slnxPath, string slnxContent, IRequiredFilesChecker? checker = null)
string slnxPath, string slnxContent, IConsole console, IRequiredFilesChecker? checker = null)
{
checker ??= Substitute.For<IRequiredFilesChecker>();
var fileSystem = new MockFileSystem(new Dictionary<string, string>
Expand All @@ -39,7 +39,7 @@ private static ValidatorRunner CreateRunnerWithSlnx(
var collector = new SlnxCollector(fileSystem, resolver, validator, checker);
var sonarReporter = new SonarReporter(fileSystem);
var sarifReporter = new SarifReporter(fileSystem);
return new ValidatorRunner(collector, sonarReporter, sarifReporter, checker, fileSystem);
return new ValidatorRunner(collector, sonarReporter, sarifReporter, checker, fileSystem, console);
}

#region RunAsync – file resolution
Expand All @@ -48,7 +48,7 @@ private static ValidatorRunner CreateRunnerWithSlnx(
public async Task RunAsync_FileNotFound_ContinueOnErrorFalse_ReturnsOne()
{
// Arrange
Comment thread
304NotModified marked this conversation as resolved.
var runner = CreateRunner(new MockFileSystem());
var runner = CreateRunner(new MockFileSystem(), new FakeConsole());

// Act
var exitCode = await runner.RunAsync(Options("nonexistent.slnx"), CancellationToken.None);
Expand All @@ -61,7 +61,7 @@ public async Task RunAsync_FileNotFound_ContinueOnErrorFalse_ReturnsOne()
public async Task RunAsync_FileNotFound_ContinueOnErrorTrue_ReturnsZero()
{
// Arrange
var runner = CreateRunner(new MockFileSystem());
var runner = CreateRunner(new MockFileSystem(), new FakeConsole());

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

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

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

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

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

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

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

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

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

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

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

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

// Act
Expand All @@ -248,5 +248,58 @@ public async Task RunAsync_IgnoreAllCodesMajorSpecificCode_SpecificCodeCausesExi
}

#endregion

#region RunAsync – console output

[Test]
public async Task RunAsync_NoFilesFound_WritesErrorToConsole()
{
// Arrange
var console = new FakeConsole();
var runner = CreateRunner(new MockFileSystem(), console);

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

// Assert
console.ErrorOutput.Should().ContainMatch("*No .slnx files found for input: nonexistent.slnx*");
}

[Test]
public async Task RunAsync_SonarqubeReportPath_WritesConfirmationToConsole()
{
// Arrange
var slnxPath = Path.GetFullPath("test.slnx");
var console = new FakeConsole();
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", console);
var options = new ValidatorRunnerOptions(slnxPath, SonarqubeReportPath: "report.xml",
ContinueOnError: false, RequiredFilesPattern: null, WorkingDirectory: ".");

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

// Assert
console.Output.Should().ContainMatch("*SonarQube report written to: report.xml*");
}

[Test]
public async Task RunAsync_SarifReportPath_WritesConfirmationToConsole()
{
// Arrange
var slnxPath = Path.GetFullPath("test.slnx");
var console = new FakeConsole();
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", console);
var options = new ValidatorRunnerOptions(slnxPath, SonarqubeReportPath: null,
ContinueOnError: false, RequiredFilesPattern: null, WorkingDirectory: ".",
SarifReportPath: "report.sarif");

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

// Assert
console.Output.Should().ContainMatch("*SARIF report written to: report.sarif*");
}

#endregion
}

Loading