Skip to content

Commit dc617d5

Browse files
better DI and tests
1 parent 77a9357 commit dc617d5

10 files changed

Lines changed: 262 additions & 10 deletions

File tree

src/SLNX-validator.Core/FileSystem/IFileSystem.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ public interface IFileSystem
55
bool FileExists(string path);
66
bool DirectoryExists(string path);
77
IEnumerable<string> GetFiles(string directory, string searchPattern);
8+
void CreateDirectory(string path);
9+
Stream CreateFile(string path);
810
}

src/SLNX-validator.Core/FileSystem/RealFileSystem.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ public sealed class RealFileSystem : IFileSystem
66
public bool DirectoryExists(string path) => Directory.Exists(path);
77
public IEnumerable<string> GetFiles(string directory, string searchPattern) =>
88
Directory.GetFiles(directory, searchPattern);
9+
public void CreateDirectory(string path) => Directory.CreateDirectory(path);
10+
public Stream CreateFile(string path) => File.Create(path);
911
}

src/SLNX-validator/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public static async Task<int> Main(string[] args)
3333
var services = new ServiceCollection()
3434
.AddSlnxValidator()
3535
.AddSingleton<ValidationCollector>()
36+
.AddSingleton<SonarReporter>()
3637
.AddSingleton<ValidatorRunner>()
3738
.BuildServiceProvider();
3839

src/SLNX-validator/SonarReporter.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
using System.Text.Encodings.Web;
22
using System.Text.Json;
33
using System.Text.Json.Serialization;
4+
using JulianVerdurmen.SlnxValidator.Core.FileSystem;
45
using JulianVerdurmen.SlnxValidator.Core.ValidationResults;
56

67
namespace JulianVerdurmen.SlnxValidator;
78

8-
internal static class SonarReporter
9+
internal sealed class SonarReporter(IFileSystem fileSystem)
910
{
1011
private static readonly JsonSerializerOptions JsonOptions = new()
1112
{
@@ -16,17 +17,17 @@ internal static class SonarReporter
1617
Converters = { new JsonStringEnumConverter() }
1718
};
1819

19-
public static async Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, string outputPath)
20+
public async Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, string outputPath)
2021
{
2122
var directory = Path.GetDirectoryName(outputPath);
2223
if (!string.IsNullOrEmpty(directory))
23-
Directory.CreateDirectory(directory);
24+
fileSystem.CreateDirectory(directory);
2425

25-
await using var stream = File.Create(outputPath);
26+
await using var stream = fileSystem.CreateFile(outputPath);
2627
await WriteReportAsync(results, stream);
2728
}
2829

