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

Add support for deterministic builds #883

Merged
merged 11 commits into from
Apr 12, 2023
18 changes: 13 additions & 5 deletions src/Shouldly/Internals/SourceCodeTextGetter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ private void ParseStackTrace(StackTrace? stackTrace)
ShouldlyFrameOffset = originatingFrame.index;

var fileName = originatingFrame.frame.GetFileName();
fileName = 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)
{
if (fileName?.StartsWith(@"/_/", StringComparison.Ordinal) == true)
{
var sourceRoot = ShouldlyConfiguration.SourceRoot;
Expand All @@ -57,15 +66,14 @@ private void ParseStackTrace(StackTrace? stackTrace)
TryFindGitRepoRoot(assemblyDirectory!, out sourceRoot);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe searching for the SLN could be an alternative to the directory .git. It would remove the dependency to Git if you start a project and add Git afterwards.

    private static bool TryFindProjectFolder(string startDirectory, [NotNullWhen(true)] out string? projectFolder)
    {
        try
        {
            var currentDirectory = new DirectoryInfo(startDirectory);
            while (currentDirectory != null)
            {
                if (IsGitParentDirectory(currentDirectory)
                    || IsSlnParentDirectory(currentDirectory))
                {
                    projectFolder = currentDirectory.FullName;
                    return true;
                }

                currentDirectory = currentDirectory.Parent;
            }
        }
        catch { }

        projectFolder = null;
        return false;
    }

    private static bool IsGitParentDirectory(DirectoryInfo currentDirectory)
    {
        var gitDirectory = Path.Combine(currentDirectory.FullName, ".git");
        return Directory.Exists(gitDirectory);
    }

    private static bool IsSlnParentDirectory(DirectoryInfo currentDirectory)
    {
        var foundFiles = Directory.EnumerateFiles(currentDirectory.FullName, "*.sln");
        return foundFiles.Any();
    }

Further checks could be implemented as needed. I think the best solution would be to look for the CSPROJ of the test projects and parse for the referenced projects to test. But there could be multiple. It would add some complexity.

Copy link
Member Author

@slang25 slang25 Mar 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd get too many false positives, I just checked a handful of open source .NET projects and about half had the .sln file one level in (Newtonsoft.Json, Spectre.Console etc...).

The git approach is quite crude, but might cover 90% of scenarios, for everything else we can tell users to pass a SourceRoot.

For deterministic builds, there an MSBuild ItemGroup item named SourceRoot which ideally we'd want to reuse, but the only way to get it would be some sort of source code generation.

}
}

if (sourceRoot != null)
{
fileName = fileName.Replace("/_/", sourceRoot + Path.PathSeparator);
return fileName.Replace("/_/", sourceRoot + Path.PathSeparator);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am working on a windows system. The files are processed with this inputs and outputs:

Non deterministic test project build:

Input: E:\Temp\shouldly\src\DeterministicTests\Tests.cs
Output: E:\Temp\shouldly\src\DeterministicTests\Tests.cs

This is fine.

Deterministic test project build:

Input: /_/src/DeterministicTests/Tests.cs
Output: E:\Temp\shouldly;src/DeterministicTests/Tests.cs

Path.PathSeperator should be Path.DirectorySeperator and the slashes must be converted.

return fileName.Replace("/_/", sourceRoot + Path.DirectorySeparatorChar)
    .Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great spot, thanks

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

return fileName;
}

private string GetCodePart()
Expand Down