|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System.IO.Compression;
using System.Reflection.Metadata;
namespace Microsoft.NET.Build.Tests
{
public class SourceLinkTests : SdkTest
{
private static readonly Guid s_embeddedSourceKindGuid = new("0E8A571B-6926-466E-B4AD-8AB04611F5FE");
public SourceLinkTests(ITestOutputHelper log)
: base(log)
{
}
private void CreateGitFiles(string repoDir, string originUrl, string commitSha = "1200000000000000000000000000000000000000")
{
var gitDir = Path.Combine(repoDir, ".git");
var headsDir = Path.Combine(gitDir, "refs", "heads");
Directory.CreateDirectory(gitDir);
File.WriteAllText(Path.Combine(gitDir, "HEAD"), "ref: refs/heads/master");
Directory.CreateDirectory(headsDir);
if (commitSha != null)
{
File.WriteAllText(Path.Combine(headsDir, "master"), commitSha);
}
if (originUrl != null)
{
File.WriteAllText(Path.Combine(gitDir, "config"), $"""
[remote "origin"]
url = {originUrl}
""");
}
File.WriteAllText(Path.Combine(repoDir, ".gitignore"), """
[Bb]in/
[Oo]bj/
""");
}
private unsafe void ValidatePdb(string pdbPath, bool expectedEmbeddedSources)
{
// Validates that *.AssemblyAttributes.cs file is embedded in the PDB.
var pdb = File.ReadAllBytes(pdbPath);
fixed (byte* pdbPtr = pdb)
{
var mdReader = new MetadataReader(pdbPtr, pdb.Length);
var attrDocHandle = mdReader.Documents.Single(h => mdReader.GetString(mdReader.GetDocument(h).Name).EndsWith(".AssemblyAttributes.cs"));
var cdis = mdReader.GetCustomDebugInformation(attrDocHandle);
Assert.Equal(expectedEmbeddedSources, cdis.Any(h => mdReader.GetGuid(mdReader.GetCustomDebugInformation(h).Kind) == s_embeddedSourceKindGuid));
}
}
private static TestAsset Multitarget(TestAsset testAsset, string targetFrameworks)
=> testAsset.WithProjectChanges(p =>
{
var tfmNode = p.Root.Descendants().Single(e => e.Name.LocalName == "TargetFramework");
tfmNode.Name = p.Root.Name.Namespace + "TargetFrameworks";
tfmNode.Value = targetFrameworks;
});
private static TestAsset WithProperties(TestAsset testAsset, params (string key, string value)[] properties)
=> testAsset.WithProjectChanges(p =>
{
var ns = p.Root.Name.Namespace;
var propertyGroup = p.Root.Descendants(ns + "PropertyGroup").First();
foreach (var (key, value) in properties)
{
propertyGroup.Add(new XElement(ns + key, value));
}
});
private static TestAsset WithItems(TestAsset testAsset, params (string key, XAttribute[] attributes)[] items)
=> testAsset.WithProjectChanges(p =>
{
var ns = p.Root.Name.Namespace;
var itemGroup = new XElement(ns + "ItemGroup");
p.Root.Add(itemGroup);
foreach (var (key, attributes) in items)
{
itemGroup.Add(new XElement(ns + key, (object[])attributes));
}
});
[Fact]
public void WithNoGitMetadata()
{
// We need to copy the test project to a directory outside of the SDK repo,
// otherwise we would find .git directory in the SDK repo root.
var testAsset = _testAssetsManager
.CopyTestAsset("SourceLinkTestApp", testDestinationDirectory: Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()))
.WithSource();
var buildCommand = new BuildCommand(testAsset);
buildCommand.Execute().Should().Pass();
var intermediateDir = buildCommand.GetIntermediateDirectory();
intermediateDir.Should().NotHaveFile("SourceLinkTestApp.sourcelink.json");
}
/// <summary>
/// When creating a new repository locally we want the build to work and not report warnings even before the remote is set.
/// </summary>
[RequiresMSBuildVersionFact("17.12.0")]
public void WithNoRemoteNoCommit()
{
var testAsset = _testAssetsManager
.CopyTestAsset("SourceLinkTestApp")
.WithSource();
CreateGitFiles(testAsset.Path, originUrl: null, commitSha: null);
var buildCommand = new BuildCommand(testAsset);
buildCommand.Execute().Should().NotHaveStdOutContaining("warning");
var intermediateDir = buildCommand.GetIntermediateDirectory();
intermediateDir.Should().NotHaveFile("SourceLinkTestApp.sourcelink.json");
}
/// <summary>
/// When creating a new repository locally we want the build to work and not report warnings even before the remote is set.
/// </summary>
[RequiresMSBuildVersionFact("17.12.0")]
public void WithNoRemote()
{
var testAsset = _testAssetsManager
.CopyTestAsset("SourceLinkTestApp")
.WithSource();
CreateGitFiles(testAsset.Path, originUrl: null);
var buildCommand = new BuildCommand(testAsset);
buildCommand.Execute().Should().NotHaveStdOutContaining("warning");
var intermediateDir = buildCommand.GetIntermediateDirectory();
intermediateDir.Should().NotHaveFile("SourceLinkTestApp.sourcelink.json");
}
[Fact]
public void WithRemoteOrigin_UnknownDomain()
{
var testAsset = _testAssetsManager
.CopyTestAsset("SourceLinkTestApp")
.WithSource();
CreateGitFiles(testAsset.Path, originUrl: "https://contoso.com");
var buildCommand = new BuildCommand(testAsset)
{
WorkingDirectory = testAsset.Path
};
buildCommand.Execute().Should().Pass();
var intermediateDir = buildCommand.GetIntermediateDirectory();
intermediateDir.Should().NotHaveFile("SourceLinkTestApp.sourcelink.json");
}
[Theory]
[InlineData("https://github.com/org/repo", "https://raw.githubusercontent.com/org/repo/1200000000000000000000000000000000000000/*", true)]
[InlineData("https://github.com/org/repo", "https://raw.githubusercontent.com/org/repo/1200000000000000000000000000000000000000/*", false)]
[InlineData("https://gitlab.com/org/repo", "https://gitlab.com/org/repo/-/raw/1200000000000000000000000000000000000000/*")]
[InlineData("https://bitbucket.org/org/repo", "https://api.bitbucket.org/2.0/repositories/org/repo/src/1200000000000000000000000000000000000000/*")]
[InlineData("https://test.visualstudio.com/org/_git/repo", "https://test.visualstudio.com/org/_apis/git/repositories/repo/items?api-version=1.0&versionType=commit&version=1200000000000000000000000000000000000000&path=/*")]
public void WithRemoteOrigin_KnownDomain(string origin, string expectedLink, bool multitarget = false)
{
string targetFrameworks = ToolsetInfo.CurrentTargetFramework + (multitarget ? ";netstandard2.0" : "");
var testAsset = _testAssetsManager
.CopyTestAsset("SourceLinkTestApp", identifier: origin + multitarget.ToString())
.WithSource();
if (multitarget)
{
testAsset = Multitarget(testAsset, targetFrameworks);
}
CreateGitFiles(testAsset.Path, origin);
var buildCommand = new BuildCommand(testAsset)
{
WorkingDirectory = testAsset.Path
};
buildCommand.Execute().Should().Pass();
foreach (var targetFramework in targetFrameworks.Split(';'))
{
var intermediateDir = buildCommand.GetIntermediateDirectory(targetFramework: targetFramework);
var sourceLinkFilePath = Path.Combine(intermediateDir.FullName, "SourceLinkTestApp.sourcelink.json");
var actualContent = File.ReadAllText(sourceLinkFilePath, Encoding.UTF8);
var expectedPattern = Path.Combine(testAsset.Path, "*").Replace("\\", "\\\\");
Assert.Equal($$$"""{"documents":{"{{{expectedPattern}}}":"{{{expectedLink}}}"}}""", actualContent);
ValidatePdb(Path.Combine(intermediateDir.FullName, "SourceLinkTestApp.pdb"), expectedEmbeddedSources: true);
}
// check that commit sha is included in the package nuspec:
var binDir = buildCommand.GetOutputDirectory(targetFramework: "");
using var nupkg = ZipFile.OpenRead(Path.Combine(binDir.FullName, "SourceLinkTestApp.1.0.0.nupkg"));
using var nuspec = nupkg.GetEntry("SourceLinkTestApp.nuspec").Open();
using var nuspecStream = new MemoryStream();
nuspec.CopyTo(nuspecStream);
var nuspecStr = Encoding.UTF8.GetString(nuspecStream.ToArray());
Assert.Contains(@"repository type=""git"" commit=""1200000000000000000000000000000000000000""", nuspecStr);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void SuppressImplicitGitSourceLink_SetExplicitly(bool multitarget)
{
string targetFrameworks = ToolsetInfo.CurrentTargetFramework + (multitarget ? ";netstandard2.0" : "");
var testAsset = _testAssetsManager
.CopyTestAsset("SourceLinkTestApp", identifier: multitarget.ToString())
.WithSource();
testAsset = WithProperties(testAsset, ("SuppressImplicitGitSourceLink", "true"));
if (multitarget)
{
testAsset = Multitarget(testAsset, targetFrameworks);
}
CreateGitFiles(testAsset.Path, "https://github.com/org/repo");
var buildCommand = new BuildCommand(testAsset)
{
WorkingDirectory = testAsset.Path
};
buildCommand.Execute().Should().Pass();
foreach (var targetFramework in targetFrameworks.Split(';'))
{
var intermediateDir = buildCommand.GetIntermediateDirectory(targetFramework: targetFramework);
intermediateDir.Should().NotHaveFile("SourceLinkTestApp.sourcelink.json");
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void SuppressImplicitGitSourceLink_ExplicitPackage(bool multitarget)
{
string targetFrameworks = ToolsetInfo.CurrentTargetFramework + (multitarget ? ";netstandard2.0" : "");
var testAsset = _testAssetsManager
.CopyTestAsset("SourceLinkTestApp", identifier: multitarget.ToString())
.WithSource();
if (multitarget)
{
testAsset = Multitarget(testAsset, targetFrameworks);
}
testAsset = WithItems(testAsset, ("PackageReference", new[] { new XAttribute("Include", "Microsoft.SourceLink.GitHub"), new XAttribute("Version", "1.0.0") }));
CreateGitFiles(testAsset.Path, "https://github.com/org/repo");
var buildCommand = new BuildCommand(testAsset)
{
WorkingDirectory = testAsset.Path
};
buildCommand.Execute().Should().Pass();
foreach (var targetFramework in targetFrameworks.Split(';'))
{
var intermediateDir = buildCommand.GetIntermediateDirectory(targetFramework: targetFramework);
intermediateDir.Should().HaveFile("SourceLinkTestApp.sourcelink.json");
// EmbedUntrackedSources is not set by default by SourceLink v1.0.0 package:
ValidatePdb(Path.Combine(intermediateDir.FullName, "SourceLinkTestApp.pdb"), expectedEmbeddedSources: false);
}
}
[FullMSBuildOnlyFact]
public void Cpp()
{
var testAsset = _testAssetsManager
.CopyTestAsset("NetCoreCsharpAppReferenceCppCliLib")
.WithSource();
var intDir = Path.Combine(testAsset.Path, "NETCoreCppCliTest", "IntDir");
testAsset = WithProperties(testAsset,
("EnableManagedPackageReferenceSupport", "true"),
("IntermediateOutputPath", intDir));
CreateGitFiles(testAsset.Path, "https://github.com/org/repo");
var buildCommand = new BuildCommand(testAsset, "NETCoreCppCliTest")
{
WorkingDirectory = testAsset.Path
};
buildCommand.Execute("-p:Platform=x64").Should().Pass();
var sourceLinkFilePath = Path.Combine(intDir, "NETCoreCppCliTest.sourcelink.json");
var actualContent = File.ReadAllText(sourceLinkFilePath, Encoding.UTF8);
var expectedPattern = Path.Combine(testAsset.Path, "*").Replace("\\", "\\\\");
var expectedSourceLink = $$$"""{"documents":{"{{{expectedPattern}}}":"https://raw.githubusercontent.com/org/repo/1200000000000000000000000000000000000000/*"}}""";
Assert.Equal(expectedSourceLink, actualContent);
var outputDir = Path.Combine(testAsset.Path, "NETCoreCppCliTest", "x64", "Debug");
var pdbText = File.ReadAllText(Path.Combine(outputDir, "NETCoreCppCliTest.pdb"), Encoding.UTF8);
Assert.Contains(expectedSourceLink, pdbText);
}
[FullMSBuildOnlyFact]
public void LegacyDesktopWpf()
{
var testAsset = _testAssetsManager
.CopyTestAsset("DesktopWpf")
.WithSource();
CreateGitFiles(testAsset.Path, "https://github.com/org/repo");
var buildCommand = new BuildCommand(testAsset, relativePathToProject: "FxWpf")
{
WorkingDirectory = Path.Combine(testAsset.Path, "FxWpf")
};
buildCommand.Execute().Should().Pass();
Assert.True(File.Exists(Path.Combine(testAsset.Path, "obj", "net472", "MainWindow.g.cs")));
}
}
}
|