diff --git a/Directory.Build.props b/Directory.Build.props index 598b4c6..0746ad4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,7 @@ + $(MSBuildThisFileDirectory) + false @@ -36,6 +38,7 @@ diff --git a/DotNet.ReproducibleBuilds.sln b/DotNet.ReproducibleBuilds.sln index c7c9d0d..a5409f9 100644 --- a/DotNet.ReproducibleBuilds.sln +++ b/DotNet.ReproducibleBuilds.sln @@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNet.ReproducibleBuilds.Isolated", "src\DotNet.ReproducibleBuilds.Isolated\DotNet.ReproducibleBuilds.Isolated.csproj", "{BD88D2CB-4342-47A3-B0D1-07321E9A92C1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNet.ReproducibleBuilds.Tests", "tests\DotNet.ReproducibleBuilds.Tests\DotNet.ReproducibleBuilds.Tests.csproj", "{72BE4FA4-D190-44AF-B056-23AA79D1553A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +33,10 @@ Global {BD88D2CB-4342-47A3-B0D1-07321E9A92C1}.Debug|Any CPU.Build.0 = Debug|Any CPU {BD88D2CB-4342-47A3-B0D1-07321E9A92C1}.Release|Any CPU.ActiveCfg = Release|Any CPU {BD88D2CB-4342-47A3-B0D1-07321E9A92C1}.Release|Any CPU.Build.0 = Release|Any CPU + {72BE4FA4-D190-44AF-B056-23AA79D1553A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72BE4FA4-D190-44AF-B056-23AA79D1553A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72BE4FA4-D190-44AF-B056-23AA79D1553A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72BE4FA4-D190-44AF-B056-23AA79D1553A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 123f459..18466f3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -30,6 +30,9 @@ stages: - script: dotnet nbgv cloud displayName: Set Version + - script: dotnet test dirs.proj + displayName: Test + - script: dotnet pack dirs.proj displayName: Create package(s) diff --git a/tests/DotNet.ReproducibleBuilds.Tests/BooleanExtensions.cs b/tests/DotNet.ReproducibleBuilds.Tests/BooleanExtensions.cs new file mode 100644 index 0000000..05a2bf6 --- /dev/null +++ b/tests/DotNet.ReproducibleBuilds.Tests/BooleanExtensions.cs @@ -0,0 +1,9 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DotNet.ReproducibleBuilds.Tests; + +internal static class BooleanExtensions +{ + public static string ToLowerInvariant(this bool value) => value.ToString().ToLowerInvariant(); + public static string? ToLowerInvariant([NotNullIfNotNull(nameof(value))] this bool? value) => value?.ToString().ToLowerInvariant(); +} diff --git a/tests/DotNet.ReproducibleBuilds.Tests/DotNet.ReproducibleBuilds.Tests.csproj b/tests/DotNet.ReproducibleBuilds.Tests/DotNet.ReproducibleBuilds.Tests.csproj new file mode 100644 index 0000000..c0e5b89 --- /dev/null +++ b/tests/DotNet.ReproducibleBuilds.Tests/DotNet.ReproducibleBuilds.Tests.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + enable + enable + + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/tests/DotNet.ReproducibleBuilds.Tests/EnvironmentVariableSuppressor.cs b/tests/DotNet.ReproducibleBuilds.Tests/EnvironmentVariableSuppressor.cs new file mode 100644 index 0000000..0f62f24 --- /dev/null +++ b/tests/DotNet.ReproducibleBuilds.Tests/EnvironmentVariableSuppressor.cs @@ -0,0 +1,19 @@ +namespace DotNet.ReproducibleBuilds.Tests; + +internal sealed class EnvironmentVariableSuppressor : IDisposable +{ + private readonly string? _value; + private readonly string _name; + + public EnvironmentVariableSuppressor(string name) + { + _name = name; + _value = Environment.GetEnvironmentVariable(name); + Environment.SetEnvironmentVariable(name, null); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable(_name, _value); + } +} diff --git a/tests/DotNet.ReproducibleBuilds.Tests/FileSystemInfoExtensions.cs b/tests/DotNet.ReproducibleBuilds.Tests/FileSystemInfoExtensions.cs new file mode 100644 index 0000000..7abcbb7 --- /dev/null +++ b/tests/DotNet.ReproducibleBuilds.Tests/FileSystemInfoExtensions.cs @@ -0,0 +1,9 @@ +namespace DotNet.ReproducibleBuilds.Tests; + +internal static class FileSystemInfoExtensions +{ + public static string Combine(this FileSystemInfo info, params string[] paths) + { + return Path.Combine([info.FullName, ..paths]); + } +} diff --git a/tests/DotNet.ReproducibleBuilds.Tests/MSBuildModuleInitializer.cs b/tests/DotNet.ReproducibleBuilds.Tests/MSBuildModuleInitializer.cs new file mode 100644 index 0000000..a892955 --- /dev/null +++ b/tests/DotNet.ReproducibleBuilds.Tests/MSBuildModuleInitializer.cs @@ -0,0 +1,13 @@ +using Microsoft.Build.Utilities.ProjectCreation; +using System.Runtime.CompilerServices; + +namespace DotNet.ReproducibleBuilds.Tests; + +internal static class MSBuildModuleInitializer +{ + [ModuleInitializer] + internal static void InitializeMSBuild() + { + MSBuildAssemblyResolver.Register(); + } +} diff --git a/tests/DotNet.ReproducibleBuilds.Tests/ProjectTemplates.cs b/tests/DotNet.ReproducibleBuilds.Tests/ProjectTemplates.cs new file mode 100644 index 0000000..d2ebecb --- /dev/null +++ b/tests/DotNet.ReproducibleBuilds.Tests/ProjectTemplates.cs @@ -0,0 +1,26 @@ +using Microsoft.Build.Utilities.ProjectCreation; + +namespace DotNet.ReproducibleBuilds.Tests; + +internal static class ProjectTemplates +{ + private static readonly string ThisAssemblyDirectory = Path.GetDirectoryName(typeof(ProjectTemplates).Assembly.Location)!; + + public static ProjectCreator ReproducibleBuildProject(this ProjectCreatorTemplates templates, FileInfo project) + { + DirectoryInfo directory = project.Directory ?? throw new ArgumentException("Project's path does not appear to have a parent.", nameof(project)); + + _ = ProjectCreator + .Create(path: directory.Combine("obj", $"{project.Name}.tests.g.props")) + .Import(Path.Combine(ThisAssemblyDirectory, "DotNet.ReproducibleBuilds.props")) + .Save(); + + _ = ProjectCreator + .Create(path: directory.Combine("obj", $"{project.Name}.tests.g.targets")) + .Import(Path.Combine(ThisAssemblyDirectory, "DotNet.ReproducibleBuilds.targets")) + .Save(); + + return templates + .SdkCsproj(path: project.FullName, targetFramework: "net8.0"); + } +} diff --git a/tests/DotNet.ReproducibleBuilds.Tests/SourceLinkTests.cs b/tests/DotNet.ReproducibleBuilds.Tests/SourceLinkTests.cs new file mode 100644 index 0000000..d46c7f9 --- /dev/null +++ b/tests/DotNet.ReproducibleBuilds.Tests/SourceLinkTests.cs @@ -0,0 +1,105 @@ +using FluentAssertions; +using Microsoft.Build.Utilities.ProjectCreation; + +namespace DotNet.ReproducibleBuilds.Tests; + +public class SourceLinkTests : TestBase +{ + [Theory] + [InlineData(null, true)] + [InlineData(false, false)] + [InlineData(true, true)] + public void PublishRepositoryUrlIsSet(bool? publishRepositoryUrl, bool expected) + { + ProjectCreator.Templates + .ReproducibleBuildProject(GetRandomFile(".csproj")) + .PropertyGroup() + .Property("PublishRepositoryUrl", publishRepositoryUrl.ToLowerInvariant()) + .Project + .GetPropertyValue("PublishRepositoryUrl") + .Should().Be(expected.ToLowerInvariant()); + } + + [Theory] + [InlineData(null, "embedded")] + [InlineData("embedded", "embedded")] + [InlineData("portable", "portable")] + public void DebugTypeIsSet(string? debugType, string expected) + { + ProjectCreator.Templates + .ReproducibleBuildProject(GetRandomFile(".csproj")) + .PropertyGroup() + .Property("DebugType", debugType) + .Project + .GetPropertyValue("DebugType") + .Should().Be(expected); + } + + [Theory] + [InlineData(null, true)] + [InlineData(false, false)] + [InlineData(true, true)] + public void EmbedUntrackedSourcesIsSet(bool? embedUntrackedSources, bool expected) + { + ProjectCreator.Templates + .ReproducibleBuildProject(GetRandomFile(".csproj")) + .PropertyGroup() + .Property("PublishRepositoryUrl", embedUntrackedSources.ToLowerInvariant()) + .Project + .GetPropertyValue("PublishRepositoryUrl") + .Should().Be(expected.ToLowerInvariant()); + } + + [Theory] + [InlineData("GITHUB_REF", "refs/pull/1234/merge", "pr1234")] + [InlineData("GITHUB_REF", "refs/heads/my-branch", "my-branch")] + [InlineData("GITHUB_REF", "refs/tags/v1.2.3", "v1.2.3")] + + [InlineData("BUILD_SOURCEBRANCH", "refs/heads/my-branch", "my-branch")] + [InlineData("BUILD_SOURCEBRANCH", "refs/tags/v1.2.3", "v1.2.3")] + + [InlineData("APPVEYOR_PULL_REQUEST_NUMBER", "1234", "pr1234")] + [InlineData("APPVEYOR_REPO_TAG_NAME", "refs/tags/v1.2.3", "refs/tags/v1.2.3")] + [InlineData("APPVEYOR_REPO_BRANCH", "refs/heads/my-branch", "refs/heads/my-branch")] + + [InlineData("TEAMCITY_BUILD_BRANCH", "refs/heads/my-branch", "refs/heads/my-branch")] + + [InlineData("TRAVIS_PULL_REQUEST", "1234", "pr1234")] + [InlineData("TRAVIS_BRANCH", "refs/heads/my-branch", "refs/heads/my-branch")] + + [InlineData("CIRCLE_PR_NUMBER", "1234", "pr1234")] + [InlineData("CIRCLE_TAG", "refs/heads/v1.2.3", "refs/heads/v1.2.3")] + [InlineData("CIRCLE_BRANCH", "refs/heads/my-branch", "refs/heads/my-branch")] + + [InlineData("CI_COMMIT_TAG", "refs/tags/v1.2.3", "refs/tags/v1.2.3")] + [InlineData("CI_MERGE_REQUEST_IID", "1234", "pr1234")] + [InlineData("CI_COMMIT_BRANCH", "refs/heads/my-branch", "refs/heads/my-branch")] + + [InlineData("BUDDY_EXECUTION_PULL_REQUEST_NO", "1234", "pr1234")] + [InlineData("BUDDY_EXECUTION_TAG", "refs/tags/v1.2.3", "refs/tags/v1.2.3")] + [InlineData("BUDDY_EXECUTION_BRANCH", "refs/heads/my-branch", "refs/heads/my-branch")] + public void RepositoryBranchIsSet(string ci, string original, string expected) + { + using EnvironmentVariableSuppressor hostSuppressor = new("BUILD_SOURCEBRANCH"); // Suppress our own CI provider variables (i.e. Azure DevOps) + using EnvironmentVariableSuppressor ciSuppressor = new(ci); // Suppress the mock CI provider (just in case). + + // If RepositoryBranch is set, it should take precedence over the CI provider variables + ProjectCreator.Templates + .ReproducibleBuildProject(GetRandomFile(".csproj")) + .PropertyGroup() + .Property("RepositoryBranch", "explicitly-set") + .Property(ci, original) + .Project + .GetPropertyValue("RepositoryBranch") + .Should().Be("explicitly-set", "because explicitly setting `RepositoryBranch` should always win."); + + // If RepositoryBranch is not set, it should be set from the CI provider property + ProjectCreator.Templates + .ReproducibleBuildProject(GetRandomFile(".csproj")) + .PropertyGroup() + .Property(ci, original) + .Project + .GetPropertyValue("RepositoryBranch") + .Should().Be(expected); + } +} diff --git a/tests/DotNet.ReproducibleBuilds.Tests/TestBase.cs b/tests/DotNet.ReproducibleBuilds.Tests/TestBase.cs new file mode 100644 index 0000000..b1599f4 --- /dev/null +++ b/tests/DotNet.ReproducibleBuilds.Tests/TestBase.cs @@ -0,0 +1,38 @@ +namespace DotNet.ReproducibleBuilds.Tests; + +public abstract class TestBase : IDisposable +{ + protected TestBase() + { + TestRootPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())); + } + + public DirectoryInfo TestRootPath { get; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool isDisposing) + { + TestRootPath.Refresh(); + if (TestRootPath.Exists) + { + try + { + TestRootPath.Delete(recursive: true); + } + catch (Exception) + { + // Ignored + } + } + } + + protected FileInfo GetRandomFile(string? extension = null) + { + return new(TestRootPath.Combine($"{Path.GetRandomFileName()}{extension ?? string.Empty}")); + } +}