Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to capture compilation ids #228

Merged
merged 14 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ FROM mcr.microsoft.com/devcontainers/dotnet:8.0

RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh && \
chmod +x dotnet-install.sh && \
./dotnet-install.sh --channel 9.0 --version latest --quality preview --install-dir /usr/share/dotnet && \
./dotnet-install.sh --channel 9.0 --version latest --install-dir /usr/share/dotnet && \
rm dotnet-install.sh
2 changes: 1 addition & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
test-results: test-results-linux

env:
TEST_ARTIFACTS_PATH: ${{ github.workspace }}${{ matrix.slash }}artifacts${{ matrix.slash }}test
TEST_ARTIFACTS_PATH: ${{ github.workspace }}${{ matrix.slash }}artifacts${{ matrix.slash }}test-artifacts
TEST_RESULTS_PATH: ${{ github.workspace }}${{ matrix.slash }}artifacts${{ matrix.slash }}test-results
TEST_COVERAGE_PATH: ${{ github.workspace }}${{ matrix.slash }}artifacts${{ matrix.slash }}coverage

Expand Down
6 changes: 3 additions & 3 deletions src/Basic.CompilerLog.UnitTests/CompilerLogFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ public CompilerLogFixture(IMessageSink messageSink)
Directory.CreateDirectory(ComplogDirectory);
Directory.CreateDirectory(ScratchDirectory);

var testArtifactsDir = Environment.GetEnvironmentVariable("TEST_ARTIFACTS_PATH");
if (testArtifactsDir is not null)
string? testArtifactsDir = null;
if (TestUtil.InGitHubActions)
{
testArtifactsDir = Path.Combine(testArtifactsDir, Guid.NewGuid().ToString("N"));
testArtifactsDir = Path.Combine(TestUtil.GitHubActionsTestArtifactsDirectory, "compilerlogfixture");
Directory.CreateDirectory(testArtifactsDir);
}

Expand Down
159 changes: 158 additions & 1 deletion src/Basic.CompilerLog.UnitTests/ProgramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ private void AssertCorrectReader(ICompilerCallReader reader, string logFilePath)
{
Assert.Empty(Directory.EnumerateFileSystemEntries(Constants.LocalAppDataDirectory));
}
return ((int)ret!, writer.ToString());

var output = writer.ToString();
TestOutputHelper.WriteLine(output);
return ((int)ret!, output);
}
finally
{
Expand All @@ -113,6 +116,16 @@ private void AssertCorrectReader(ICompilerCallReader reader, string logFilePath)
}
}

private static string GetIdentityHashConsole() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "0E662CF750EE1DD812AB28EEF043007BFE72655681838EB7AA35EC9BD48541FC"
: "3100BA355001A464D45D7636823F4B7E1729368DAFBA20BC1C482B9F6FA9E5E4";

private static string GetIdentityHashExample() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "7EEAE6122F14D721453ED7DEAE7E1BE1D7AC3E0D69657834FF376D91888A7B11"
: "D339B2B333F7C2344D6AFD47135FF7BF6F7DF64FB9E5E5E76690B093FC302BF9";

private void RunWithBoth(Action<string> action)
{
// Run with the binary log
Expand Down Expand Up @@ -304,6 +317,150 @@ public void CreateMultipleFiles()
Assert.Contains($"Found multiple log files in {RootDirectory}", output);
}

[Fact]
public void HashBadCommand()
{
var (exitCode, output) = RunCompLogEx($"hash move {Fixture.SolutionBinaryLogPath}");
Assert.NotEqual(Constants.ExitSuccess, exitCode);
Assert.Contains("move is not a valid command", output);
}

[Fact]
public void HashNoCommand()
{
var (exitCode, output) = RunCompLogEx("hash");
Assert.NotEqual(Constants.ExitSuccess, exitCode);
Assert.Contains("Need a subcommand", output);
}

[Fact]
public void HashHelp()
{
var (exitCode, output) = RunCompLogEx("hash help");
Assert.Equal(Constants.ExitSuccess, exitCode);
Assert.Contains("complog hash [command] [args]", output);
}

[Fact]
public void HashPrintSimple()
{
var (exitCode, output) = RunCompLogEx($"hash print -p {Fixture.ConsoleProjectName} {Fixture.SolutionBinaryLogPath}");
Assert.Equal(Constants.ExitSuccess, exitCode);
var expected = GetIdentityHashConsole();
Assert.Contains($"console {expected}", output);
}

[Fact]
public void HashPrintAll()
{
var (exitCode, output) = RunCompLogEx($"hash print {Fixture.SolutionBinaryLogPath}");
Assert.Equal(Constants.ExitSuccess, exitCode);
var expected = GetIdentityHashConsole();
Assert.Contains($"console {expected}", output);
}

