Skip to content

Commit

Permalink
Use in-memory compilation (#44)
Browse files Browse the repository at this point in the history
* Working on in-memory compilation

* Almost working

* Cleanup

* Don't support trace logging

* More speedup

* Use runtime image

* Use self-contained app

* Move ready-to-run configuration into project file

* Don't remove globalization options

* Use correct Docker call from code

* Remove todo

* Refactor and fix concurrency issue

* Rename

* Update to latest formatter

* Format code
  • Loading branch information
ErikSchierboom authored May 11, 2021
1 parent 1cd34d8 commit c3ca3f5
Show file tree
Hide file tree
Showing 37 changed files with 213 additions and 262 deletions.
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-format": {
"version": "4.1.131201",
"version": "5.1.225507",
"commands": [
"dotnet-format"
]
Expand Down
13 changes: 3 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:5.0.202-alpine3.12-amd64 AS build
FROM mcr.microsoft.com/dotnet/sdk:5.0.202-alpine3.13-amd64 AS build
WORKDIR /app

# Copy csproj and restore as distinct layers
Expand All @@ -7,21 +7,14 @@ RUN dotnet restore -r linux-musl-x64

# Copy everything else and build
COPY src/Exercism.TestRunner.CSharp/ ./
RUN dotnet publish -r linux-musl-x64 -c Release -o /opt/test-runner --no-restore -p:PublishReadyToRun=true

# Pre-install packages for offline usage
RUN dotnet add package Microsoft.NET.Test.Sdk -v 16.8.3 && \
dotnet add package xunit -v 2.4.1 && \
dotnet add package xunit.runner.visualstudio -v 2.4.3 && \
dotnet add package Exercism.Tests -v 0.1.0-alpha
RUN dotnet publish -r linux-musl-x64 -c Release -o /opt/test-runner --no-restore --self-contained true

# Build runtime image
FROM mcr.microsoft.com/dotnet/sdk:5.0.202-alpine3.12-amd64 AS runtime
FROM mcr.microsoft.com/dotnet/runtime-deps:5.0.5-alpine3.13-amd64 AS runtime
WORKDIR /opt/test-runner

COPY --from=build /opt/test-runner/ .
COPY --from=build /usr/local/bin/ /usr/local/bin/
COPY --from=build /root/.nuget/packages/ /root/.nuget/packages/

COPY run.sh /opt/test-runner/bin/

Expand Down
11 changes: 7 additions & 4 deletions src/Exercism.TestRunner.CSharp/Exercism.TestRunner.CSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
<IsTestProject>false</IsTestProject>
</PropertyGroup>

<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Globalization.Invariant" Value="true" />
</ItemGroup>
<PropertyGroup>
<PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Exercism.Tests" Version="0.1.0-alpha" />
<PackageReference Include="Humanizer.Core" Version="2.8.26" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.utility" Version="2.4.1" />
</ItemGroup>

</Project>
4 changes: 0 additions & 4 deletions src/Exercism.TestRunner.CSharp/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ public Options(string slug, string inputDirectory, string outputDirectory) =>

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

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

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

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

private string Exercise => Slug.Dehumanize().Pascalize();
Expand Down
3 changes: 1 addition & 2 deletions src/Exercism.TestRunner.CSharp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ private static void CreateTestResults(Options options)
{
Console.WriteLine($"[{DateTimeOffset.UtcNow:u}] Running test runner for '{options.Slug}' solution...");

var testSuite = TestSuite.FromOptions(options);
var testRun = testSuite.Run();
var testRun = TestSuite.RunTests(options);
testRun.WriteToFile(options.ResultsJsonFilePath);

Console.WriteLine($"[{DateTimeOffset.UtcNow:u}] Ran test runner for '{options.Slug}' solution");
Expand Down
3 changes: 3 additions & 0 deletions src/Exercism.TestRunner.CSharp/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
{
public static string UseUnixNewlines(this string str) =>
str.Replace("\r\n", "\n");

public static string NullIfEmpty(this string str) =>
str == string.Empty ? null : str;
}
52 changes: 52 additions & 0 deletions src/Exercism.TestRunner.CSharp/TestCompilation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

using Exercism.Tests;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;

namespace Exercism.TestRunner.CSharp
{
internal static class TestCompilation
{
public static Compilation Compile(Options options) =>
CSharpCompilation.Create(Guid.NewGuid().ToString("N"), SyntaxTrees(options), References(), CompilationOptions());

private static IEnumerable<SyntaxTree> SyntaxTrees(Options options)
{
SyntaxTree ParseSyntaxTree(string file)
{
var source = SourceText.From(File.OpenRead(file));
var syntaxTree = CSharpSyntaxTree.ParseText(source, path: file);

// We need to rewrite the test suite to un-skip all tests and capture any console output
if (file == options.TestsFilePath)
{
return syntaxTree.Rewrite();
}

return syntaxTree;
}

return Directory.EnumerateFiles(options.InputDirectory, "*.cs", SearchOption.AllDirectories)
.Select(ParseSyntaxTree);
}

private static CSharpCompilationOptions CompilationOptions() =>
new(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release);

private static IEnumerable<PortableExecutableReference> References()
{
var trustedAssembliesPaths = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))!.Split(Path.PathSeparator);
return trustedAssembliesPaths
.Select(p => MetadataReference.CreateFromFile(p))
.Append(MetadataReference.CreateFromFile(typeof(Xunit.FactAttribute).Assembly.Location))
.Append(MetadataReference.CreateFromFile(typeof(Xunit.Assert).Assembly.Location))
.Append(MetadataReference.CreateFromFile(typeof(TaskAttribute).Assembly.Location));
}
}
}
143 changes: 43 additions & 100 deletions src/Exercism.TestRunner.CSharp/TestResultParser.cs
Original file line number Diff line number Diff line change
@@ -1,89 +1,78 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Serialization;

