Skip to content

Commit 64e144c

Browse files
Various refactorings (#300)
* Use file-scoped namespaces * Use sealed class * Fix warnings * Use null pattern matching * Use namespace * Various refactorings
1 parent 2d113aa commit 64e144c

File tree

9 files changed

+333
-355
lines changed

9 files changed

+333
-355
lines changed

src/Exercism.TestRunner.CSharp/Options.cs

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,33 @@
22

33
using Humanizer;
44

5-
namespace Exercism.TestRunner.CSharp
5+
namespace Exercism.TestRunner.CSharp;
6+
7+
internal sealed class Options
68
{
7-
internal class Options
8-
{
9-
[Value(0, Required = true, HelpText = "The solution's exercise")]
10-
public string Slug { get; }
9+
[Value(0, Required = true, HelpText = "The solution's exercise")]
10+
public string Slug { get; }
1111

12-
[Value(1, Required = true, HelpText = "The directory containing the solution")]
13-
public string InputDirectory { get; }
12+
[Value(1, Required = true, HelpText = "The directory containing the solution")]
13+
public string InputDirectory { get; }
1414

15-
[Value(2, Required = true, HelpText = "The directory to which the results will be written")]
16-
public string OutputDirectory { get; }
15+
[Value(2, Required = true, HelpText = "The directory to which the results will be written")]
16+
public string OutputDirectory { get; }
1717

18-
public Options(string slug, string inputDirectory, string outputDirectory) =>
19-
(Slug, InputDirectory, OutputDirectory) = (slug, inputDirectory, outputDirectory);
18+
public Options(string slug, string inputDirectory, string outputDirectory) =>
19+
(Slug, InputDirectory, OutputDirectory) = (slug, inputDirectory, outputDirectory);
2020

21-
public string TestsFilePath => Path.Combine(InputDirectory, $"{Exercise}Tests.cs");
21+
public string TestsFilePath => Path.Combine(InputDirectory, $"{Exercise}Tests.cs");
2222

23-
public string ProjectFilePath => Path.Combine(InputDirectory, $"{Exercise}.csproj");
23+
public string ProjectFilePath => Path.Combine(InputDirectory, $"{Exercise}.csproj");
2424

25-
public string AssemblyInfoFilePath => Path.Combine(InputDirectory, "AssemblyInfo.cs");
25+
public string AssemblyInfoFilePath => Path.Combine(InputDirectory, "AssemblyInfo.cs");
2626

27-
public string BuildLogFilePath => Path.Combine(InputDirectory, "msbuild.log");
27+
public string BuildLogFilePath => Path.Combine(InputDirectory, "msbuild.log");
2828

29-
public string TestResultsFilePath => Path.Combine(InputDirectory, "TestResults", "tests.trx");
29+
public string TestResultsFilePath => Path.Combine(InputDirectory, "TestResults", "tests.trx");
3030

31-
public string ResultsJsonFilePath => Path.GetFullPath(Path.Combine(OutputDirectory, "results.json"));
31+
public string ResultsJsonFilePath => Path.GetFullPath(Path.Combine(OutputDirectory, "results.json"));
3232

33-
private string Exercise => Slug.Dehumanize().Pascalize();
34-
}
33+
private string Exercise => Slug.Dehumanize().Pascalize();
3534
}
Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
using CommandLine;
22

3-
namespace Exercism.TestRunner.CSharp
3+
namespace Exercism.TestRunner.CSharp;
4+
5+
public static class Program
46
{
5-
public static class Program
6-
{
7-
public static void Main(string[] args) =>
8-
Parser.Default
9-
.ParseArguments<Options>(args)
10-
.WithParsed(CreateTestResults);
7+
public static void Main(string[] args) =>
8+
Parser.Default
9+
.ParseArguments<Options>(args)
10+
.WithParsed(CreateTestResults);
1111

12-
private static void CreateTestResults(Options options)
13-
{
14-
Console.WriteLine($"[{DateTimeOffset.UtcNow:u}] Running test runner for '{options.Slug}' solution...");
12+
private static void CreateTestResults(Options options)
13+
{
14+
Console.WriteLine($"[{DateTimeOffset.UtcNow:u}] Running test runner for '{options.Slug}' solution...");
1515

16-
var testSuite = TestSuite.FromOptions(options);
17-
var testRun = testSuite.Run();
18-
testRun.WriteToFile(options.ResultsJsonFilePath);
16+
var testSuite = TestSuite.FromOptions(options);
17+
var testRun = testSuite.Run();
18+
testRun.WriteToFile(options.ResultsJsonFilePath);
1919

20-
Console.WriteLine($"[{DateTimeOffset.UtcNow:u}] Ran test runner for '{options.Slug}' solution");
21-
}
20+
Console.WriteLine($"[{DateTimeOffset.UtcNow:u}] Ran test runner for '{options.Slug}' solution");
2221
}
2322
}

src/Exercism.TestRunner.CSharp/StringExtensions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
public static class StringExtensions
1+
namespace Exercism.TestRunner.CSharp;
2+
3+
public static class StringExtensions
24
{
35
public static string UseUnixNewlines(this string str) =>
46
str.Replace("\r\n", "\n");

src/Exercism.TestRunner.CSharp/TestResultParser.cs

Lines changed: 124 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -5,149 +5,146 @@
55
using Microsoft.CodeAnalysis;
66
using Microsoft.CodeAnalysis.CSharp;
77
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.Win32.SafeHandles;
89

9-
namespace Exercism.TestRunner.CSharp
10+
namespace Exercism.TestRunner.CSharp;
11+
12+
internal static class TestResultParser
1013
{
11-
internal static class TestResultParser
14+
internal static TestResult[] FromFile(string logFilePath, SyntaxTree testsSyntaxTree)
1215
{
13-
internal static TestResult[] FromFile(string logFilePath, SyntaxTree testsSyntaxTree)
14-
{
15-
using var fileStream = File.OpenRead(logFilePath);
16-
var result = (XmlTestRun)new XmlSerializer(typeof(XmlTestRun)).Deserialize(fileStream);
17-
18-
if (result.Results == null)
19-
return Array.Empty<TestResult>();
20-
21-
return result.ToTestResults(testsSyntaxTree);
22-
}
16+
using var fileStream = File.OpenRead(logFilePath);
17+
var result = new XmlSerializer(typeof(XmlTestRun)).Deserialize(fileStream) as XmlTestRun;
2318

24-
private static TestResult[] ToTestResults(this XmlTestRun result, SyntaxTree testsSyntaxTree)
25-
{
26-
var methodDeclarations =
27-
testsSyntaxTree
28-
.GetRoot()
29-
.DescendantNodes()
30-
.OfType<MethodDeclarationSyntax>()
31-
.ToArray();
32-
33-
var testResults =
34-
from unitTestResult in result.Results.UnitTestResult
35-
let testMethodDeclaration = unitTestResult.TestMethod(methodDeclarations)
36-
orderby testMethodDeclaration.GetLocation().GetLineSpan().StartLinePosition.Line
37-
select ToTestResult(unitTestResult, testMethodDeclaration);
38-
39-
return testResults.ToArray();
40-
}
41-
42-
private static TestResult ToTestResult(XmlUnitTestResult xmlUnitTestResult, MethodDeclarationSyntax testMethodDeclaration) =>
43-
new()
44-
{
45-
Name = xmlUnitTestResult.Name(),
46-
Status = xmlUnitTestResult.Status(),
47-
Message = xmlUnitTestResult.Message(),
48-
Output = xmlUnitTestResult.Output(),
49-
TaskId = testMethodDeclaration.TaskId(),
50-
TestCode = testMethodDeclaration.TestCode()
51-
};
52-
53-
private static MethodDeclarationSyntax TestMethod(this XmlUnitTestResult xmlUnitTestResult, IEnumerable<MethodDeclarationSyntax> methodDeclarations)
54-
{
55-
var classAndMethodName = xmlUnitTestResult.TestName.Split(".");
56-
var className = classAndMethodName[0];
57-
var methodName = classAndMethodName[1].Split('(')[0];
58-
59-
return methodDeclarations.Single(method =>
60-
method.Identifier.Text == methodName &&
61-
method.Parent is ClassDeclarationSyntax classDeclaration &&
62-
classDeclaration.Identifier.Text == className);
63-
}
64-
65-
private static string Name(this XmlUnitTestResult xmlUnitTestResult) =>
66-
xmlUnitTestResult.TestName
67-
.Substring(xmlUnitTestResult.TestName.LastIndexOf(".", StringComparison.Ordinal) + 1)
68-
.Humanize();
69-
70-
private static TestStatus Status(this XmlUnitTestResult xmlUnitTestResult) =>
71-
xmlUnitTestResult.Outcome switch
72-
{
73-
"Passed" => TestStatus.Pass,
74-
"Failed" => TestStatus.Fail,
75-
_ => TestStatus.Error
76-
};
77-
78-
private static string Message(this XmlUnitTestResult xmlUnitTestResult) =>
79-
xmlUnitTestResult.Output?.ErrorInfo?.Message?.UseUnixNewlines()?.Trim();
80-
81-
private static string Output(this XmlUnitTestResult xmlUnitTestResult) =>
82-
xmlUnitTestResult.Output?.StdOut?.UseUnixNewlines()?.Trim();
83-
84-
private static string TestCode(this MethodDeclarationSyntax testMethod)
85-
{
86-
if (testMethod.Body != null)
87-
return SyntaxFactory.List(testMethod.Body.Statements.Select(statement => statement.WithoutLeadingTrivia())).ToString();
88-
89-
return testMethod.ExpressionBody!
90-
.Expression
91-
.WithoutLeadingTrivia()
92-
.ToString();
93-
}
94-
95-
private static int? TaskId(this MethodDeclarationSyntax testMethod) =>
96-
testMethod.AttributeLists
97-
.SelectMany(attributeList => attributeList.Attributes)
98-
.Where(attribute =>
99-
attribute.Name.ToString() == "Task" &&
100-
attribute.ArgumentList != null &&
101-
attribute.ArgumentList.Arguments.Count == 1 &&
102-
attribute.ArgumentList.Arguments[0].Expression.IsKind(SyntaxKind.NumericLiteralExpression))
103-
.Select(attribute => (LiteralExpressionSyntax)attribute.ArgumentList.Arguments[0].Expression)
104-
.Select(taskNumberExpression => (int?)taskNumberExpression.Token.Value!)
105-
.FirstOrDefault();
19+
return result?.Results is null ? [] : result.ToTestResults(testsSyntaxTree);
10620
}
10721

108-
[XmlRoot(ElementName = "Output", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
109-
public class XmlOutput
22+
private static TestResult[] ToTestResults(this XmlTestRun result, SyntaxTree testsSyntaxTree)
11023
{
111-
[XmlElement(ElementName = "StdOut", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
112-
public string StdOut { get; set; }
24+
var methodDeclarations =
25+
testsSyntaxTree
26+
.GetRoot()
27+
.DescendantNodes()
28+
.OfType<MethodDeclarationSyntax>()
29+
.ToArray();
30+
31+
var testResults =
32+
from unitTestResult in result.Results.UnitTestResult
33+
let testMethodDeclaration = unitTestResult.TestMethod(methodDeclarations)
34+
orderby testMethodDeclaration.GetLocation().GetLineSpan().StartLinePosition.Line
35+
select ToTestResult(unitTestResult, testMethodDeclaration);
11336

114-
[XmlElement(ElementName = "ErrorInfo",
115-
Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
116-
public XmlErrorInfo ErrorInfo { get; set; }
37+
return testResults.ToArray();
11738
}
11839

119-
[XmlRoot(ElementName = "UnitTestResult", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
120-
public class XmlUnitTestResult
40+
private static TestResult ToTestResult(XmlUnitTestResult xmlUnitTestResult, MethodDeclarationSyntax testMethodDeclaration) =>
41+
new()
42+
{
43+
Name = xmlUnitTestResult.Name(),
44+
Status = xmlUnitTestResult.Status(),
45+
Message = xmlUnitTestResult.Message(),
46+
Output = xmlUnitTestResult.Output(),
47+
TaskId = testMethodDeclaration.TaskId(),
48+
TestCode = testMethodDeclaration.TestCode()
49+
};
50+
51+
private static MethodDeclarationSyntax TestMethod(this XmlUnitTestResult xmlUnitTestResult, IEnumerable<MethodDeclarationSyntax> methodDeclarations)
12152
{
122-
[XmlElement(ElementName = "Output", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
123-
public XmlOutput Output { get; set; }
53+
var classAndMethodName = xmlUnitTestResult.TestName.Split(".");
54+
var className = classAndMethodName[0];
55+
var methodName = classAndMethodName[1].Split('(')[0];
56+
57+
return methodDeclarations.Single(method =>
58+
method.Identifier.Text == methodName &&
59+
method.Parent is ClassDeclarationSyntax classDeclaration &&
60+
classDeclaration.Identifier.Text == className);
61+
}
12462

125-
[XmlAttribute(AttributeName = "testName")]
126-
public string TestName { get; set; }
63+
private static string Name(this XmlUnitTestResult xmlUnitTestResult) =>
64+
xmlUnitTestResult.TestName
65+
.Substring(xmlUnitTestResult.TestName.LastIndexOf(".", StringComparison.Ordinal) + 1)
66+
.Humanize();
12767

128-
[XmlAttribute(AttributeName = "outcome")]
129-
public string Outcome { get; set; }
130-
}
68+
private static TestStatus Status(this XmlUnitTestResult xmlUnitTestResult) =>
69+
xmlUnitTestResult.Outcome switch
70+
{
71+
"Passed" => TestStatus.Pass,
72+
"Failed" => TestStatus.Fail,
73+
_ => TestStatus.Error
74+
};
13175

132-
[XmlRoot(ElementName = "ErrorInfo", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
133-
public class XmlErrorInfo
134-
{
135-
[XmlElement(ElementName = "Message", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
136-
public string Message { get; set; }
137-
}
76+
private static string? Message(this XmlUnitTestResult xmlUnitTestResult) =>
77+
xmlUnitTestResult.Output?.ErrorInfo?.Message?.UseUnixNewlines()?.Trim();
13878

139-
[XmlRoot(ElementName = "Results", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
140-
public class XmlResults
141-
{
142-
[XmlElement(ElementName = "UnitTestResult",
143-
Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
144-
public List<XmlUnitTestResult> UnitTestResult { get; set; }
145-
}
79+
private static string? Output(this XmlUnitTestResult xmlUnitTestResult) =>
80+
xmlUnitTestResult.Output?.StdOut?.UseUnixNewlines()?.Trim();
14681

147-
[XmlRoot(ElementName = "TestRun", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
148-
public class XmlTestRun
82+
private static string TestCode(this MethodDeclarationSyntax testMethod)
14983
{
150-
[XmlElement(ElementName = "Results", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
151-
public XmlResults Results { get; set; }
84+
if (testMethod.Body is not null)
85+
return SyntaxFactory.List(testMethod.Body.Statements.Select(statement => statement.WithoutLeadingTrivia())).ToString();
86+
87+
return testMethod.ExpressionBody!
88+
.Expression
89+
.WithoutLeadingTrivia()
90+
.ToString();
15291
}
92+
93+
private static int? TaskId(this MethodDeclarationSyntax testMethod) =>
94+
testMethod.AttributeLists
95+
.SelectMany(attributeList => attributeList.Attributes)
96+
.Where(attribute =>
97+
attribute.Name.ToString() == "Task" &&
98+
attribute.ArgumentList != null &&
99+
attribute.ArgumentList.Arguments.Count == 1 &&
100+
attribute.ArgumentList.Arguments[0].Expression.IsKind(SyntaxKind.NumericLiteralExpression))
101+
.Select(attribute => (LiteralExpressionSyntax)attribute.ArgumentList!.Arguments[0].Expression)
102+
.Select(taskNumberExpression => (int?)taskNumberExpression.Token.Value!)
103+
.FirstOrDefault();
104+
}
105+
106+
[XmlRoot(ElementName = "Output", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
107+
public sealed class XmlOutput
108+
{
109+
[XmlElement(ElementName = "StdOut", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
110+
public string? StdOut { get; set; }
111+
112+
[XmlElement(ElementName = "ErrorInfo",
113+
Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
114+
public XmlErrorInfo? ErrorInfo { get; set; }
115+
}
116+
117+
[XmlRoot(ElementName = "UnitTestResult", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
118+
public sealed class XmlUnitTestResult
119+
{
120+
[XmlElement(ElementName = "Output", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
121+
public XmlOutput? Output { get; set; }
122+
123+
[XmlAttribute(AttributeName = "testName")]
124+
public required string TestName { get; set; }
125+
126+
[XmlAttribute(AttributeName = "outcome")]
127+
public required string Outcome { get; set; }
128+
}
129+
130+
[XmlRoot(ElementName = "ErrorInfo", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
131+
public sealed class XmlErrorInfo
132+
{
133+
[XmlElement(ElementName = "Message", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
134+
public string? Message { get; set; }
135+
}
136+
137+
[XmlRoot(ElementName = "Results", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
138+
public sealed class XmlResults
139+
{
140+
[XmlElement(ElementName = "UnitTestResult",
141+
Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
142+
public List<XmlUnitTestResult>? UnitTestResult { get; set; }
143+
}
144+
145+
[XmlRoot(ElementName = "TestRun", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
146+
public sealed class XmlTestRun
147+
{
148+
[XmlElement(ElementName = "Results", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
149+
public required XmlResults Results { get; set; }
153150
}

0 commit comments

Comments
 (0)