[Fact]
public void HashPrintHelp()
{
var (exitCode, output) = RunCompLogEx("hash print -h");
Assert.Equal(Constants.ExitSuccess, exitCode);
Assert.StartsWith("complog hash print [OPTIONS]", output);
}

[Fact]
public void HashPrintBadOption()
{
var (exitCode, output) = RunCompLogEx("hash print --does-not-exist");
Assert.NotEqual(Constants.ExitSuccess, exitCode);
Assert.Contains("complog hash print [OPTIONS]", output);
}

[Fact]
public void HashExportInline()
{
var dir = Root.NewDirectory();
RunDotNet($"new console --name example --output .", dir);
var (exitCode, output) = RunCompLogEx($"hash export -i", dir);
Assert.Equal(Constants.ExitSuccess, exitCode);
Assert.Contains("Generating hash files inline", output);

var identityFilePath = Path.Combine(dir, "build-identity-hash.txt");
var contentFilePath = Path.Combine(dir, "build-content-hash.txt");
try
{
Assert.True(File.Exists(identityFilePath));
Assert.Equal(GetIdentityHashExample(), File.ReadAllText(identityFilePath));
Assert.True(File.Exists(contentFilePath));
var actualContentHash = File.ReadAllText(contentFilePath);
Assert.Contains(@"""outputKind"": ""ConsoleApplication""", actualContentHash);
Assert.Contains(@"""moduleName"": ""example.dll""", actualContentHash);
}
catch (Exception)
{
AddFileToTestArtifacts(contentFilePath);
throw;
}
}

[Fact]
public void HashExportFull()
{
var dir = Root.NewDirectory();
RunDotNet($"new console --name example --output .", dir);

var outDir = Root.NewDirectory();
var (exitCode, output) = RunCompLogEx($"hash export -o {outDir} ", dir);
Assert.Equal(Constants.ExitSuccess, exitCode);

var identityFilePath = Path.Combine(outDir, "example", "build-identity-hash.txt");
Assert.True(File.Exists(identityFilePath));
var contentFilePath = Path.Combine(outDir, "example", "build-content-hash.txt");
Assert.True(File.Exists(identityFilePath));
Assert.Contains("""
"outputKind": "ConsoleApplication",
"moduleName": "example.dll",
""", File.ReadAllText(contentFilePath));
}

[Fact]
public void HashExportBadOption()
{
var (exitCode, output) = RunCompLogEx("hash export --does-not-exist");
Assert.NotEqual(Constants.ExitSuccess, exitCode);
Assert.Contains("complog hash export [OPTIONS]", output);
}

[Fact]
public void HashExportInlineAndOutput()
{
var (exitCode, output) = RunCompLogEx("hash export --inline --out example");
Assert.NotEqual(Constants.ExitSuccess, exitCode);
Assert.Contains("complog hash export [OPTIONS]", output);
}

[Fact]
public void HashExportHelp()
{
var (exitCode, output) = RunCompLogEx("hash export -h");
Assert.Equal(Constants.ExitSuccess, exitCode);
Assert.Contains("complog hash export [OPTIONS]", output);
}

[Fact]
public void HashInlineAndOutput()
{
var dir = Root.NewDirectory();
var (exitCode, output) = RunCompLogEx($"id -i -o blah");
Assert.NotEqual(Constants.ExitSuccess, exitCode);
}

[Fact]
public void HashBadOption()
{
var (exitCode, output) = RunCompLogEx($"id -blah");
Assert.NotEqual(Constants.ExitSuccess, exitCode);
}

[Fact]
public void References()
{
Expand Down
30 changes: 29 additions & 1 deletion src/Basic.CompilerLog.UnitTests/TestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ public virtual void Dispose()
}
Assert.Fail("Bad assembly loads");
}
TestOutputHelper.WriteLine("Deleting temp directory");
Root.Dispose();
}

Expand Down Expand Up @@ -294,4 +293,33 @@ private void OnAssemblyLoad(object? sender, AssemblyLoadEventArgs e)
BadAssemblyLoadList.Add(Path.GetFileName(e.LoadedAssembly.Location));
}
}

protected void AddFileToTestArtifacts(string filePath, [CallerMemberName] string? memberName = null)
{
Debug.Assert(memberName is not null);

string testResultsDir;
bool overwrite;
if (TestUtil.InGitHubActions)
{
overwrite = false;
testResultsDir = TestUtil.GitHubActionsTestArtifactsDirectory;
}
else
{
var assemblyDir = Path.GetDirectoryName(typeof(TestBase).Assembly.Location)!;
testResultsDir = Path.Combine(assemblyDir, "test-artifacts");

// Need to overwrite locally or else every time you re-run the test you need to go and
// delete the test-artifacts directory
overwrite = true;
}

var typeName = this.GetType().FullName;
var memberDir = Path.Combine(testResultsDir, $"{typeName}.{memberName}");
Directory.CreateDirectory(memberDir);

TestOutputHelper.WriteLine($"Saving {filePath} to test artifacts dir {memberDir}");
File.Copy(filePath, Path.Combine(memberDir, Path.GetFileName(filePath)), overwrite);
}
}
31 changes: 31 additions & 0 deletions src/Basic.CompilerLog.UnitTests/TestUtil.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
Expand All @@ -10,6 +11,36 @@ namespace Basic.CompilerLog.UnitTests;

