diff --git a/src/Sarif/ExecutionEnvironment.cs b/src/Sarif/ExecutionEnvironment.cs
deleted file mode 100644
index f8090a513..000000000
--- a/src/Sarif/ExecutionEnvironment.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-
-using System;
-
-namespace Microsoft.CodeAnalysis.Sarif
-{
- ///
- /// A wrapper class for accessing the execution environment.
- ///
- ///
- /// Clients should use this class rather than directly using the System.Environment
- /// class, so they can mock the IExecutionEnvironment interface in unit tests.
- ///
- public class ExecutionEnvironment : IExecutionEnvironment
- {
- ///
- /// Replaces the name of each environment variable embedded in the specified string with
- /// the string equivalent of the value of the variable, then returns the resulting string.
- ///
- ///
- /// A string containing the names of zero or more environment variables. Each environment
- /// variable is quoted with the percent sign character (%).
- ///
- ///
- /// A string with each environment variable replaced by its value.
- ///
- public string ExpandEnvironmentVariables(string name) => Environment.ExpandEnvironmentVariables(name);
-
- ///
- /// Gets the fully qualified path of the current working directory.
- ///
- public string CurrentDirectory => Environment.CurrentDirectory;
- }
-}
diff --git a/src/Sarif/ExternalProcess.cs b/src/Sarif/ExternalProcess.cs
index e8d617610..dc742c820 100644
--- a/src/Sarif/ExternalProcess.cs
+++ b/src/Sarif/ExternalProcess.cs
@@ -17,7 +17,7 @@ public class ExternalProcess
public ExternalProcess(
string workingDirectory,
- string fileName,
+ string exePath,
string arguments,
IConsoleCapture stdOut = null,
int[] acceptableReturnCodes = null)
@@ -26,7 +26,7 @@ public ExternalProcess(
ProcessStartInfo psi = new ProcessStartInfo();
- psi.FileName = fileName;
+ psi.FileName = exePath;
psi.Arguments = arguments;
psi.WorkingDirectory = workingDirectory;
diff --git a/src/Sarif/GitHelper.cs b/src/Sarif/GitHelper.cs
new file mode 100644
index 000000000..647935d28
--- /dev/null
+++ b/src/Sarif/GitHelper.cs
@@ -0,0 +1,192 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+
+namespace Microsoft.CodeAnalysis.Sarif
+{
+ public class GitHelper : IDisposable
+ {
+ public static readonly GitHelper Default = new GitHelper();
+
+ public delegate string ProcessRunner(string workingDirectory, string exePath, string arguments);
+
+ private static readonly ProcessRunner DefaultProcessRunner =
+ (string workingDirectory, string exePath, string arguments)
+ => new ExternalProcess(workingDirectory, exePath, arguments, stdOut: null, acceptableReturnCodes: null).StdOut.Text;
+
+ private readonly IFileSystem fileSystem;
+ private readonly ProcessRunner processRunner;
+ private readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
+
+ internal static readonly string s_expectedGitExePath = Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\Git\cmd\git.exe");
+
+ // A cache that maps directory names to the root directory of the repository, if any, that
+ // contains them.
+ //
+ // The case-insensitive key comparison is correct on Windows systems and wrong on Linux/MacOS.
+ // This is a general problem in the SDK. See https://github.com/microsoft/sarif-sdk/issues/1736,
+ // "File path comparisons are not file-system-appropriate".
+ //
+ // The cache is internal rather than private so that tests can verify that the cache is
+ // being populated appropriately. It's not so easy to verify that it's being _used_
+ // appropriately.
+ internal readonly Dictionary directoryToRepoRootPathDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ public bool IsRepositoryRoot(string repositoryPath) => this.fileSystem.DirectoryExists(Path.Combine(repositoryPath, ".git"));
+
+ public GitHelper(IFileSystem fileSystem = null, ProcessRunner processRunner = null)
+ {
+ this.fileSystem = fileSystem ?? new FileSystem();
+ this.processRunner = processRunner ?? DefaultProcessRunner;
+
+ GitExePath = GetGitExePath();
+ }
+
+ public string GitExePath { get; }
+
+ public Uri GetRemoteUri(string repoPath)
+ {
+ string uriText = GetSimpleGitCommandOutput(
+ repoPath,
+ args: $"remote get-url origin");
+
+ return uriText == null
+ ? null
+ : new Uri(uriText, UriKind.Absolute);
+ }
+
+ public string GetCurrentCommit(string repoPath)
+ {
+ return GetSimpleGitCommandOutput(
+ repoPath,
+ args: "rev-parse --verify HEAD");
+ }
+
+ public void Checkout(string repoPath, string commitSha)
+ {
+ GetSimpleGitCommandOutput(
+ repoPath,
+ args: $"checkout {commitSha}");
+ }
+
+ private string GetGitExePath()
+ => this.fileSystem.FileExists(s_expectedGitExePath) ? s_expectedGitExePath : null;
+
+ public string GetCurrentBranch(string repoPath)
+ {
+ return GetSimpleGitCommandOutput(
+ repoPath,
+ args: "rev-parse --abbrev-ref HEAD");
+ }
+
+ private string GetSimpleGitCommandOutput(string repositoryPath, string args)
+ {
+ string gitPath = this.GitExePath;
+
+ if (gitPath == null || !IsRepositoryRoot(repositoryPath))
+ {
+ return null;
+ }
+
+ string stdOut = this.processRunner(
+ workingDirectory: repositoryPath,
+ exePath: gitPath,
+ arguments: args);
+
+ return TrimNewlines(stdOut);
+ }
+
+ private static string TrimNewlines(string text) => text
+ .Replace("\r", string.Empty)
+ .Replace("\n", string.Empty);
+
+ public string GetRepositoryRoot(string path, bool useCache = true)
+ {
+ // The "default" instance won't let you use the cache, to prevent independent users
+ // from interfering with each other.
+ if (useCache && object.ReferenceEquals(this, Default))
+ {
+ throw new ArgumentException(SdkResources.GitHelperDefaultInstanceDoesNotPermitCaching, nameof(useCache));
+ }
+
+ if (this.fileSystem.FileExists(path))
+ {
+ path = Path.GetDirectoryName(path);
+ }
+
+ string repoRootPath;
+ if (useCache)
+ {
+ cacheLock.EnterReadLock();
+ try
+ {
+ if (directoryToRepoRootPathDictionary.TryGetValue(path, out repoRootPath))
+ {
+ return repoRootPath;
+ }
+ }
+ finally
+ {
+ cacheLock.ExitReadLock();
+ }
+ }
+
+ repoRootPath = path;
+ while (!string.IsNullOrEmpty(repoRootPath) && !IsRepositoryRoot(repoRootPath))
+ {
+ repoRootPath = Path.GetDirectoryName(repoRootPath);
+ }
+
+ // It's important to terminate with a slash because the value returned from this method
+ // will be used to create an absolute URI on which MakeUriRelative will be called. For
+ // example, suppose this method returns @"C:\\dev\sarif-sdk\". The caller will use it
+ // to create an absolute URI (call it repoRootUri) "file:///C:/dev/sarif-sdk/". The
+ // caller will use this URI to "rebase" another URI (call it artifactUri) such as
+ // "file:///C:/dev/sarif-sdk/src/Sarif". The caller will do that by calling
+ // repoRootUri.MakeRelativeUri(artifactUri). It turns out that unless repoRootUri
+ // ends with a slash, this call will return "sarif-sdk/src/Sarif" rather than the
+ // expected (at least for me) "src/Sarif".
+ if (repoRootPath != null && !repoRootPath.EndsWith(@"\")) { repoRootPath += @"\"; }
+
+ if (useCache)
+ {
+ cacheLock.EnterWriteLock();
+ try
+ {
+ // Add whatever we found to the cache, even if it was null (in which case we now know
+ // that this path isn't under source control).
+ directoryToRepoRootPathDictionary.Add(path, repoRootPath);
+ }
+ finally
+ {
+ cacheLock.ExitWriteLock();
+ }
+ }
+
+ return repoRootPath;
+ }
+
+ ///
+ /// Dispose pattern as required by style
+ ///
+ /// Set to true to actually dispose
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ cacheLock.Dispose();
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Sarif/GitInformation.cs b/src/Sarif/GitInformation.cs
deleted file mode 100644
index 57aa301a6..000000000
--- a/src/Sarif/GitInformation.cs
+++ /dev/null
@@ -1,180 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-
-namespace Microsoft.CodeAnalysis.Sarif
-{
- internal class GitInformation : IDisposable
- {
- private readonly IFileSystem fileSystem;
- private readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
- private readonly Dictionary repoRoots = new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- public bool IsRepositoryRoot(string repositoryPath) => this.fileSystem.DirectoryExists(Path.Combine(repositoryPath, ".git"));
-
- public GitInformation(IFileSystem fileSystem = null)
- {
- this.fileSystem = fileSystem ?? new FileSystem();
- GitExePath = GetGitExePath();
- }
-
- public string GitExePath { get; }
-
- public Uri GetRemoteUri(string repoPath)
- {
- string uriText = GetSimpleGitCommandOutput(
- repoPath,
- args: $"remote get-url origin");
-
- return uriText == null
- ? null
- : new Uri(uriText, UriKind.Absolute);
- }
-
- public string GetCurrentCommit(string repoPath)
- {
- return GetSimpleGitCommandOutput(
- repoPath,
- args: "rev-parse --verify HEAD");
- }
-
- public void Checkout(string repoPath, string commitSha)
- {
- GetSimpleGitCommandOutput(
- repoPath,
- args: $"checkout {commitSha}");
- }
-
- private string GetGitExePath()
- {
- string gitPath = Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\Git\cmd\git.exe");
- return (!string.IsNullOrEmpty(gitPath) && this.fileSystem.FileExists(gitPath)) ? gitPath : null;
- }
-
- public string GetCurrentBranch(string repoPath)
- {
- return GetSimpleGitCommandOutput(
- repoPath,
- args: "rev-parse --abbrev-ref HEAD");
- }
-
- private string GetSimpleGitCommandOutput(string repositoryPath, string args)
- {
- string gitPath = this.GitExePath;
-
- if (gitPath == null || !IsRepositoryRoot(repositoryPath))
- {
- return null;
- }
-
- var gitCommand = new ExternalProcess(
- workingDirectory: repositoryPath,
- fileName: gitPath,
- arguments: args);
-
- return TrimNewlines(gitCommand.StdOut.Text);
- }
-
- private static string TrimNewlines(string text) => text
- .Replace("\r", string.Empty)
- .Replace("\n", string.Empty);
-
- public VersionControlDetails GetVersionControlDetails(string repositoryPath, bool crawlParentDirectories)
- {
- VersionControlDetails value;
-
- cacheLock.EnterReadLock();
- try
- {
- if (!crawlParentDirectories)
- {
- if (repoRoots.TryGetValue(repositoryPath, out value))
- {
- return value;
- }
- }
- else
- {
- foreach (KeyValuePair kp in repoRoots)
- {
- if (repositoryPath.StartsWith(kp.Key, StringComparison.OrdinalIgnoreCase) && ((repositoryPath.Length == kp.Key.Length) || (repositoryPath[kp.Key.Length] == '\\')))
- {
- return kp.Value;
- }
- }
- }
- }
- finally
- {
- cacheLock.ExitReadLock();
- }
-
- repositoryPath = GetRepositoryRoot(repositoryPath);
-
- if (string.IsNullOrEmpty(repositoryPath))
- {
- return null;
- }
-
- Uri repoRemoteUri = GetRemoteUri(repositoryPath);
- value = (repoRemoteUri is null)
- ? null
- : new VersionControlDetails
- {
- RepositoryUri = repoRemoteUri,
- RevisionId = GetCurrentCommit(repositoryPath),
- Branch = GetCurrentBranch(repositoryPath),
- MappedTo = new ArtifactLocation { Uri = new Uri(repositoryPath, UriKind.Absolute) },
- };
-
- cacheLock.EnterWriteLock();
- try
- {
- if (!repoRoots.ContainsKey(repositoryPath))
- {
- repoRoots.Add(repositoryPath, value);
- }
- }
- finally
- {
- cacheLock.ExitWriteLock();
- }
-
- return value;
- }
-
- // Internal rather than private for unit-testability.
- internal string GetRepositoryRoot(string path)
- {
- while (!string.IsNullOrEmpty(path) && !IsRepositoryRoot(path))
- {
- path = Path.GetDirectoryName(path);
- }
-
- return path;
- }
-
- ///
- /// Dispose pattern as required by style
- ///
- /// Set to true to actually dispose
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- cacheLock.Dispose();
- }
- }
-
- ///
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
- }
-}
diff --git a/src/Sarif/IExecutionEnvironment.cs b/src/Sarif/IExecutionEnvironment.cs
deleted file mode 100644
index 3ebe9867b..000000000
--- a/src/Sarif/IExecutionEnvironment.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-
-namespace Microsoft.CodeAnalysis.Sarif
-{
- ///
- /// An interface for accessing the execution environment.
- ///
- ///
- /// Clients wishing to access aspects of the execution environment such as environment
- /// variables should instantiate an ExecutionEnvironment object rather than directly using
- /// the System.Environment class, so they can mock the IExecutionEnvironment interface in
- /// unit tests.
- ///
- public interface IExecutionEnvironment
- {
- ///
- /// Replaces the name of each environment variable embedded in the specified string with
- /// the string equivalent of the value of the variable, then returns the resulting string.
- ///
- ///
- /// A string containing the names of zero or more environment variables. Each environment
- /// variable is quoted with the percent sign character (%).
- ///
- ///
- /// A string with each environment variable replaced by its value.
- ///
- string ExpandEnvironmentVariables(string name);
-
- ///
- /// Gets the fully qualified path of the current working directory.
- ///
- string CurrentDirectory { get; }
- }
-}
diff --git a/src/Sarif/SdkResources.Designer.cs b/src/Sarif/SdkResources.Designer.cs
index 9f8d39b53..a6d9298f2 100644
--- a/src/Sarif/SdkResources.Designer.cs
+++ b/src/Sarif/SdkResources.Designer.cs
@@ -249,6 +249,15 @@ public static string ERR999_UnhandledEngineException {
}
}
+ ///
+ /// Looks up a localized string similar to When using the static 'Default' instance of GitHelper, you must pass the argument useCache: false to GetRepositoryRoot, which degrades performance. If the performance is not acceptable, create a separate GitHelper instance. You need not pass useCache: true because that is the default..
+ ///
+ public static string GitHelperDefaultInstanceDoesNotPermitCaching {
+ get {
+ return ResourceManager.GetString("GitHelperDefaultInstanceDoesNotPermitCaching", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Element expected to be located under a different parent element..
///
diff --git a/src/Sarif/SdkResources.resx b/src/Sarif/SdkResources.resx
index 1f87d639d..a6bfa828b 100644
--- a/src/Sarif/SdkResources.resx
+++ b/src/Sarif/SdkResources.resx
@@ -252,4 +252,7 @@
Cannot provide version control information because the current directory '{0}' is not under a Git repository root directory.
+
+ When using the static 'Default' instance of GitHelper, you must pass the argument useCache: false to GetRepositoryRoot, which degrades performance. If the performance is not acceptable, create a separate GitHelper instance. You need not pass useCache: true because that is the default.
+
\ No newline at end of file
diff --git a/src/Sarif/Visitors/InsertOptionalDataVisitor.cs b/src/Sarif/Visitors/InsertOptionalDataVisitor.cs
index c833baa2a..eb5356fe0 100644
--- a/src/Sarif/Visitors/InsertOptionalDataVisitor.cs
+++ b/src/Sarif/Visitors/InsertOptionalDataVisitor.cs
@@ -10,10 +10,13 @@ namespace Microsoft.CodeAnalysis.Sarif.Visitors
{
public class InsertOptionalDataVisitor : SarifRewritingVisitor
{
- internal IFileSystem s_fileSystem = new FileSystem();
- internal IExecutionEnvironment s_executionEnvironment = new ExecutionEnvironment();
+ private readonly IFileSystem _fileSystem;
+ private readonly GitHelper.ProcessRunner _processRunner;
private Run _run;
+ private HashSet _repoRootUris;
+ private GitHelper _gitHelper;
+
private int _ruleIndex;
private FileRegionsCache _fileRegionsCache;
private readonly OptionallyEmittedData _dataToInsert;
@@ -25,8 +28,15 @@ public InsertOptionalDataVisitor(OptionallyEmittedData dataToInsert, Run run)
_run = run ?? throw new ArgumentNullException(nameof(run));
}
- public InsertOptionalDataVisitor(OptionallyEmittedData dataToInsert, IDictionary originalUriBaseIds = null)
+ public InsertOptionalDataVisitor(
+ OptionallyEmittedData dataToInsert,
+ IDictionary originalUriBaseIds = null,
+ IFileSystem fileSystem = null,
+ GitHelper.ProcessRunner processRunner = null)
{
+ _fileSystem = fileSystem ?? new FileSystem();
+ _processRunner = processRunner;
+
_dataToInsert = dataToInsert;
_originalUriBaseIds = originalUriBaseIds;
_ruleIndex = -1;
@@ -35,6 +45,8 @@ public InsertOptionalDataVisitor(OptionallyEmittedData dataToInsert, IDictionary
public override Run VisitRun(Run node)
{
_run = node;
+ _gitHelper = new GitHelper(_fileSystem, _processRunner);
+ _repoRootUris = new HashSet();
if (_originalUriBaseIds != null)
{
@@ -46,11 +58,6 @@ public override Run VisitRun(Run node)
}
}
- if (_run.VersionControlProvenance == null && _dataToInsert.HasFlag(OptionallyEmittedData.VersionControlInformation))
- {
- InsertVersionControlInformation();
- }
-
if (node == null) { return null; }
bool scrapeFileReferences = _dataToInsert.HasFlag(OptionallyEmittedData.Hashes) ||
@@ -65,29 +72,13 @@ public override Run VisitRun(Run node)
Run visited = base.VisitRun(node);
- return visited;
- }
-
- private void InsertVersionControlInformation()
- {
- var gitInformation = new GitInformation(s_fileSystem);
- string currentDirectory = s_executionEnvironment.CurrentDirectory;
-
- VersionControlDetails versionControlDetails =
- gitInformation.GetVersionControlDetails(currentDirectory, crawlParentDirectories: true);
- if (versionControlDetails == null)
+ // After all the ArtifactLocations have been visited,
+ if (_run.VersionControlProvenance == null && _dataToInsert.HasFlag(OptionallyEmittedData.VersionControlInformation))
{
- throw new InvalidOperationException(
- string.Format(
- CultureInfo.CurrentCulture,
- SdkResources.CannotProvideVersionControlInformation,
- currentDirectory));
+ _run.VersionControlProvenance = CreateVersionControlProvenance();
}
- _run.VersionControlProvenance = new List
- {
- versionControlDetails
- };
+ return visited;
}
public override PhysicalLocation VisitPhysicalLocation(PhysicalLocation node)
@@ -195,6 +186,16 @@ public override Artifact VisitArtifact(Artifact node)
return base.VisitArtifact(node);
}
+ public override ArtifactLocation VisitArtifactLocation(ArtifactLocation node)
+ {
+ if (_dataToInsert.HasFlag(OptionallyEmittedData.VersionControlInformation))
+ {
+ node = ExpressRelativeToRepoRoot(node);
+ }
+
+ return base.VisitArtifactLocation(node);
+ }
+
public override Result VisitResult(Result node)
{
_ruleIndex = node.RuleIndex;
@@ -236,5 +237,111 @@ public override Message VisitMessage(Message node)
}
return base.VisitMessage(node);
}
+
+ private List CreateVersionControlProvenance()
+ {
+ var versionControlProvenance = new List();
+
+ foreach (Uri repoRootUri in _repoRootUris)
+ {
+ string repoRootPath = repoRootUri.LocalPath;
+ Uri repoRemoteUri = _gitHelper.GetRemoteUri(repoRootPath);
+ if (repoRemoteUri != null)
+ {
+ versionControlProvenance.Add(
+ new VersionControlDetails
+ {
+ RepositoryUri = repoRemoteUri,
+ RevisionId = _gitHelper.GetCurrentCommit(repoRootPath),
+ Branch = _gitHelper.GetCurrentBranch(repoRootPath),
+ MappedTo = new ArtifactLocation { Uri = repoRootUri }
+ });
+ }
+ }
+
+ return versionControlProvenance;
+ }
+
+ private ArtifactLocation ExpressRelativeToRepoRoot(ArtifactLocation node)
+ {
+ Uri uri = node.Uri;
+ if (uri == null && node.Index >= 0 && _run.Artifacts?.Count > node.Index)
+ {
+ uri = _run.Artifacts[node.Index].Location.Uri;
+ }
+
+ if (uri != null && uri.IsAbsoluteUri && uri.IsFile)
+ {
+ string repoRootPath = _gitHelper.GetRepositoryRoot(uri.LocalPath);
+ if (repoRootPath != null)
+ {
+ var repoRootUri = new Uri(repoRootPath, UriKind.Absolute);
+ _repoRootUris.Add(repoRootUri);
+
+ Uri repoRelativeUri = repoRootUri.MakeRelativeUri(uri);
+ node.Uri = repoRelativeUri;
+ node.UriBaseId = GetUriBaseIdForRepoRoot(repoRootUri);
+ }
+ }
+
+ return node;
+ }
+
+ private string GetUriBaseIdForRepoRoot(Uri repoRootUri)
+ {
+ // Is there already an entry in OriginalUriBaseIds for this repo?
+ if (_run.OriginalUriBaseIds != null)
+ {
+ foreach (KeyValuePair pair in _run.OriginalUriBaseIds)
+ {
+ if (pair.Value.Uri == repoRootUri)
+ {
+ // Yes, so return it.
+ return pair.Key;
+ }
+ }
+ }
+
+ // No, so add one.
+ if (_run.OriginalUriBaseIds == null)
+ {
+ _run.OriginalUriBaseIds = new Dictionary();
+ }
+
+ string uriBaseId = GetNextRepoRootUriBaseId();
+ _run.OriginalUriBaseIds.Add(
+ uriBaseId,
+ new ArtifactLocation
+ {
+ Uri = repoRootUri
+ });
+
+ return uriBaseId;
+ }
+
+ private string GetNextRepoRootUriBaseId()
+ {
+ ICollection originalUriBaseIdSymbols = _run.OriginalUriBaseIds.Keys;
+
+ for (int i = 0; ; i++)
+ {
+ string uriBaseId = GetUriBaseId(i);
+ if (!originalUriBaseIdSymbols.Contains(uriBaseId))
+ {
+ return uriBaseId;
+ }
+ }
+ }
+
+ private const string RepoRootUriBaseIdStem = "REPO_ROOT";
+
+ // When there is only one repo root (the usual case), the uriBaseId is "REPO_ROOT" (unless
+ // that symbol is already in use in originalUriBaseIds. The second and subsequent uriBaseIds
+ // are REPO_ROOT_2, _3, etc. (again, skipping over any that are in use). We never assign
+ // REPO_ROOT_1 (although of course it might exist in originalUriBaseIds).
+ internal static string GetUriBaseId(int i)
+ => i == 0
+ ? RepoRootUriBaseIdStem
+ : $"{RepoRootUriBaseIdStem}_{i + 1}";
}
}
diff --git a/src/Test.FunctionalTests.Sarif/GitInformationTests.cs b/src/Test.FunctionalTests.Sarif/GitInformationTests.cs
deleted file mode 100644
index 0c200538a..000000000
--- a/src/Test.FunctionalTests.Sarif/GitInformationTests.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-
-using System;
-using FluentAssertions;
-using Xunit;
-
-namespace Microsoft.CodeAnalysis.Sarif
-{
- public class GitInformationTests
- {
- [Fact]
- public void GetVersionControlDetails_ReturnsExpectedInformation()
- {
- var gitInformation = new GitInformation();
-
- // This test assumes that the directory in which the tests are run lies under the local
- // repo root, for example, \bld\bin\AnyCPU_Release\Test.FunctionalTests.Sarif\netcoreapp2.1.
- string localRepoRoot = gitInformation.GetRepositoryRoot(Environment.CurrentDirectory);
-
- VersionControlDetails versionControlDetails =
- gitInformation.GetVersionControlDetails(Environment.CurrentDirectory, crawlParentDirectories: true);
-
- // We don't check for "microsoft/sarif-sdk" so that the test will pass in forks of the
- // original repo.
- versionControlDetails.RepositoryUri.OriginalString
- .Should().StartWith("https://github.com/")
- .And.EndWith("/sarif-sdk");
-
- versionControlDetails.MappedTo.Uri.OriginalString.Should().Be(localRepoRoot);
- versionControlDetails.MappedTo.UriBaseId.Should().BeNull();
- }
- }
-}
diff --git a/src/Test.UnitTests.Sarif/GitHelperTests.cs b/src/Test.UnitTests.Sarif/GitHelperTests.cs
new file mode 100644
index 000000000..fe02d3a63
--- /dev/null
+++ b/src/Test.UnitTests.Sarif/GitHelperTests.cs
@@ -0,0 +1,121 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using FluentAssertions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.CodeAnalysis.Sarif
+{
+ public class GitHelperTests
+ {
+ [Fact]
+ public void GetRepositoryRoot_WhenDotGitIsAbsent_ReturnsNull()
+ {
+ var mockFileSystem = new Mock();
+ mockFileSystem.Setup(x => x.DirectoryExists(It.IsAny())).Returns(false);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\src")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\src\Sarif")).Returns(true);
+
+ var gitHelper = new GitHelper(mockFileSystem.Object);
+
+ gitHelper.GetRepositoryRoot(@"C:\dev\sarif-sdk\src\Sarif").Should().BeNull();
+ }
+
+ [Fact]
+ public void GetRepositoryRoot_WhenDotGitIsPresent_ReturnsTheDirectortyContainingDotGit()
+ {
+ var mockFileSystem = new Mock();
+ mockFileSystem.Setup(x => x.DirectoryExists(It.IsAny())).Returns(false);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\.git")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\src")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\src\Sarif")).Returns(true);
+
+ var gitHelper = new GitHelper(mockFileSystem.Object);
+
+ gitHelper.GetRepositoryRoot(@"C:\dev\sarif-sdk\src\Sarif").Should().Be(@"C:\dev\sarif-sdk\");
+ }
+
+ [Fact]
+ public void GetRepositoryRoot_ByDefault_PopulatesTheDirectoryToRepoRootCache()
+ {
+ var mockFileSystem = new Mock();
+
+ mockFileSystem.Setup(x => x.DirectoryExists(It.IsAny())).Returns(false);
+
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\.git")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\src")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\src\Sarif")).Returns(true);
+
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\docs")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\docs\public")).Returns(true);
+
+ var gitHelper = new GitHelper(mockFileSystem.Object);
+
+ string topLevelDirectoryRoot = gitHelper.GetRepositoryRoot(@"C:\dev\sarif-sdk");
+ string topLevelDirectoryRootAgain = gitHelper.GetRepositoryRoot(@"C:\dev\sarif-sdk");
+ string subdirectoryRoot = gitHelper.GetRepositoryRoot(@"C:\dev\sarif-sdk\src\Sarif");
+ string nonSourceControlledRoot = gitHelper.GetRepositoryRoot(@"C:\docs\public");
+
+ // Verify that the API returns the correct results whether or not the cache is in use.
+ topLevelDirectoryRoot.Should().Be(@"C:\dev\sarif-sdk\");
+ topLevelDirectoryRootAgain.Should().Be(topLevelDirectoryRoot);
+ subdirectoryRoot.Should().Be(topLevelDirectoryRoot);
+ nonSourceControlledRoot.Should().BeNull();
+
+ gitHelper.directoryToRepoRootPathDictionary.Count.Should().Be(3);
+ gitHelper.directoryToRepoRootPathDictionary[@"C:\dev\sarif-sdk\src\Sarif"].Should().Be(@"C:\dev\sarif-sdk\");
+ gitHelper.directoryToRepoRootPathDictionary[@"C:\dev\sarif-sdk"].Should().Be(@"C:\dev\sarif-sdk\");
+ gitHelper.directoryToRepoRootPathDictionary[@"C:\docs\public"].Should().BeNull();
+ }
+
+ [Fact]
+ public void GetRepositoryRoot_WhenCachingIsDisabled_DoesNotPopulateTheDirectoryToRepoRootCache()
+ {
+ var mockFileSystem = new Mock();
+
+ mockFileSystem.Setup(x => x.DirectoryExists(It.IsAny())).Returns(false);
+
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\.git")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\src")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\src\Sarif")).Returns(true);
+
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\docs")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@"C:\docs\public")).Returns(true);
+
+ var gitHelper = new GitHelper(mockFileSystem.Object);
+
+ // Verify that the API returns the correct results whether or not the cache is in use.
+ string topLevelDirectoryRoot = gitHelper.GetRepositoryRoot(@"C:\dev\sarif-sdk", useCache: false);
+ string topLevelDirectoryRootAgain = gitHelper.GetRepositoryRoot(@"C:\dev\sarif-sdk", useCache: false);
+ string subdirectoryRoot = gitHelper.GetRepositoryRoot(@"C:\dev\sarif-sdk\src\Sarif", useCache: false);
+ string nonSourceControlledRoot = gitHelper.GetRepositoryRoot(@"C:\docs\public", useCache: false);
+
+ gitHelper.directoryToRepoRootPathDictionary.Count.Should().Be(0);
+ }
+
+ [Fact]
+ public void GetRepositoryRoot_WhenCalledOnTheDefaultInstanceWithNoParameters_Throws()
+ {
+ Action action = () => GitHelper.Default.GetRepositoryRoot(@"C:\dev");
+
+ action.Should().Throw().WithMessage($"{SdkResources.GitHelperDefaultInstanceDoesNotPermitCaching}*");
+ }
+
+ [Fact]
+ public void GetRepositoryRoot_WhenCalledOnTheDefaultInstanceWithCachingDisabled_DoesNotThrow()
+ {
+ Action action = () => GitHelper.Default.GetRepositoryRoot(@"C:\dev", useCache: false);
+
+ action.Should().NotThrow();
+ }
+ }
+}
diff --git a/src/Test.UnitTests.Sarif/GitInformationTests.cs b/src/Test.UnitTests.Sarif/GitInformationTests.cs
deleted file mode 100644
index 576f4a809..000000000
--- a/src/Test.UnitTests.Sarif/GitInformationTests.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-
-using FluentAssertions;
-using Moq;
-using Xunit;
-
-namespace Microsoft.CodeAnalysis.Sarif
-{
- public class GitInformationTests
- {
- [Fact]
- public void GetRepositoryRoot_WhenDotGitIsAbsent_ReturnsNull()
- {
- var mockFileSystem = new Mock();
- mockFileSystem.Setup(x => x.DirectoryExists(It.IsAny())).Returns(false);
- mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev")).Returns(true);
- mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk")).Returns(true);
- mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\src")).Returns(true);
- mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\src\Sarif")).Returns(true);
-
- var gitInformation = new GitInformation(mockFileSystem.Object);
-
- gitInformation.GetRepositoryRoot(@"C:\dev\sarif-sdk\src\Sarif").Should().BeNull();
- }
-
- [Fact]
- public void GetRepositoryRoot_WhenDotGitIsPresent_ReturnsTheDirectortyContainingDotGit()
- {
- var mockFileSystem = new Mock();
- mockFileSystem.Setup(x => x.DirectoryExists(It.IsAny())).Returns(false);
- mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk")).Returns(true);
- mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\.git")).Returns(true);
- mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\src")).Returns(true);
- mockFileSystem.Setup(x => x.DirectoryExists(@"C:\dev\sarif-sdk\src\Sarif")).Returns(true);
-
- var gitInformation = new GitInformation(mockFileSystem.Object);
-
- gitInformation.GetRepositoryRoot(@"C:\dev\sarif-sdk\src\Sarif").Should().Be(@"C:\dev\sarif-sdk");
- }
- }
-}
diff --git a/src/Test.UnitTests.Sarif/TestData/InsertOptionalDataVisitor/ExpectedOutputs/CoreTests-Relative_All.sarif b/src/Test.UnitTests.Sarif/TestData/InsertOptionalDataVisitor/ExpectedOutputs/CoreTests-Relative_All.sarif
index 51571765b..777cd34a0 100644
--- a/src/Test.UnitTests.Sarif/TestData/InsertOptionalDataVisitor/ExpectedOutputs/CoreTests-Relative_All.sarif
+++ b/src/Test.UnitTests.Sarif/TestData/InsertOptionalDataVisitor/ExpectedOutputs/CoreTests-Relative_All.sarif
@@ -68,14 +68,16 @@
{
"repositoryUri": "https://REMOTE_URI",
"mappedTo": {
- "uri": "file:///REPLACED_AT_TEST_RUNTIME",
- "index": 1
+ "uri": "file:///REPLACED_AT_TEST_RUNTIME"
}
}
],
"originalUriBaseIds": {
"TESTROOT": {
"uri": "file:///REPLACED_AT_TEST_RUNTIME/src/Test.UnitTests.Sarif/TestData/"
+ },
+ "REPO_ROOT": {
+ "uri": "file:///REPLACED_AT_TEST_RUNTIME"
}
},
"artifacts": [
@@ -92,11 +94,6 @@
"sha-1": "91655EA8262D81C262A8687E9667AEFF7432906A",
"sha-256": "1BDE85DC91168DAD541E776BB0437AC8A22D2959351A0640F2757D72AEE60C8A"
}
- },
- {
- "location": {
- "uri": "file:///REPLACED_AT_TEST_RUNTIME"
- }
}
],
"results": [
diff --git a/src/Test.UnitTests.Sarif/TestData/InsertOptionalDataVisitor/ExpectedOutputs/CoreTests-Relative_VersionControlInformation.sarif b/src/Test.UnitTests.Sarif/TestData/InsertOptionalDataVisitor/ExpectedOutputs/CoreTests-Relative_VersionControlInformation.sarif
index 1a046ab30..aa8c206c4 100644
--- a/src/Test.UnitTests.Sarif/TestData/InsertOptionalDataVisitor/ExpectedOutputs/CoreTests-Relative_VersionControlInformation.sarif
+++ b/src/Test.UnitTests.Sarif/TestData/InsertOptionalDataVisitor/ExpectedOutputs/CoreTests-Relative_VersionControlInformation.sarif
@@ -73,6 +73,9 @@
"originalUriBaseIds": {
"TESTROOT": {
"uri": "file:///REPLACED_AT_TEST_RUNTIME/src/Test.UnitTests.Sarif/TestData/"
+ },
+ "REPO_ROOT": {
+ "uri": "file:///REPLACED_AT_TEST_RUNTIME"
}
},
"artifacts": [
diff --git a/src/Test.UnitTests.Sarif/Visitors/InsertOptionalDataVisitorTests.cs b/src/Test.UnitTests.Sarif/Visitors/InsertOptionalDataVisitorTests.cs
index 330122772..265f31c68 100644
--- a/src/Test.UnitTests.Sarif/Visitors/InsertOptionalDataVisitorTests.cs
+++ b/src/Test.UnitTests.Sarif/Visitors/InsertOptionalDataVisitorTests.cs
@@ -10,6 +10,7 @@
using Microsoft.CodeAnalysis.Sarif.Writers;
using Microsoft.CodeAnalysis.Test.Utilities.Sarif;
+using Moq;
using Newtonsoft.Json;
using Xunit;
using Xunit.Abstractions;
@@ -46,6 +47,7 @@ protected override string ConstructTestOutputFromInputResource(string inputResou
string enlistmentRoot = currentDirectory
.Substring(0, currentDirectory.IndexOf(@"\bld\"))
.Replace('\\', '/');
+ if (!enlistmentRoot.EndsWith("/")) { enlistmentRoot += "/"; }
if (inputResourceName == "Inputs.CoreTests-Relative.sarif")
{
@@ -59,8 +61,20 @@ protected override string ConstructTestOutputFromInputResource(string inputResou
var visitor = new InsertOptionalDataVisitor(_currentOptionallyEmittedData);
visitor.Visit(actualLog.Runs[0]);
- // Restore the remanufactured URI so that file diffing matches
+ // Restore the remanufactured URI so that file diffing succeeds.
actualLog.Runs[0].OriginalUriBaseIds["TESTROOT"] = new ArtifactLocation { Uri = originalUri };
+
+ // In some of the tests, the visitor added an originalUriBaseId for the repo root.
+ // Adjust that one, too.
+ string repoRootUriBaseId = InsertOptionalDataVisitor.GetUriBaseId(0);
+ if (actualLog.Runs[0].OriginalUriBaseIds.TryGetValue(repoRootUriBaseId, out ArtifactLocation artifactLocation))
+ {
+ Uri repoRootUri = artifactLocation.Uri;
+ string repoRootString = repoRootUri.ToString();
+ repoRootString = repoRootString.Replace(enlistmentRoot, EnlistmentRoot);
+
+ actualLog.Runs[0].OriginalUriBaseIds[repoRootUriBaseId] = new ArtifactLocation { Uri = new Uri(repoRootString, UriKind.Absolute) };
+ }
}
else if (inputResourceName == "Inputs.CoreTests-Absolute.sarif")
{
@@ -116,8 +130,8 @@ protected override string ConstructTestOutputFromInputResource(string inputResou
}
// Verify and replace the remote repo URI, because it would be different in a fork.
- var gitInformation = new GitInformation();
- Uri remoteUri = gitInformation.GetRemoteUri(enlistmentRoot);
+ var gitHelper = new GitHelper();
+ Uri remoteUri = gitHelper.GetRemoteUri(enlistmentRoot);
versionControlDetails.RepositoryUri.Should().Be(remoteUri);
versionControlDetails.RepositoryUri = new Uri("https://REMOTE_URI");
@@ -224,6 +238,117 @@ public void InsertOptionalDataVisitor_ContextRegionSnippets_DoesNotFail_TopLevel
OptionallyEmittedData.ContextRegionSnippets);
}
+ [Fact]
+ public void InsertOptionalDataVisitor_SkipsExistingRepoRootSymbolsAndHandlesMultipleRoots()
+ {
+ const string ParentRepoRoot = @"C:\repo\";
+ const string ParentRepoBranch = "users/mary/my-feature";
+ const string ParentRepoCommit = "11111";
+
+ const string SubmoduleRepoRoot = @"C:\repo\submodule\";
+ const string SubmoduleBranch = "main";
+ const string SubmoduleCommit = "22222";
+
+ var mockFileSystem = new Mock();
+
+ mockFileSystem.Setup(x => x.DirectoryExists(It.IsAny())).Returns(false);
+ mockFileSystem.Setup(x => x.FileExists(It.IsAny())).Returns(false);
+
+ mockFileSystem.Setup(x => x.DirectoryExists(@$"{ParentRepoRoot}.git")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@$"C:\repo")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@$"{ParentRepoRoot}src")).Returns(true);
+ mockFileSystem.Setup(x => x.FileExists(@$"{ParentRepoRoot}src\File.cs")).Returns(true);
+
+ mockFileSystem.Setup(x => x.DirectoryExists($@"{SubmoduleRepoRoot}.git")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@$"C:\repo\submodule")).Returns(true);
+ mockFileSystem.Setup(x => x.DirectoryExists(@$"{SubmoduleRepoRoot}src")).Returns(true);
+ mockFileSystem.Setup(x => x.FileExists(@$"{SubmoduleRepoRoot}src\File2.cs")).Returns(true);
+
+ mockFileSystem.Setup(x => x.FileExists(GitHelper.s_expectedGitExePath)).Returns(true);
+
+ var mockProcessRunner = new Mock();
+
+ mockProcessRunner.Setup(x => x(ParentRepoRoot, GitHelper.s_expectedGitExePath, "remote get-url origin")).Returns(ParentRepoRoot);
+ mockProcessRunner.Setup(x => x(ParentRepoRoot, GitHelper.s_expectedGitExePath, "rev-parse --abbrev-ref HEAD")).Returns(ParentRepoBranch);
+ mockProcessRunner.Setup(x => x(ParentRepoRoot, GitHelper.s_expectedGitExePath, "rev-parse --verify HEAD")).Returns(ParentRepoCommit);
+
+ mockProcessRunner.Setup(x => x(SubmoduleRepoRoot, GitHelper.s_expectedGitExePath, "remote get-url origin")).Returns(SubmoduleRepoRoot);
+ mockProcessRunner.Setup(x => x(SubmoduleRepoRoot, GitHelper.s_expectedGitExePath, "rev-parse --abbrev-ref HEAD")).Returns(SubmoduleBranch);
+ mockProcessRunner.Setup(x => x(SubmoduleRepoRoot, GitHelper.s_expectedGitExePath, "rev-parse --verify HEAD")).Returns(SubmoduleCommit);
+
+ var run = new Run
+ {
+ OriginalUriBaseIds = new Dictionary
+ {
+ // Called "REPO_ROOT" but doesn't actually point to a repo.
+ ["REPO_ROOT"] = new ArtifactLocation
+ {
+ Uri = new Uri(@"C:\dir1\dir2", UriKind.Absolute)
+ }
+ },
+ Results = new List
+ {
+ new Result
+ {
+ Locations = new List
+ {
+ new Location
+ {
+ PhysicalLocation = new PhysicalLocation
+ {
+ ArtifactLocation = new ArtifactLocation
+ {
+ // The visitor will encounter this file and notice that it
+ // is under a repo root. It will invent a uriBaseId symbol
+ // for this repo. Since REPO_ROOT is already taken, it will
+ // choose REPO_ROOT_2
+ Uri = new Uri(@$"{ParentRepoRoot}src\File.cs", UriKind.Absolute)
+ }
+ }
+ },
+ new Location
+ {
+ PhysicalLocation = new PhysicalLocation
+ {
+ ArtifactLocation = new ArtifactLocation
+ {
+ Uri = new Uri(@$"{SubmoduleRepoRoot}src\File2.cs", UriKind.Absolute)
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+
+ var visitor = new InsertOptionalDataVisitor(
+ OptionallyEmittedData.VersionControlInformation,
+ originalUriBaseIds: null,
+ fileSystem: mockFileSystem.Object,
+ processRunner: mockProcessRunner.Object);
+ visitor.Visit(run);
+
+ run.VersionControlProvenance[0].MappedTo.Uri.LocalPath.Should().Be(ParentRepoRoot);
+ run.VersionControlProvenance[0].Branch.Should().Be(ParentRepoBranch);
+ run.VersionControlProvenance[0].RevisionId.Should().Be(ParentRepoCommit);
+
+ run.VersionControlProvenance[1].MappedTo.Uri.LocalPath.Should().Be(SubmoduleRepoRoot);
+ run.VersionControlProvenance[1].Branch.Should().Be(SubmoduleBranch);
+ run.VersionControlProvenance[1].RevisionId.Should().Be(SubmoduleCommit);
+
+ run.OriginalUriBaseIds["REPO_ROOT_2"].Uri.LocalPath.Should().Be(ParentRepoRoot);
+
+ IList resultLocations = run.Results[0].Locations;
+
+ ArtifactLocation resultArtifactLocation = resultLocations[0].PhysicalLocation.ArtifactLocation;
+ resultArtifactLocation.Uri.OriginalString.Should().Be("src/File.cs");
+ resultArtifactLocation.UriBaseId.Should().Be("REPO_ROOT_2");
+
+ resultArtifactLocation = resultLocations[1].PhysicalLocation.ArtifactLocation;
+ resultArtifactLocation.Uri.OriginalString.Should().Be("src/File2.cs");
+ resultArtifactLocation.UriBaseId.Should().Be("REPO_ROOT_3");
+ }
+
[Fact]
public void InsertOptionalDataVisitor_CanVisitIndividualResultsInASuppliedRun()
{
@@ -543,11 +668,10 @@ public void InsertOptionalDataVisitor_ResolvesOriginalUriBaseIds()
string inputFileName = "InsertOptionalDataVisitor.txt";
string testDirectory = GetTestDirectory("InsertOptionalDataVisitor") + @"\";
string uriBaseId = "TEST_DIR";
- string fileKey = "#" + uriBaseId + "#" + inputFileName;
IDictionary originalUriBaseIds = new Dictionary { { uriBaseId, new ArtifactLocation { Uri = new Uri(testDirectory, UriKind.Absolute) } } };
- Run run = new Run()
+ var run = new Run()
{
DefaultEncoding = "UTF-8",
OriginalUriBaseIds = null,