Skip to content

Commit

Permalink
Merge pull request #84 from jaredpar/coverage
Browse files Browse the repository at this point in the history
Coverage
  • Loading branch information
jaredpar authored Nov 29, 2023
2 parents 5ef1ead + b518c33 commit 6fd0357
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 45 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Compiler Logs
===

[![codecov](https://codecov.io/gh/jaredpar/complog/graph/badge.svg?token=MIM7Y2JZ5G)](https://codecov.io/gh/jaredpar/complog)

This is the repository for creating and consuming compiler log files. These are files created from a [MSBuild binary log](https://github.com/KirillOsenkov/MSBuildStructuredLog) that contain information necessary to recreate all of the [Compilation](https://docs.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.compilation?view=roslyn-dotnet-4.2.0) instances from that build.

The compiler log files are self contained. They must be created on the same machine where the binary log was created but after creation they can be freely copied between machines. That enables a number of scenarios:
Expand Down
26 changes: 8 additions & 18 deletions src/Basic.CompilerLog.UnitTests/CompilerLogFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

namespace Basic.CompilerLog.UnitTests;

public sealed class CompilerLogFixture : IDisposable
public sealed class CompilerLogFixture : FixtureBase, IDisposable
{
private readonly ImmutableArray<Lazy<string>> _allCompLogs;

Expand Down Expand Up @@ -62,6 +62,7 @@ public sealed class CompilerLogFixture : IDisposable
/// Add the following to xunit.runner.json to enable "diagnosticMessages": true
/// </summary>
public CompilerLogFixture(IMessageSink messageSink)
: base(messageSink)
{
StorageDirectory = Path.Combine(Path.GetTempPath(), nameof(CompilerLogFixture), Guid.NewGuid().ToString("N"));
ComplogDirectory = Path.Combine(StorageDirectory, "logs");
Expand All @@ -74,23 +75,6 @@ public CompilerLogFixture(IMessageSink messageSink)
Directory.CreateDirectory(testArtifactsDir);
}

int processCount = 0;
void RunDotnetCommand(string args, string workingDirectory)
{
(string, string)[]? extraEnvironmentVariables = null;
var start = DateTime.UtcNow;
var diagnosticBuilder = new StringBuilder();

diagnosticBuilder.AppendLine($"Running: {processCount++} {args} in {workingDirectory}");
var result = DotnetUtil.Command(args, workingDirectory, extraEnvironmentVariables);
diagnosticBuilder.AppendLine($"Succeeded: {result.Succeeded}");
diagnosticBuilder.AppendLine($"Standard Output: {result.StandardOut}");
diagnosticBuilder.AppendLine($"Standard Error: {result.StandardError}");
diagnosticBuilder.AppendLine($"Finished: {(DateTime.UtcNow - start).TotalSeconds:F2}s");
messageSink.OnMessage(new DiagnosticMessage(diagnosticBuilder.ToString()));
Assert.True(result.Succeeded);
}

var builder = ImmutableArray.CreateBuilder<Lazy<string>>();
ConsoleComplogPath = WithBuild("console.complog", void (string scratchPath) =>
{
Expand Down Expand Up @@ -177,6 +161,7 @@ partial class Util {
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="resource.txt" />
<AdditionalFiles Include="additional.txt" />
</ItemGroup>
</Project>
""", TestBase.DefaultEncoding);
Expand All @@ -193,6 +178,11 @@ class C { }
""", TestBase.DefaultEncoding);
File.WriteAllText(Path.Combine(scratchPath, "line.txt"), "this is content", TestBase.DefaultEncoding);

File.WriteAllText(Path.Combine(scratchPath, "additional.txt"), """
This is an additional file.
It just has some text in it
""", TestBase.DefaultEncoding);

// File with a space in the name to make sure we quote correctly in RSP
File.WriteAllText(Path.Combine(scratchPath, "Code With Space In The Name.cs"), """
class D { }
Expand Down
9 changes: 9 additions & 0 deletions src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ public void KeyFileCustomState()
Assert.False(File.Exists(data.CompilationOptions.CryptoKeyFile));
}

[Fact]
public void AdditionalFiles()
{
using var reader = CompilerLogReader.Create(Fixture.ConsoleComplexComplogPath.Value);
var data = reader.ReadCompilationData(0);
Assert.Single(data.AdditionalTexts);
Assert.Equal("additional.txt", Path.GetFileName(data.AdditionalTexts[0].Path));
}

[Fact]
public void AnalyzerLoadOptions()
{
Expand Down
35 changes: 35 additions & 0 deletions src/Basic.CompilerLog.UnitTests/FixtureBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

using System.Security.Cryptography;
using System.Text;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace Basic.CompilerLog.UnitTests;

public abstract class FixtureBase
{
private int _processCount;

protected IMessageSink MessageSink { get; }

protected FixtureBase(IMessageSink messageSink)
{
MessageSink = messageSink;
}

protected void RunDotnetCommand(string args, string workingDirectory)
{
var start = DateTime.UtcNow;
var diagnosticBuilder = new StringBuilder();

diagnosticBuilder.AppendLine($"Running: {_processCount++} {args} in {workingDirectory}");
var result = DotnetUtil.Command(args, workingDirectory);
diagnosticBuilder.AppendLine($"Succeeded: {result.Succeeded}");
diagnosticBuilder.AppendLine($"Standard Output: {result.StandardOut}");
diagnosticBuilder.AppendLine($"Standard Error: {result.StandardError}");
diagnosticBuilder.AppendLine($"Finished: {(DateTime.UtcNow - start).TotalSeconds:F2}s");
MessageSink.OnMessage(new DiagnosticMessage(diagnosticBuilder.ToString()));
Assert.True(result.Succeeded);
}
}
69 changes: 52 additions & 17 deletions src/Basic.CompilerLog.UnitTests/ProgramTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#if NETCOREAPP
using Basic.CompilerLog.Util;
using Microsoft.Build.Logging.StructuredLogger;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Collections.Generic;
Expand All @@ -8,19 +9,20 @@
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;

namespace Basic.CompilerLog.UnitTests;

[Collection(CompilerLogCollection.Name)]
[Collection(SolutionFixtureCollection.Name)]
public sealed class ProgramTests : TestBase
{
public CompilerLogFixture Fixture { get; }
public SolutionFixture Fixture { get; }

public ProgramTests(ITestOutputHelper testOutputHelper, CompilerLogFixture fixture)
public ProgramTests(ITestOutputHelper testOutputHelper, SolutionFixture fixture)
: base(testOutputHelper, nameof(ProgramTests))
{
Fixture = fixture;
Expand All @@ -38,15 +40,25 @@ public int RunCompLog(string args, string? currentDirectory = null)
return (int)ret!;
}

private void RunWithBoth(Action<string> action)
{
// Run with the binary log
action(Fixture.SolutionBinaryLogPath);

// Now create a compiler log
var complogPath = Path.Combine(RootDirectory, "msbuild.complog");
var diagnostics = CompilerLogUtil.ConvertBinaryLog(Fixture.SolutionBinaryLogPath, complogPath);
Assert.Empty(diagnostics);
action(complogPath);
}

[Theory]
[InlineData("", "msbuild.complog")]
[InlineData("--out custom.complog", "custom.complog")]
[InlineData("-o custom.complog", "custom.complog")]
public void Create(string extra, string fileName)
{
RunDotNet("new console");
RunDotNet("build -bl -nr:false");
Assert.Equal(0, RunCompLog($"create {extra}"));
Assert.Equal(0, RunCompLog($"create {extra} -p {Fixture.ConsoleProjectName} {Fixture.SolutionBinaryLogPath}"));
var complogPath = Path.Combine(RootDirectory, fileName);
Assert.True(File.Exists(complogPath));
}
Expand All @@ -69,7 +81,7 @@ public void CreateNoopBuild()
{
RunDotNet("new console --name console -o .");
RunDotNet("build");
Assert.Equal(0, RunCompLog($"create console.csproj -o msbuild.complog -- -t:Build"));
Assert.Equal(1, RunCompLog($"create console.csproj -o msbuild.complog -- -t:Build"));
var complogPath = Path.Combine(RootDirectory, "msbuild.complog");
using var reader = CompilerLogReader.Create(complogPath, BasicAnalyzerHostOptions.None);
Assert.Empty(reader.ReadAllCompilerCalls());
Expand All @@ -90,6 +102,17 @@ public void CreateSolution(string target)
Assert.Single(reader.ReadAllCompilerCalls());
}

/// <summary>
/// When the resulting compiler log is empty an error should be returned cause clearly
/// there was a mistake somewhere on the command line.
/// </summary>
[Fact]
public void CreateEmpty()
{
var result = RunCompLog($"create -p does-not-exist.csproj {Fixture.SolutionBinaryLogPath}");
Assert.NotEqual(0, result);
}

[Fact]
public void CreateFullPath()
{
Expand All @@ -98,6 +121,12 @@ public void CreateFullPath()
Assert.Equal(0, RunCompLog($"create {GetBinaryLogFullPath()}", RootDirectory));
}

[Fact]
public void CreateOverRemovedProject()
{
Assert.Equal(1, RunCompLog($"create {Fixture.RemovedBinaryLogPath}"));
}

/// <summary>
/// Don't search for complogs when an explicit log source isn't provided.
/// </summary>
Expand All @@ -113,22 +142,28 @@ public void CreateOtherComplogExists()
[Fact]
public void References()
{
Assert.Equal(0, RunCompLog($"ref -o {RootDirectory} {Fixture.ConsoleComplogPath.Value}"));
Assert.NotEmpty(Directory.EnumerateFiles(Path.Combine(RootDirectory, "console", "refs"), "*.dll"));
Assert.NotEmpty(Directory.EnumerateFiles(Path.Combine(RootDirectory, "console", "analyzers"), "*.dll", SearchOption.AllDirectories));
RunWithBoth(logPath =>
{
Assert.Equal(0, RunCompLog($"ref -o {RootDirectory} {logPath}"));
Assert.NotEmpty(Directory.EnumerateFiles(Path.Combine(RootDirectory, "console", "refs"), "*.dll"));
Assert.NotEmpty(Directory.EnumerateFiles(Path.Combine(RootDirectory, "console", "analyzers"), "*.dll", SearchOption.AllDirectories));
});
}

[Fact]
public void ExportCompilerLog()
{
using var exportDir = new TempDir();
RunWithBoth(logPath =>
{
using var exportDir = new TempDir();

Assert.Equal(0, RunCompLog($"export -o {exportDir.DirectoryPath} {Fixture.ConsoleComplogPath.Value} ", RootDirectory));
Assert.Equal(0, RunCompLog($"export -o {exportDir.DirectoryPath} {logPath} ", RootDirectory));

// Now run the generated build.cmd and see if it succeeds;
var exportPath = Path.Combine(exportDir.DirectoryPath, "console", "export");
var buildResult = RunBuildCmd(exportPath);
Assert.True(buildResult.Succeeded);
// Now run the generated build.cmd and see if it succeeds;
var exportPath = Path.Combine(exportDir.DirectoryPath, "console", "export");
var buildResult = RunBuildCmd(exportPath);
Assert.True(buildResult.Succeeded);
});
}

[Theory]
Expand All @@ -137,7 +172,7 @@ public void ExportCompilerLog()
public void ReplayConsoleWithEmit(string arg)
{
using var emitDir = new TempDir();
RunCompLog($"replay {arg} -emit -o {emitDir.DirectoryPath} {Fixture.ConsoleComplogPath.Value}");
RunCompLog($"replay {arg} -emit -o {emitDir.DirectoryPath} {Fixture.SolutionBinaryLogPath}");

AssertOutput(@"console\emit\console.dll");
AssertOutput(@"console\emit\console.pdb");
Expand Down
107 changes: 107 additions & 0 deletions src/Basic.CompilerLog.UnitTests/SolutionFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Xunit;
using Xunit.Abstractions;

namespace Basic.CompilerLog.UnitTests;

/// <summary>
/// This fixture houses a solution with a variety of projects that have been built and
/// contain an available binary log.
/// </summary>
public sealed class SolutionFixture : FixtureBase, IDisposable
{
internal ImmutableArray<string> ProjectPaths { get; }

/// <summary>
/// Storage directory for all the generated artifacts and scatch directories
/// </summary>
internal string StorageDirectory { get; }

internal string SolutionPath { get; }

internal string SolutionBinaryLogPath { get; }

internal string ConsoleProjectPath { get; }

internal string ConsoleProjectName => Path.GetFileName(ConsoleProjectPath);

internal string ClassLibProjectPath { get; }

internal string RemovedBinaryLogPath { get; }

/// <summary>
/// This project is deleted off of disk after the binary log is created. This means subsequent calls
/// to create a compiler log over it will fail. Useful for testing error cases.
/// </summary>
internal string RemovedConsoleProjectPath { get; }

internal string RemovedConsoleProjectName => Path.GetFileName(RemovedConsoleProjectPath);

public SolutionFixture(IMessageSink messageSink)
: base(messageSink)
{
StorageDirectory = Path.Combine(Path.GetTempPath(), nameof(CompilerLogFixture), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(StorageDirectory);
SolutionPath = Path.Combine(StorageDirectory, "Solution.sln");
var binlogDir = Path.Combine(StorageDirectory, "binlogs");
Directory.CreateDirectory(binlogDir);

RunDotnetCommand("new globaljson --sdk-version 7.0.400", StorageDirectory);
RunDotnetCommand("dotnet new sln -n Solution", StorageDirectory);

var builder = ImmutableArray.CreateBuilder<string>();

ConsoleProjectPath = WithProject("console", string (string dir) =>
{
RunDotnetCommand("new console --name console -o .", dir);
return Path.Combine(dir, "console.csproj");
});

ClassLibProjectPath = WithProject("classlib", string (string dir) =>
{
RunDotnetCommand("new classlib --name classlib -o .", dir);
return Path.Combine(dir, "classlib.csproj");
});

string WithProject(string name, Func<string, string> func)
{
var dir = Path.Combine(StorageDirectory, name);
Directory.CreateDirectory(dir);
var projectPath = func(dir);
RunDotnetCommand($@"dotnet sln add ""{projectPath}""", StorageDirectory);
builder.Add(projectPath);
return projectPath;
};
ProjectPaths = builder.ToImmutableArray();
SolutionBinaryLogPath = Path.Combine(binlogDir, "msbuild.binlog");
DotnetUtil.CommandOrThrow($"dotnet build -bl:{SolutionBinaryLogPath} -nr:false", StorageDirectory);
(RemovedConsoleProjectPath, RemovedBinaryLogPath) = CreateRemovedProject();

(string, string) CreateRemovedProject()
{
var dir = Path.Combine(StorageDirectory, "removed");
Directory.CreateDirectory(dir);
RunDotnetCommand("new console --name removed-console -o .", dir);
var projectPath = Path.Combine(dir, "removed-console.csproj");
var binlogFilePath = Path.Combine(binlogDir, "removed-console.binlog");

DotnetUtil.CommandOrThrow($"dotnet build -bl:{binlogFilePath} -nr:false", dir);
Directory.Delete(dir, recursive: true);
return (projectPath, binlogFilePath);
}
}

public void Dispose()
{
Directory.Delete(StorageDirectory, recursive: true);
}
}

[CollectionDefinition(Name)]
public sealed class SolutionFixtureCollection : ICollectionFixture<SolutionFixture>
{
public const string Name = "Solution Collection";
}
Loading

0 comments on commit 6fd0357

Please sign in to comment.