internal static class TestUtil
{
internal static bool IsNetFramework =>
#if NETFRAMEWORK
true;
#else
false;
#endif

internal static bool IsNetCore => !IsNetFramework;

internal static bool InGitHubActions => Environment.GetEnvironmentVariable("GITHUB_ACTIONS") is not null;

internal static string GitHubActionsTestArtifactsDirectory
{
get
{
Debug.Assert(InGitHubActions);

var testArtifactsDir = Environment.GetEnvironmentVariable("TEST_ARTIFACTS_PATH");
if (testArtifactsDir is null)
{
throw new Exception("TEST_ARTIFACTS_PATH is not set in GitHub actions");

}

var suffix = IsNetCore ? "netcore" : "netfx";

return Path.Combine(testArtifactsDir, suffix);
}
}

/// <summary>
/// Internally a <see cref="IIncrementalGenerator" /> is wrapped in a type called IncrementalGeneratorWrapper.
/// This method will dig through that and return the original type.
Expand Down
3 changes: 2 additions & 1 deletion src/Basic.CompilerLog.UnitTests/xunit.runner.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"appDomain": "ifAvailable",
"shadowCopy": false,
"parallelizeTestCollections": false
"parallelizeTestCollections": false,
"printMaxStringLength": 0
}
81 changes: 81 additions & 0 deletions src/Basic.CompilerLog.Util/CompilationData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;

namespace Basic.CompilerLog.Util;

Expand Down Expand Up @@ -360,6 +363,84 @@
cancellationToken: cancellationToken);
}

/// <summary>
/// This returns the compilation as a content string. Two compilations that are equal will have the
/// same content text. This can be checksum'd to produce concise compilation ids
/// </summary>
/// <returns></returns>
public string GetContentHash()
{
var assembly = typeof(Compilation).Assembly;
var type = assembly.GetType( "Microsoft.CodeAnalysis.DeterministicKey", throwOnError: true)!;
var method = type
.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)
.Where(x => IsMethod(x))
.Single();

// This removes our implementation of the syntax tree options provider. Leaving that in means
// that the content text will be different every time compiler log is updated and that is not
// desirable.
var options = CompilationOptions.WithSyntaxTreeOptionsProvider(null);

// This removes file full paths and tool versions from the content text.
int flags = 0b11;
object[] args =
[
options,
Compilation.SyntaxTrees.ToImmutableArray(),
Compilation.References.ToImmutableArray(),
ImmutableArray<byte>.Empty,
AdditionalTexts,
GetAnalyzers(),
GetGenerators(),
ImmutableArray<KeyValuePair<string, string>>.Empty,
EmitOptions,
flags,
(CancellationToken)default,
];

var result = method.Invoke(null, args)!;
return (string)result;

static bool IsMethod(MethodInfo method)
{
if (method.Name != "GetDeterministicKey")
{
return false;

Check warning on line 409 in src/Basic.CompilerLog.Util/CompilationData.cs

View check run for this annotation

Codecov / codecov/patch

src/Basic.CompilerLog.Util/CompilationData.cs#L408-L409

Added lines #L408 - L409 were not covered by tests
}

var parameters = method.GetParameters();
if (parameters.Length < 2)
{
return false;

Check warning on line 415 in src/Basic.CompilerLog.Util/CompilationData.cs

View check run for this annotation

Codecov / codecov/patch

src/Basic.CompilerLog.Util/CompilationData.cs#L414-L415

Added lines #L414 - L415 were not covered by tests
}

return parameters[1].ParameterType == typeof(ImmutableArray<SyntaxTree>);
}
}

/// <summary>
/// This produces the content hash from <see cref="GetContentHash"/> as well as the identity hash
/// which is just a checksum of the content hash.
/// </summary>
/// <returns></returns>
public (string ContentHash, string IdentityHash) GetContentAndIdentityHash()
{
var contentHash = GetContentHash();
var identityHash = GetIdentityHash(contentHash);
return (contentHash, identityHash);
}

public string GetIdentityHash() =>
GetIdentityHash(GetContentHash());

private static string GetIdentityHash(string contentHash)
{
var sum = SHA256.Create();
var bytes = sum.ComputeHash(Encoding.UTF8.GetBytes(contentHash));
return bytes.AsHexString();
}

private bool IncludeMetadataStream() =>
!EmitOptions.EmitMetadataOnly &&
!EmitOptions.IncludePrivateMembers &&
Expand Down
Loading