29-
public static async Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, Stream outputStream)
30+
public async Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, Stream outputStream)
3031
{
3132
var usedCodes = results
3233
.SelectMany(r => r.Errors)

src/SLNX-validator/ValidatorRunner.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace JulianVerdurmen.SlnxValidator;
55

6-
internal sealed class ValidatorRunner(SlnxFileResolver resolver, ValidationCollector collector)
6+
internal sealed class ValidatorRunner(SlnxFileResolver resolver, ValidationCollector collector, SonarReporter sonarReporter)
77
{
88
public async Task<int> RunAsync(string input, string? sonarqubeReportPath, bool continueOnError, CancellationToken cancellationToken)
99
{
@@ -20,7 +20,7 @@ public async Task<int> RunAsync(string input, string? sonarqubeReportPath, bool
2020
await ValidationReporter.Report(results);
2121

2222
if (sonarqubeReportPath is not null)
23-
await SonarReporter.WriteReportAsync(results, sonarqubeReportPath);
23+
await sonarReporter.WriteReportAsync(results, sonarqubeReportPath);
2424

2525
var hasErrors = results.Any(r => r.HasErrors);
2626
return !continueOnError && hasErrors ? 1 : 0;

tests/SLNX-validator.Core.Tests/MockFileSystem.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ internal sealed class MockFileSystem(params string[] existingPaths) : IFileSyste
99
public bool FileExists(string path) => _existingPaths.Contains(path);
1010
public bool DirectoryExists(string path) => false;
1111
public IEnumerable<string> GetFiles(string directory, string searchPattern) => [];
12+
public void CreateDirectory(string path) { }
13+
public Stream CreateFile(string path) => new MemoryStream();
1214
}

tests/SLNX-validator.Tests/MockFileSystem.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@ internal sealed class MockFileSystem(params string[] existingPaths) : IFileSyste
66
{
77
private readonly HashSet<string> _existingPaths = new(existingPaths, StringComparer.OrdinalIgnoreCase);
88

9+
public List<string> CreatedDirectories { get; } = [];
10+
public Dictionary<string, MemoryStream> CreatedFiles { get; } = [];
11+
912
public bool FileExists(string path) => _existingPaths.Contains(path);
1013
public bool DirectoryExists(string path) => false;
1114
public IEnumerable<string> GetFiles(string directory, string searchPattern) => [];
15+
public void CreateDirectory(string path) => CreatedDirectories.Add(path);
16+
public Stream CreateFile(string path)
17+
{
18+
var ms = new MemoryStream();
19+
CreatedFiles[path] = ms;
20+
return ms;
21+
}
1222
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
{
2+
"rules": [
3+
{
4+
"id": "SLNX001",
5+
"name": "Input file not found",
6+
"description": "The specified .slnx file does not exist.",
7+
"engineId": "slnx-validator",
8+
"cleanCodeAttribute": "COMPLETE",
9+
"type": "BUG",
10+
"severity": "MAJOR",
11+
"impacts": [
12+
{
13+
"softwareQuality": "MAINTAINABILITY",
14+
"severity": "HIGH"
15+
}
16+
]
17+
},
18+
{
19+
"id": "SLNX002",
20+
"name": "Invalid file extension",
21+
"description": "The input file does not have a .slnx extension.",
22+
"engineId": "slnx-validator",
23+
"cleanCodeAttribute": "CONVENTIONAL",
24+
"type": "BUG",
25+
"severity": "MINOR",
26+
"impacts": [
27+
{
28+
"softwareQuality": "MAINTAINABILITY",
29+
"severity": "HIGH"
30+
}
31+
]
32+
},
33+
{
34+
"id": "SLNX003",
35+
"name": "File is not a text file",
36+
"description": "The file is binary and cannot be parsed as XML.",
37+
"engineId": "slnx-validator",
38+
"cleanCodeAttribute": "COMPLETE",
39+
"type": "BUG",
40+
"severity": "MAJOR",
41+
"impacts": [
42+
{
43+
"softwareQuality": "MAINTAINABILITY",
44+
"severity": "HIGH"
45+
}
46+
]
47+
},
48+
{
49+
"id": "SLNX010",
50+
"name": "Invalid XML",
51+
"description": "The .slnx file is not valid XML.",
52+
"engineId": "slnx-validator",
53+
"cleanCodeAttribute": "COMPLETE",
54+
"type": "BUG",
55+
"severity": "MAJOR",
56+
"impacts": [
57+
{
58+
"softwareQuality": "MAINTAINABILITY",
59+
"severity": "HIGH"
60+
}
61+
]
62+
},
63+
{
64+
"id": "SLNX011",
65+
"name": "Referenced file not found",
66+
"description": "A file referenced in a <File Path=\"...\"> element does not exist on disk.",
67+
"engineId": "slnx-validator",
68+
"cleanCodeAttribute": "COMPLETE",
69+
"type": "BUG",
70+
"severity": "MAJOR",
71+
"impacts": [
72+
{
73+
"softwareQuality": "MAINTAINABILITY",
74+
"severity": "HIGH"
75+
}
76+
]
77+
},
78+
{
79+
"id": "SLNX012",
80+
"name": "Invalid wildcard usage",
81+
"description": "A <File Path=\"...\"> element contains a wildcard pattern, which is not supported.",
82+
"engineId": "slnx-validator",
83+
"cleanCodeAttribute": "COMPLETE",
84+
"type": "BUG",
85+
"severity": "MINOR",
86+
"impacts": [
87+
{
88+
"softwareQuality": "MAINTAINABILITY",
89+
"severity": "HIGH"
90+
}
91+
]
92+
},
93+
{
94+
"id": "SLNX013",
95+
"name": "XSD schema violation",
96+
"description": "The XML structure violates the .slnx schema.",
97+
"engineId": "slnx-validator",
98+
"cleanCodeAttribute": "COMPLETE",
99+
"type": "BUG",
100+
"severity": "MAJOR",
101+
"impacts": [
102+
{
103+
"softwareQuality": "MAINTAINABILITY",
104+
"severity": "MEDIUM"
105+
}
106+
]
107+
}
108+
],
109+
"issues": [
110+
{
111+
"ruleId": "SLNX001",
112+
"primaryLocation": {
113+
"message": "Sample message for FileNotFound",
114+
"filePath": "Solution.slnx",
115+
"textRange": {
116+
"startLine": 1
117+
}
118+
}
119+
},
120+
{
121+
"ruleId": "SLNX002",
122+
"primaryLocation": {
123+
"message": "Sample message for InvalidExtension",
124+
"filePath": "Solution.slnx",
125+
"textRange": {
126+
"startLine": 2
127+
}
128+
}
129+
},
130+
{
131+
"ruleId": "SLNX003",
132+
"primaryLocation": {
133+
"message": "Sample message for NotATextFile",
134+
"filePath": "Solution.slnx",
135+
"textRange": {
136+
"startLine": 3
137+
}
138+
}
139+
},
140+
{
141+
"ruleId": "SLNX010",
142+
"primaryLocation": {
143+
"message": "Sample message for InvalidXml",
144+
"filePath": "Solution.slnx",
145+
"textRange": {
146+
"startLine": 4
147+
}
148+
}
149+
},
150+
{
151+
"ruleId": "SLNX011",
152+
"primaryLocation": {
153+
"message": "Sample message for ReferencedFileNotFound",
154+
"filePath": "Solution.slnx",
155+
"textRange": {
156+
"startLine": 5
157+
}
158+
}
159+
},
160+
{
161+
"ruleId": "SLNX012",
162+
"primaryLocation": {
163+
"message": "Sample message for InvalidWildcardUsage",
164+
"filePath": "Solution.slnx",
165+
"textRange": {
166+
"startLine": 6
167+
}
168+
}
169+
},
170+
{
171+
"ruleId": "SLNX013",
172+
"primaryLocation": {
173+
"message": "Sample message for XsdViolation",
174+
"filePath": "Solution.slnx",
175+
"textRange": {
176+
"startLine": 7
177+
}
178+
}
179+
}
180+
]
181+
}

tests/SLNX-validator.Tests/SonarReporterTests.cs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ namespace JulianVerdurmen.SlnxValidator.Tests;
77

88
public class SonarReporterTests
99
{
10+
private static SonarReporter CreateReporter() => new(new MockFileSystem());
11+
1012
private static async Task<JsonDocument> WriteAndReadReportAsync(IReadOnlyList<FileValidationResult> results)
1113
{
1214
using var stream = new MemoryStream();
13-
await SonarReporter.WriteReportAsync(results, stream);
15+
await CreateReporter().WriteReportAsync(results, stream);
1416
return JsonDocument.Parse(stream.ToArray());
1517
}
1618

@@ -163,8 +165,58 @@ public async Task WriteReportAsync_MatchesSnapshot()
163165
};
164166

165167
using var stream = new MemoryStream();
166-
await SonarReporter.WriteReportAsync(results, stream);
168+
await CreateReporter().WriteReportAsync(results, stream);
169+
170+
await Verify(stream, "json");
171+
}
172+
173+
[Test]
174+
public async Task WriteReportAsync_AllErrorCodes_MatchesSnapshot()
175+
{
176+
var allCodes = Enum.GetValues<ValidationErrorCode>();
177+
var results = new List<FileValidationResult>
178+
{
179+
new()
180+
{
181+
File = "Solution.slnx",
182+
HasErrors = true,
183+
Errors = [.. allCodes.Select((code, i) => new ValidationError(code, $"Sample message for {code}", Line: i + 1))]
184+
}
185+
};
186+
187+
using var stream = new MemoryStream();
188+
await CreateReporter().WriteReportAsync(results, stream);
167189

168190
await Verify(stream, "json");
169191
}
192+
193+
[Test]
194+
public async Task WriteReportAsync_WithOutputPath_CreatesParentDirectory()
195+
{
196+
var fileSystem = new MockFileSystem();
197+
var reporter = new SonarReporter(fileSystem);
198+
var results = new List<FileValidationResult>
199+
{
200+
new() { File = "test.slnx", HasErrors = false, Errors = [] }
201+
};
202+
203+
await reporter.WriteReportAsync(results, "output/reports/sonar.json");
204+
205+
fileSystem.CreatedDirectories.Should().Equal([Path.Combine("output", "reports")]);
206+
}
207+
208+
[Test]
209+
public async Task WriteReportAsync_WithOutputPathNoSubdirectory_DoesNotCreateDirectory()
210+
{
211+
var fileSystem = new MockFileSystem();
212+
var reporter = new SonarReporter(fileSystem);
213+
var results = new List<FileValidationResult>
214+
{
215+
new() { File = "test.slnx", HasErrors = false, Errors = [] }
216+
};
217+
218+
await reporter.WriteReportAsync(results, "sonar.json");
219+
220+
fileSystem.CreatedDirectories.Should().BeEmpty();
221+
}
170222
}

tests/SLNX-validator.Tests/ValidatorRunnerTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ private static ValidatorRunner CreateRunner(IFileSystem fileSystem)
1212
var resolver = new SlnxFileResolver(fileSystem);
1313
var validator = new CoreSlnxValidator(fileSystem, new XsdValidator());
1414
var collector = new ValidationCollector(fileSystem, validator);
15-
return new ValidatorRunner(resolver, collector);
15+
var sonarReporter = new SonarReporter(fileSystem);
16+
return new ValidatorRunner(resolver, collector, sonarReporter);
1617
}
1718

1819
[Test]

0 commit comments

Comments
 (0)