using Humanizer;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

using Xunit.Runners;

namespace Exercism.TestRunner.CSharp
{
internal static class TestResultParser
{
internal static TestResult[] FromFile(string logFilePath, SyntaxTree testsSyntaxTree)
public static TestResult[] FromTests(IEnumerable<TestInfo> tests, SyntaxTree testsSyntaxTree)
{
using var fileStream = File.OpenRead(logFilePath);
var result = (XmlTestRun)new XmlSerializer(typeof(XmlTestRun)).Deserialize(fileStream);

if (result.Results == null)
return Array.Empty<TestResult>();

return result.ToTestResults(testsSyntaxTree);
}

private static TestResult[] ToTestResults(this XmlTestRun result, SyntaxTree testsSyntaxTree)
{
var methodDeclarations =
var testMethods =
testsSyntaxTree
.GetRoot()
.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.ToArray();

var testResults =
from unitTestResult in result.Results.UnitTestResult
let testMethodDeclaration = unitTestResult.TestMethod(methodDeclarations)
orderby testMethodDeclaration.GetLocation().GetLineSpan().StartLinePosition.Line
select ToTestResult(unitTestResult, testMethodDeclaration);

return testResults.ToArray();
return tests
.Select(test => (test: test, testMethod: test.TestMethod(testMethods)))
.OrderBy(testAndMethod => Array.IndexOf(testMethods, testAndMethod.testMethod))
.Select(testAndMethod => FromTest(testAndMethod.test, testAndMethod.testMethod))
.ToArray();
}

private static TestResult ToTestResult(XmlUnitTestResult xmlUnitTestResult, MethodDeclarationSyntax testMethodDeclaration) =>
private static TestResult FromTest(TestInfo test, MethodDeclarationSyntax testMethod) =>
test switch
{
TestFailedInfo failedTest => FromFailedTest(failedTest, testMethod),
TestPassedInfo passedTest => FromPassedTest(passedTest, testMethod),
_ => throw new ArgumentOutOfRangeException(nameof(test))
};

private static TestResult FromFailedTest(TestFailedInfo info, MethodDeclarationSyntax testMethod) =>
new()
{
Name = xmlUnitTestResult.Name(),
Status = xmlUnitTestResult.Status(),
Message = xmlUnitTestResult.Message(),
Output = xmlUnitTestResult.Output(),
TaskId = testMethodDeclaration.TaskId(),
TestCode = testMethodDeclaration.TestCode()
Name = info.Name(),
Status = TestStatus.Fail,
Message = info.Message(),
Output = info.Output(),
TaskId = testMethod.TaskId(),
TestCode = testMethod.TestCode()
};

private static MethodDeclarationSyntax TestMethod(this XmlUnitTestResult xmlUnitTestResult, IEnumerable<MethodDeclarationSyntax> methodDeclarations)
{
var classAndMethodName = xmlUnitTestResult.TestName.Split(".");
var className = classAndMethodName[0];
var methodName = classAndMethodName[1];
private static TestResult FromPassedTest(TestPassedInfo info, MethodDeclarationSyntax testMethod) =>
new()
{
Name = info.Name(),
Status = TestStatus.Pass,
Output = info.Output(),
TaskId = testMethod.TaskId(),
TestCode = testMethod.TestCode()
};

return methodDeclarations.Single(method =>
method.Identifier.Text == methodName &&
private static MethodDeclarationSyntax TestMethod(this TestInfo testInfo, IEnumerable<MethodDeclarationSyntax> methodDeclarations) =>
methodDeclarations.Single(method =>
method.Identifier.Text == testInfo.MethodName &&
method.Parent is ClassDeclarationSyntax classDeclaration &&
classDeclaration.Identifier.Text == className);
}

private static string Name(this XmlUnitTestResult xmlUnitTestResult) =>
xmlUnitTestResult.TestName
.Substring(xmlUnitTestResult.TestName.LastIndexOf(".", StringComparison.Ordinal) + 1)
.Humanize();
classDeclaration.Identifier.Text == testInfo.TypeName);

private static TestStatus Status(this XmlUnitTestResult xmlUnitTestResult) =>
xmlUnitTestResult.Outcome switch
{
"Passed" => TestStatus.Pass,
"Failed" => TestStatus.Fail,
_ => TestStatus.Error
};
private static string Name(this TestInfo testInfo) =>
testInfo.MethodName.Humanize();

private static string Message(this XmlUnitTestResult xmlUnitTestResult) =>
xmlUnitTestResult.Output?.ErrorInfo?.Message?.UseUnixNewlines()?.Trim();
private static string Message(this TestFailedInfo testInfo) =>
testInfo.ExceptionMessage.UseUnixNewlines()?.Trim();

private static string Output(this XmlUnitTestResult xmlUnitTestResult) =>
xmlUnitTestResult.Output?.StdOut?.UseUnixNewlines()?.Trim();
private static string Output(this TestExecutedInfo testInfo) =>
testInfo.Output.UseUnixNewlines().Trim().NullIfEmpty();

private static string TestCode(this MethodDeclarationSyntax testMethod)
{
Expand All @@ -108,50 +97,4 @@ private static string TestCode(this MethodDeclarationSyntax testMethod)
.Select(taskNumberExpression => (int?)taskNumberExpression.Token.Value!)
.FirstOrDefault();
}

[XmlRoot(ElementName = "Output", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
public class XmlOutput
{
[XmlElement(ElementName = "StdOut", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
public string StdOut { get; set; }

[XmlElement(ElementName = "ErrorInfo",
Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
public XmlErrorInfo ErrorInfo { get; set; }
}

[XmlRoot(ElementName = "UnitTestResult", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
public class XmlUnitTestResult
{
[XmlElement(ElementName = "Output", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
public XmlOutput Output { get; set; }

[XmlAttribute(AttributeName = "testName")]
public string TestName { get; set; }

[XmlAttribute(AttributeName = "outcome")]
public string Outcome { get; set; }
}

[XmlRoot(ElementName = "ErrorInfo", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
public class XmlErrorInfo
{
[XmlElement(ElementName = "Message", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
public string Message { get; set; }
}

[XmlRoot(ElementName = "Results", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
public class XmlResults
{
[XmlElement(ElementName = "UnitTestResult",
Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
public List<XmlUnitTestResult> UnitTestResult { get; set; }
}

[XmlRoot(ElementName = "TestRun", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
public class XmlTestRun
{
[XmlElement(ElementName = "Results", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")]
public XmlResults Results { get; set; }
}
}
4 changes: 2 additions & 2 deletions src/Exercism.TestRunner.CSharp/TestRun.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal class TestResult

[JsonPropertyName("status")]
public TestStatus Status { get; set; }

[JsonPropertyName("task_id")]
public int? TaskId { get; set; }

Expand All @@ -34,7 +34,7 @@ internal class TestRun
{
[JsonPropertyName("version")]
public int Version { get; set; } = 3;

[JsonPropertyName("status")]
public TestStatus Status { get; set; }

Expand Down
Loading

0 comments on commit c3ca3f5

Please sign in to comment.