Skip to content

Commit

Permalink
Merge pull request #898 from shouldly/better-determistic-builds
Browse files Browse the repository at this point in the history
Improved deterministic build support
  • Loading branch information
slang25 committed Apr 24, 2023
2 parents 1e08eef + 1336932 commit c26fcfc
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 34 deletions.
1 change: 1 addition & 0 deletions src/DeterministicTests/DeterministicTests.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Shouldly\buildTransitive\Shouldly.targets" />
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
Expand Down
1 change: 0 additions & 1 deletion src/DeterministicTests/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<PropertyGroup>
<NoWarn>CS1591;CS0649</NoWarn>
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup>
<SourceRoot Include = " $(MSBuildThisFileDirectory)/ " />
Expand Down
5 changes: 4 additions & 1 deletion src/Shouldly/Configuration/TestMethodInfo.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using System.Diagnostics;
using System.Reflection;
using Shouldly.Internals;

namespace Shouldly.Configuration;

public class TestMethodInfo
{
public TestMethodInfo(StackFrame callingFrame)
{
SourceFileDirectory = Path.GetDirectoryName(callingFrame.GetFileName());
var fileName = callingFrame.GetFileName();
fileName = DeterministicBuildHelpers.ResolveDeterministicPaths(fileName);
SourceFileDirectory = Path.GetDirectoryName(fileName);

var method = callingFrame.GetMethod();
var originalMethodInfo = GetOriginalMethodInfoForStateMachineMethod(method);
Expand Down
41 changes: 41 additions & 0 deletions src/Shouldly/Internals/DeterministicBuildHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Text.RegularExpressions;

namespace Shouldly.Internals;

internal static class DeterministicBuildHelpers
{
private static readonly Regex DeterministicPathRegex = new(@"^/_\d*/");

private static readonly Lazy<IEnumerable<(string, string)>> LazySourcePathMap
= new(() =>
{
var shouldlySourcePathMap = Environment.GetEnvironmentVariable("SHOULDLY_SOURCE_PATH_MAP") ?? "";
var pathMapPairs = shouldlySourcePathMap
.Split(',')
.Select(x => x.Split('='))
.Where(x => x.Length == 2)
.Select(x => (x[0], x[1]));
return pathMapPairs;
});

private static IEnumerable<(string, string)> SourcePathMap => LazySourcePathMap.Value;

internal static string? ResolveDeterministicPaths(string? fileName)
{
foreach (var (path, placeholder) in SourcePathMap)
{
if (fileName?.StartsWith(placeholder, StringComparison.Ordinal) == true)
{
return fileName.Replace(placeholder, path);
}
}

return fileName;
}

internal static bool PathAppearsToBeDeterministic(string fileName)
{
return DeterministicPathRegex.IsMatch(fileName);
}
}
29 changes: 1 addition & 28 deletions src/Shouldly/Internals/SourceCodeTextGetter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,6 @@ internal class ActualCodeTextGetter : ICodeTextGetter
private bool _determinedOriginatingFrame;
private string? _shouldMethod;

private static readonly Lazy<IEnumerable<(string, string)>> SourcePathMap
= new(() =>
{
var shouldlySourcePathMap = Environment.GetEnvironmentVariable("SHOULDLY_SOURCE_PATH_MAP") ?? "";
var pathMapPairs = shouldlySourcePathMap
.Split(',')
.Select(x => x.Split('='))
.Where(x => x.Length == 2)
.Select(x => (x[0], x[1]));
return pathMapPairs;
});

public int ShouldlyFrameOffset { get; private set; }
public string? FileName { get; private set; }
public int LineNumber { get; private set; }
Expand Down Expand Up @@ -56,27 +43,13 @@ private void ParseStackTrace(StackTrace? stackTrace)
ShouldlyFrameOffset = originatingFrame.index;

var fileName = originatingFrame.frame.GetFileName();
fileName = ResolveDeterministicPaths(fileName);
fileName = DeterministicBuildHelpers.ResolveDeterministicPaths(fileName);
_determinedOriginatingFrame = fileName != null && File.Exists(fileName);
_shouldMethod = shouldlyFrame.method.Name;
FileName = fileName;
LineNumber = originatingFrame.frame.GetFileLineNumber() - 1;
}

private static string? ResolveDeterministicPaths(string? fileName)
{
var sourcePathMap = SourcePathMap.Value;
foreach ((var path, var placeholder) in sourcePathMap)
{
if (fileName?.StartsWith(placeholder, StringComparison.Ordinal) == true)
{
return fileName.Replace(placeholder, path);
}
}

return fileName;
}

private string GetCodePart()
{
var codePart = "Shouldly uses your source code to generate its great error messages, build your test project with full debug information to get better error messages" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public static void ShouldMatchApproved(this string actual, Action<ShouldMatchCon
var outputFolder = testMethodInfo.SourceFileDirectory;
if (string.IsNullOrEmpty(outputFolder))
throw new($"Source information not available, make sure you are compiling with full debug information. Frame: {testMethodInfo.DeclaringTypeName}.{testMethodInfo.MethodName}");
if (DeterministicBuildHelpers.PathAppearsToBeDeterministic(outputFolder))
throw new($"Unable to resolve source file from deterministic build source path. Frame: {testMethodInfo.DeclaringTypeName}.{testMethodInfo.MethodName}");

if (!string.IsNullOrEmpty(config.ApprovalFileSubFolder))
{
outputFolder = Path.Combine(outputFolder, config.ApprovalFileSubFolder);
Expand Down Expand Up @@ -66,4 +69,4 @@ public static void ShouldMatchApproved(this string actual, Action<ShouldMatchCon

File.Delete(receivedFile);
}
}
}
36 changes: 33 additions & 3 deletions src/Shouldly/buildTransitive/Shouldly.targets
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,37 @@
</Task>
</UsingTask>

<Target Name="SetPathMapsForShouldly" BeforeTargets="CoreCompile" DependsOnTargets="_SetPathMapFromSourceRoots" Condition=" '$(PathMap)' != '' AND '$(SHOULDLY_SOURCE_PATH_MAP)' == '' " >
<SetEnvVar EnvName="SHOULDLY_SOURCE_PATH_MAP" EnvValue="$(PathMap)" />
<!--
This target is used to capture the PathMap value when building the project. The PathMap is used by Shouldly to map paths
in stack traces to the source files. The PathMap is written to a file and read by the SetShouldlyPathMaps target.
When VSTestNoBuild is not true, then VSTest has inititiated the build so the SetShouldlyPathMaps target has already started,
so we need to set the SHOULDLY_SOURCE_PATH_MAP environment variable here.
-->
<Target Name="CapturePathMapsForShouldly" BeforeTargets="CoreCompile" DependsOnTargets="_SetPathMapFromSourceRoots" Condition=" '$(DisableShouldlyPathMaps)' != 'true' AND '$(DeterministicSourcePaths)' == 'true' " >
<PropertyGroup>
<_ShouldlyPathMapsFilePath>$([MSBuild]::EnsureTrailingSlash('$(OutputPath)'))ShouldlyPathMaps_$(AssemblyName)</_ShouldlyPathMapsFilePath>
</PropertyGroup>
<WriteLinesToFile File="$(_ShouldlyPathMapsFilePath)" Lines="$(PathMap)" Overwrite="true" Encoding="Unicode" Condition=" '$(PathMap)' != '' " WriteOnlyWhenDifferent="true" />
<ItemGroup>
<FileWrites Include="$(_ShouldlyPathMapsFilePath)" Condition=" '$(PathMap)' != '' " />
</ItemGroup>
<SetEnvVar EnvName="SHOULDLY_SOURCE_PATH_MAP" EnvValue="$(PathMap)" Condition=" '$(VSTestNoBuild)' != 'true' " />
</Target>
</Project>

<!--
This target is used to set the SHOULDLY_SOURCE_PATH_MAP environment variable when running tests with VSTestNoBuild=true.
This is necessary because the VSTest task does not run the CoreCompile target, so the PathMap is not set.
-->
<Target Name="SetShouldlyPathMaps" BeforeTargets="VSTest" Condition=" '$(DisableShouldlyPathMaps)' != 'true' AND '$(VSTestNoBuild)' == 'true' ">
<PropertyGroup>
<_ShouldlyPathMapsFilePath>$([MSBuild]::EnsureTrailingSlash('$(OutputPath)'))ShouldlyPathMaps_$(AssemblyName)</_ShouldlyPathMapsFilePath>
</PropertyGroup>
<ReadLinesFromFile File="$(_ShouldlyPathMapsFilePath)">
<Output TaskParameter="Lines" ItemName="_ShouldlyPathMaps" />
</ReadLinesFromFile>
<PropertyGroup>
<ShouldlyPathMaps>%(_ShouldlyPathMaps.Identity)</ShouldlyPathMaps>
</PropertyGroup>
<SetEnvVar EnvName="SHOULDLY_SOURCE_PATH_MAP" EnvValue="$(ShouldlyPathMaps)" Condition=" '$(ShouldlyPathMaps)' != '' " />
</Target>
</Project>

0 comments on commit c26fcfc

Please sign in to comment.