From b857f62b45f89797c9172c39956c7e476aa8ecd1 Mon Sep 17 00:00:00 2001 From: Larry Golding Date: Mon, 20 Jul 2020 13:39:23 -0700 Subject: [PATCH] Modify sample to use uriBaseIds (#2002) * Modify sample to use uriBaseIds. * Add TryReconstructAbsoluteUri unit test for missing trailing slash. * Shorten and move comment. * Introduce string constants. * Add SarifLogger tests for run enhancement. * Add a period. * Test population of artifact contents in presence of uriBaseId. * Add tests for GetEncodingFromName. * Remove renamed-and-mostly-changed file. * Fix file-scheme-related bug in UriConverter. * Test for analysis targets with encoding and contents. * DRY out "file" scheme constant. * Mentioned fix for #2001 in release history. * Remove extra blank line. * Fix typo in comment. * Fix another typo. * Add version control provenance; change to REPO_ROOT. * Visit results to provide region snippets. * Clean up InsertOptionalDataVisitorTests * Add unit test for visiting individual result. * Add rule help URIs to test data. Co-authored-by: Larry Golding Co-authored-by: Michael C. Fanning --- src/ReleaseHistory.md | 4 +- src/Samples/Sarif.Sdk.Sample/Program.cs | 82 ++++-- .../Properties/AssemblyInfo.cs | 9 +- src/Sarif.WorkItems/SarifWorkItemFiler.cs | 4 +- src/Sarif/Core/ArtifactLocation.cs | 4 +- src/Sarif/Core/Run.cs | 52 ++-- src/Sarif/Readers/UriConverter.cs | 19 +- src/Sarif/SarifUtilities.cs | 14 ++ src/Sarif/UriUtilities.cs | 18 ++ .../Visitors/InsertOptionalDataVisitor.cs | 6 + src/Sarif/Writers/SarifLogger.cs | 63 +++-- .../ConvertToSchemaUriTests.cs | 25 -- .../Core/ArtifactLocationTests.cs | 55 +++- .../SarifUtilitiesTests.cs | 84 +++++++ .../JsonConverters/UriConverterTests.json | 3 +- src/Test.UnitTests.Sarif/UriConverterTests.cs | 9 +- .../InsertOptionalDataVisitorTests.cs | 96 +++++-- .../Writers/SarifLoggerTests.cs | 236 +++++++++++++++++- ...tsDirectoryOnClassInitializationFixture.cs | 2 +- src/Test.Utilities.Sarif/TestData.cs | 1 + 20 files changed, 644 insertions(+), 142 deletions(-) create mode 100644 src/Sarif/UriUtilities.cs delete mode 100644 src/Test.UnitTests.Sarif/ConvertToSchemaUriTests.cs create mode 100644 src/Test.UnitTests.Sarif/SarifUtilitiesTests.cs diff --git a/src/ReleaseHistory.md b/src/ReleaseHistory.md index 9519cae15..97ca8f579 100644 --- a/src/ReleaseHistory.md +++ b/src/ReleaseHistory.md @@ -1,6 +1,8 @@ # SARIF Package Release History (SDK, Driver, Converters, and Multitool) ## **v2.3.3** [Sdk](https://www.nuget.org/packages/Sarif.Sdk/2.3.3) | [Driver](https://www.nuget.org/packages/Sarif.Driver/2.3.3) | [Converters](https://www.nuget.org/packages/Sarif.Converters/2.3.3) | [Multitool](https://www.nuget.org/packages/Sarif.Multitool/2.3.3) +* FEATURE: Improve `SarifSdkSample` application: use `uriBaseIds`. +* BUGFIX: If you created a URI from an absolute file path (for example, `C:\test\file.c`), then it would be serialized with that exact string, which is not a valid URI. This is now fixed. [#2001](https://github.com/microsoft/sarif-sdk/issues/2001) * FEATURE: Add additional checks to SARIF analysis rule `SARIF2004.OptimizeFileSize`. ## **v2.3.2** [Sdk](https://www.nuget.org/packages/Sarif.Sdk/2.3.2) | [Driver](https://www.nuget.org/packages/Sarif.Driver/2.3.2) | [Converters](https://www.nuget.org/packages/Sarif.Converters/2.3.2) | [Multitool](https://www.nuget.org/packages/Sarif.Multitool/2.3.2) @@ -19,7 +21,7 @@ * BUGFIX: In validation rules, `shortDescription` is now calculated by `GetFirstSentence` method, fixing a bug in sentence breaking. [#1887](https://github.com/microsoft/sarif-sdk/issues/1887) * BUGFIX: `WorkItemFiler` now logs correctly the details for `LogMetricsForProcessedModel` method [#1896](https://github.com/microsoft/sarif-sdk/issues/1896) * FEATURE: Add validation rule `SARIF1019`, which requires every result to have at least one of `result.ruleId` and `result.rule.id`. If both are present, they must be equal. [#1880](https://github.com/microsoft/sarif-sdk/issues/1880) -* FEATURE: Add validation rule `SARIF1020`, which requires that the $schema property should be present, and must refer to the final version of the SARIF 2.1.0 schema. [#1890](https://github.com/microsoft/sarif-sdk/issues/1890) +* FEATURE: Add validation rule `SARIF1020`, which requires that the $schema property should be present, and must refer to the final version of the SARIF 2.1.0 schema. [#1890](https://github.com/microsoft/sarif-sdk/issues/1890) * FEATURE: Expose `Run.MergeResultsFrom(Run)` to merge Results from multiple Runs using code from result matching algorithm. * BREAKING: Rename `RemapIndicesVisitor` to `RunMergingVisitor` and redesign to control how much merging occurs internally. diff --git a/src/Samples/Sarif.Sdk.Sample/Program.cs b/src/Samples/Sarif.Sdk.Sample/Program.cs index 6401a4401..6059af015 100644 --- a/src/Samples/Sarif.Sdk.Sample/Program.cs +++ b/src/Samples/Sarif.Sdk.Sample/Program.cs @@ -7,7 +7,6 @@ using System.Text; using CommandLine; using Microsoft.CodeAnalysis.Sarif; -using Microsoft.CodeAnalysis.Sarif.Readers; using Microsoft.CodeAnalysis.Sarif.Writers; using Newtonsoft.Json; @@ -15,7 +14,10 @@ namespace Sarif.Sdk.Sample { public class Program { - static int Main(string[] args) + private const string RepoRootBaseId = "REPO_ROOT"; + private const string BinRootBaseId = "BIN_ROOT"; + + internal static int Main(string[] args) { int result = Parser.Default.ParseArguments(args) .MapResult( @@ -31,7 +33,7 @@ static int Main(string[] args) /// /// Load verb options. /// Exit code - static int LoadSarifLogFile(LoadOptions options) + internal static int LoadSarifLogFile(LoadOptions options) { string logText = File.ReadAllText(options.InputFilePath); SarifLog log = JsonConvert.DeserializeObject(logText); @@ -46,12 +48,18 @@ static int LoadSarifLogFile(LoadOptions options) /// /// Create verb options. /// Exit code - static int CreateSarifLogFile(CreateOptions options) + internal static int CreateSarifLogFile(CreateOptions options) { // We'll use this source file for several defect results -- the // SampleSourceFiles folder should be a child of the project folder, // two levels up from the folder that contains the EXE (e.g., bin\Debug). - var artifactLocation = new ArtifactLocation { Uri = new Uri($"file://{AppDomain.CurrentDomain.BaseDirectory}../../SampleSourceFiles/AnalysisSample.cs") }; + string scanRootDirectory = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\..\SampleSourceFiles\")); + var scanRootUri = new Uri(scanRootDirectory, UriKind.Absolute); + var artifactLocation = new ArtifactLocation + { + Uri = new Uri("AnalysisSample.cs", UriKind.Relative), + UriBaseId = RepoRootBaseId + }; // Create a list of rules that will be enforced during your analysis #region Rules list @@ -71,7 +79,8 @@ static int CreateSarifLogFile(CreateOptions options) Text = "The property {0} returns an array." } } - } + }, + HelpUri = new Uri("https://www.example.com/rules/CA1819") }, new ReportingDescriptor { @@ -87,7 +96,8 @@ static int CreateSarifLogFile(CreateOptions options) Text = "The test for an empty string is performed by a string comparison rather than by testing String.Length." } } - } + }, + HelpUri = new Uri("https://www.example.com/rules/CA1820") }, new ReportingDescriptor { @@ -103,7 +113,8 @@ static int CreateSarifLogFile(CreateOptions options) Text = "The array-valued field {0} is marked readonly." } } - } + }, + HelpUri = new Uri("https://www.example.com/rules/CA2105") }, new ReportingDescriptor { @@ -119,7 +130,8 @@ static int CreateSarifLogFile(CreateOptions options) Text = "The Dispose method does not call the base class Dispose method." } } - } + }, + HelpUri = new Uri("https://www.example.com/rules/CA2215") } }; #endregion @@ -247,6 +259,38 @@ static int CreateSarifLogFile(CreateOptions options) }; #endregion + string binRootDirectory = @"d:\src\module\"; + var binRootUri = new Uri(binRootDirectory, UriKind.Absolute); + + var run = new Run + { + OriginalUriBaseIds = new Dictionary + { + [RepoRootBaseId] = new ArtifactLocation + { + Uri = scanRootUri + }, + [BinRootBaseId] = new ArtifactLocation + { + Uri = binRootUri + } + }, + VersionControlProvenance = new VersionControlDetails[] + { + new VersionControlDetails + { + RepositoryUri = new Uri("https://github.com/microsoft/sarif-sdk"), + RevisionId = "ee5a1ca8", + Branch = "master", + MappedTo = new ArtifactLocation + { + Uri = new Uri(".", UriKind.Relative), + UriBaseId = RepoRootBaseId + } + } + } + }; + // The SarifLogger will write the JSON-formatted log to this StringBuilder var sb = new StringBuilder(); @@ -257,9 +301,10 @@ static int CreateSarifLogFile(CreateOptions options) loggingOptions: LoggingOptions.PrettyPrint, // Use PrettyPrint to generate readable (multi-line, indented) JSON dataToInsert: OptionallyEmittedData.TextFiles | // Embed source file content directly in the log file -- great for portability of the log! - OptionallyEmittedData.Hashes, + OptionallyEmittedData.Hashes | + OptionallyEmittedData.RegionSnippets, tool: null, - run: null, + run: run, analysisTargets: null, invocationTokensToRedact: null, invocationPropertiesToLog: null, @@ -274,7 +319,11 @@ static int CreateSarifLogFile(CreateOptions options) var result = new Result() { RuleId = rule.Id, - AnalysisTarget = new ArtifactLocation { Uri = new Uri(@"file://d:/src/module/example.dll") }, // This is the file that was analyzed + AnalysisTarget = new ArtifactLocation + { + Uri = new Uri("example.dll", UriKind.Relative), // This is the file that was analyzed + UriBaseId = BinRootBaseId + }, Message = new Message { Id = "Default", @@ -302,7 +351,8 @@ static int CreateSarifLogFile(CreateOptions options) { // Because this file doesn't exist, it will be included in the files list but will only have a path and MIME type // This is the behavior you'll see any time a file can't be located/accessed - Uri = new Uri($"file://{AppDomain.CurrentDomain.BaseDirectory}/../../../SampleSourceFiles/SomeOtherSourceFile.cs"), + Uri = new Uri("SomeOtherSourceFile.cs", UriKind.Relative), + UriBaseId = RepoRootBaseId }, Region = new Region { @@ -330,7 +380,7 @@ static int CreateSarifLogFile(CreateOptions options) ArtifactLocation = artifactLocation, Region = new Region { - StartLine = 212 + StartLine = 17 } } } @@ -345,7 +395,7 @@ static int CreateSarifLogFile(CreateOptions options) ArtifactLocation = artifactLocation, Region = new Region { - StartLine = 452 // Fake example + StartLine = 24 // Fake example } } } @@ -360,7 +410,7 @@ static int CreateSarifLogFile(CreateOptions options) ArtifactLocation = artifactLocation, Region = new Region { - StartLine = 145 + StartLine = 26 // Fake example } } } diff --git a/src/Samples/Sarif.Sdk.Sample/Properties/AssemblyInfo.cs b/src/Samples/Sarif.Sdk.Sample/Properties/AssemblyInfo.cs index 567b4c052..2cf088dcb 100644 --- a/src/Samples/Sarif.Sdk.Sample/Properties/AssemblyInfo.cs +++ b/src/Samples/Sarif.Sdk.Sample/Properties/AssemblyInfo.cs @@ -2,18 +2,17 @@ // license. See LICENSE file in the project root for full license information. using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("SARIF SDK v2.0 Sample")] -[assembly: AssemblyDescription("SARIF v2.0 log file read/write sample application")] +[assembly: AssemblyTitle("SARIF SDK v2.1.0 Sample")] +[assembly: AssemblyDescription("SARIF v2.1.0 log file read/write sample application")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("SARIF SDK v2.0")] -[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyProduct("SARIF SDK v2.1.0")] +[assembly: AssemblyCopyright("Copyright © 2018")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/Sarif.WorkItems/SarifWorkItemFiler.cs b/src/Sarif.WorkItems/SarifWorkItemFiler.cs index 1166b05e4..24ee7866d 100644 --- a/src/Sarif.WorkItems/SarifWorkItemFiler.cs +++ b/src/Sarif.WorkItems/SarifWorkItemFiler.cs @@ -67,9 +67,9 @@ public virtual SarifLog FileWorkItems(Uri sarifLogFileLocation) { sarifLogFileLocation = sarifLogFileLocation ?? throw new ArgumentNullException(nameof(sarifLogFileLocation)); - if (sarifLogFileLocation.IsAbsoluteUri && sarifLogFileLocation.Scheme == "file") + if (sarifLogFileLocation.IsAbsoluteUri && sarifLogFileLocation.Scheme == UriUtilities.FileScheme) { - if (sarifLogFileLocation.IsAbsoluteUri && sarifLogFileLocation.Scheme == "file:") + if (sarifLogFileLocation.IsAbsoluteUri && sarifLogFileLocation.Scheme == UriUtilities.FileScheme.WithColon()) { using (var stream = new FileStream(sarifLogFileLocation.LocalPath, FileMode.Open, FileAccess.Read)) using (var reader = new StreamReader(stream)) diff --git a/src/Sarif/Core/ArtifactLocation.cs b/src/Sarif/Core/ArtifactLocation.cs index 2966992db..d48f5166d 100644 --- a/src/Sarif/Core/ArtifactLocation.cs +++ b/src/Sarif/Core/ArtifactLocation.cs @@ -53,7 +53,9 @@ public bool TryReconstructAbsoluteUri(IDictionary orig // I'd like to use the ctor new Uri(baseUri, relativeUri) here, but it fails with // ArgumentOutOfRangeException, perhaps because it doesn't like the baseUri argument // to be relative. So... - stemUri = new Uri(artifactLocation.Uri.OriginalString + stemUri.OriginalString, UriKind.RelativeOrAbsolute); + string artifactLocationOriginalUriString = artifactLocation.Uri.OriginalString; + if (!artifactLocationOriginalUriString.EndsWith("/")) { artifactLocationOriginalUriString += "/"; } + stemUri = new Uri(artifactLocationOriginalUriString + stemUri.OriginalString, UriKind.RelativeOrAbsolute); } // If we got here, we found an absolute URI. diff --git a/src/Sarif/Core/Run.cs b/src/Sarif/Core/Run.cs index 5e8d089b5..9e65a461f 100644 --- a/src/Sarif/Core/Run.cs +++ b/src/Sarif/Core/Run.cs @@ -17,7 +17,7 @@ public partial class Run private static readonly Invocation EmptyInvocation = new Invocation(); private static readonly LogicalLocation EmptyLogicalLocation = new LogicalLocation(); - private IDictionary _fileToIndexMap; + private IDictionary _artifactLocationToIndexMap; public Uri ExpandUrisWithUriBaseId(string key, string currentValue = null) { @@ -47,7 +47,7 @@ public int GetFileIndex( } } - if (_fileToIndexMap == null) + if (_artifactLocationToIndexMap == null) { InitializeFileToIndexMap(); } @@ -58,11 +58,11 @@ public int GetFileIndex( return fileLocation.Index; } - // Strictly speaking, some elements that may contribute to a files table - // key are case sensitive, e.g., everything but the schema and protocol of a - // web URI. We don't have a proper comparer implementation that can handle + // Strictly speaking, some elements that may contribute to a files table + // key are case sensitive, e.g., everything but the scheme and protocol of a + // web URI. We don't have a proper comparer implementation that can handle // all cases. For now, we cover the Windows happy path, which assumes that - // most URIs in log files are file paths (which are case-insensitive) + // most URIs in log files are file paths (which are case-insensitive). // // Tracking item for an improved comparer: // https://github.com/Microsoft/sarif-sdk/issues/973 @@ -76,61 +76,65 @@ public int GetFileIndex( // throughout the emitted log. fileLocation.Uri = new Uri(UriHelper.MakeValidUri(fileLocation.Uri.OriginalString), UriKind.RelativeOrAbsolute); - var filesTableKey = new ArtifactLocation + var artifactLocation = new ArtifactLocation { Uri = fileLocation.Uri, UriBaseId = fileLocation.UriBaseId }; - if (!_fileToIndexMap.TryGetValue(filesTableKey, out int fileIndex)) + if (!_artifactLocationToIndexMap.TryGetValue(artifactLocation, out int artifactIndex)) { if (addToFilesTableIfNotPresent) { this.Artifacts = this.Artifacts ?? new List(); - fileIndex = this.Artifacts.Count; + artifactIndex = this.Artifacts.Count; - var fileData = Artifact.Create( - filesTableKey.Uri, + Uri artifactUri = artifactLocation.TryReconstructAbsoluteUri(this.OriginalUriBaseIds, out Uri resolvedUri) + ? resolvedUri + : artifactLocation.Uri; + + var artifact = Artifact.Create( + artifactUri, dataToInsert, hashData: hashData, - encoding: null); + encoding: encoding); // Copy ArtifactLocation to ensure changes to Result copy don't affect new Run.Artifacts copy - fileData.Location = new ArtifactLocation(fileLocation); + artifact.Location = new ArtifactLocation(fileLocation); - this.Artifacts.Add(fileData); + this.Artifacts.Add(artifact); - _fileToIndexMap[filesTableKey] = fileIndex; + _artifactLocationToIndexMap[artifactLocation] = artifactIndex; } else { // We did not find the item. The call was not configured to add the entry. // Return the default value that indicates the item isn't present. - fileIndex = -1; + artifactIndex = -1; } } - fileLocation.Index = fileIndex; - return fileIndex; + fileLocation.Index = artifactIndex; + return artifactIndex; } private void InitializeFileToIndexMap() { - _fileToIndexMap = new Dictionary(ArtifactLocation.ValueComparer); + _artifactLocationToIndexMap = new Dictionary(ArtifactLocation.ValueComparer); // First, we'll initialize our file object to index map // with any files that already exist in the table for (int i = 0; i < this.Artifacts?.Count; i++) { - Artifact fileData = this.Artifacts[i]; + Artifact artifact = this.Artifacts[i]; - var fileLocation = new ArtifactLocation + var artifactLocation = new ArtifactLocation { - Uri = fileData.Location?.Uri, - UriBaseId = fileData.Location?.UriBaseId, + Uri = artifact.Location?.Uri, + UriBaseId = artifact.Location?.UriBaseId, }; - _fileToIndexMap[fileLocation] = i; + _artifactLocationToIndexMap[artifactLocation] = i; } } diff --git a/src/Sarif/Readers/UriConverter.cs b/src/Sarif/Readers/UriConverter.cs index c7d165fdd..047a69555 100644 --- a/src/Sarif/Readers/UriConverter.cs +++ b/src/Sarif/Readers/UriConverter.cs @@ -44,7 +44,24 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s var uri = value as Uri; if (uri != null && !string.IsNullOrWhiteSpace(uri.OriginalString)) { - writer.WriteValue(uri.OriginalString); + // Absolute file-scheme URIs need special treatment. OriginalString might be a file + // system path such as "C:\test\file.c", which is not a valid URI. In that case we + // must instead serialize "file:///C:/test/file.c", which we can get from AbsoluteUri. + // + // However, if OriginalString starts with "file:", we do want to serialize it. It + // might (for example) be "file:///C:/dir1/dir2/..", in which case AbsolutePath would + // be "file:///C:/dir1". We don't want to lose the dot-dot segment when round-tripping + // the URI, if for no other reason than that we might want to run the SARIF validator + // on the result of the round trip, and we don't want to lose the warning that you + // shouldn't use dot-dot segments! + bool useAbsoluteUri = + uri.IsAbsoluteUri && + uri.Scheme.Equals(UriUtilities.FileScheme, StringComparison.Ordinal) && + !uri.OriginalString.StartsWith(UriUtilities.FileScheme.WithColon(), StringComparison.Ordinal); + + string serializedValue = useAbsoluteUri ? uri.AbsoluteUri : uri.OriginalString; + + writer.WriteValue(serializedValue); return; } diff --git a/src/Sarif/SarifUtilities.cs b/src/Sarif/SarifUtilities.cs index 2c4c7173b..cc77225a2 100644 --- a/src/Sarif/SarifUtilities.cs +++ b/src/Sarif/SarifUtilities.cs @@ -202,5 +202,19 @@ internal static void DebugAssert(bool conditional) Debug.Assert(conditional); } } + + internal static Encoding GetEncodingFromName(string encodingName) + { + if (string.IsNullOrWhiteSpace(encodingName)) { return null; } + + try + { + return Encoding.GetEncoding(encodingName); + } + catch (ArgumentException) + { + return null; + } + } } } diff --git a/src/Sarif/UriUtilities.cs b/src/Sarif/UriUtilities.cs new file mode 100644 index 000000000..9d4f06896 --- /dev/null +++ b/src/Sarif/UriUtilities.cs @@ -0,0 +1,18 @@ +// 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 +{ + public static class UriUtilities + { + public const string FileScheme = "file"; + + public static string WithColon(this string scheme) + { + scheme = scheme ?? throw new ArgumentNullException(nameof(scheme)); + return $"{scheme}:"; + } + } +} diff --git a/src/Sarif/Visitors/InsertOptionalDataVisitor.cs b/src/Sarif/Visitors/InsertOptionalDataVisitor.cs index 7f0c92d5c..dea7bd0c9 100644 --- a/src/Sarif/Visitors/InsertOptionalDataVisitor.cs +++ b/src/Sarif/Visitors/InsertOptionalDataVisitor.cs @@ -18,6 +18,12 @@ public class InsertOptionalDataVisitor : SarifRewritingVisitor private readonly OptionallyEmittedData _dataToInsert; private readonly IDictionary _originalUriBaseIds; + public InsertOptionalDataVisitor(OptionallyEmittedData dataToInsert, Run run) + : this(dataToInsert, run?.OriginalUriBaseIds) + { + _run = run ?? throw new ArgumentNullException(nameof(run)); + } + public InsertOptionalDataVisitor(OptionallyEmittedData dataToInsert, IDictionary originalUriBaseIds = null) { _dataToInsert = dataToInsert; diff --git a/src/Sarif/Writers/SarifLogger.cs b/src/Sarif/Writers/SarifLogger.cs index 7b55d6a93..bd9e355cc 100644 --- a/src/Sarif/Writers/SarifLogger.cs +++ b/src/Sarif/Writers/SarifLogger.cs @@ -8,6 +8,8 @@ using System.Linq; using System.Text; using Microsoft.CodeAnalysis.Sarif.Readers; +using Microsoft.CodeAnalysis.Sarif.Visitors; + using Newtonsoft.Json; namespace Microsoft.CodeAnalysis.Sarif.Writers @@ -22,6 +24,7 @@ public class SarifLogger : IDisposable, IAnalysisLogger private readonly OptionallyEmittedData _dataToInsert; private readonly OptionallyEmittedData _dataToRemove; private readonly ResultLogJsonWriter _issueLogJsonWriter; + private readonly InsertOptionalDataVisitor _insertOptionalDataVisitor; protected const LoggingOptions DefaultLoggingOptions = LoggingOptions.PrettyPrint; @@ -44,7 +47,8 @@ public SarifLogger( run: run, analysisTargets: analysisTargets, invocationTokensToRedact: invocationTokensToRedact, - invocationPropertiesToLog: invocationPropertiesToLog) + invocationPropertiesToLog: invocationPropertiesToLog, + defaultFileEncoding: defaultFileEncoding) { } @@ -66,14 +70,21 @@ public SarifLogger( AnalysisTargetToHashDataMap = HashUtilities.MultithreadedComputeTargetFileHashes(analysisTargets); } - _run = run ?? CreateRun( - analysisTargets, - dataToInsert, - dataToRemove, - invocationTokensToRedact, - invocationPropertiesToLog, - defaultFileEncoding, - AnalysisTargetToHashDataMap); + _run = run ?? new Run(); + + if (dataToInsert.HasFlag(OptionallyEmittedData.RegionSnippets)) + { + _insertOptionalDataVisitor = new InsertOptionalDataVisitor(dataToInsert, _run); + } + + EnhanceRun( + analysisTargets, + dataToInsert, + dataToRemove, + invocationTokensToRedact, + invocationPropertiesToLog, + defaultFileEncoding, + AnalysisTargetToHashDataMap); tool = tool ?? Tool.CreateFromAssemblyData(); @@ -113,7 +124,7 @@ private SarifLogger(TextWriter textWriter, LoggingOptions loggingOptions, bool c RuleToIndexMap = new Dictionary(ReportingDescriptor.ValueComparer); } - private static Run CreateRun( + private void EnhanceRun( IEnumerable analysisTargets, OptionallyEmittedData dataToInsert, OptionallyEmittedData dataToRemove, @@ -122,15 +133,17 @@ private static Run CreateRun( string defaultFileEncoding = null, IDictionary filePathToHashDataMap = null) { - var run = new Run + _run.Invocations ??= new List(); + if (defaultFileEncoding != null) { - Invocations = new List(), - DefaultEncoding = defaultFileEncoding - }; + _run.DefaultEncoding = defaultFileEncoding; + } + + Encoding encoding = SarifUtilities.GetEncodingFromName(_run.DefaultEncoding); if (analysisTargets != null) { - run.Artifacts = new List(); + _run.Artifacts ??= new List(); foreach (string target in analysisTargets) { @@ -142,9 +155,10 @@ private static Run CreateRun( filePathToHashDataMap?.TryGetValue(target, out hashData); } - var fileData = Artifact.Create( + var artifact = Artifact.Create( new Uri(target, UriKind.RelativeOrAbsolute), dataToInsert, + encoding, hashData: hashData); var fileLocation = new ArtifactLocation @@ -152,14 +166,14 @@ private static Run CreateRun( Uri = uri }; - fileData.Location = fileLocation; + artifact.Location = fileLocation; // This call will insert the file object into run.Files if not already present - fileData.Location.Index = run.GetFileIndex( - fileData.Location, + artifact.Location.Index = _run.GetFileIndex( + artifact.Location, addToFilesTableIfNotPresent: true, dataToInsert: dataToInsert, - encoding: null, + encoding: encoding, hashData: hashData); } } @@ -195,8 +209,7 @@ private static Run CreateRun( } } - run.Invocations.Add(invocation); - return run; + _run.Invocations.Add(invocation); } public IDictionary AnalysisTargetToHashDataMap { get; } @@ -294,6 +307,12 @@ public void Log(ReportingDescriptor rule, Result result) result.RuleIndex = LogRule(rule); CaptureFilesInResult(result); + + if (_insertOptionalDataVisitor != null) + { + _insertOptionalDataVisitor.VisitResult(result); + } + _issueLogJsonWriter.WriteResult(result); } diff --git a/src/Test.UnitTests.Sarif/ConvertToSchemaUriTests.cs b/src/Test.UnitTests.Sarif/ConvertToSchemaUriTests.cs deleted file mode 100644 index a46770a74..000000000 --- a/src/Test.UnitTests.Sarif/ConvertToSchemaUriTests.cs +++ /dev/null @@ -1,25 +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 Xunit; - -namespace Microsoft.CodeAnalysis.Sarif -{ - public class SarifUtilitiesTests - { - [Fact] - public void ConvertToSchemaUriTestV100() - { - Uri uri = SarifVersion.OneZeroZero.ConvertToSchemaUri(); - Assert.Equal(SarifUtilities.SarifSchemaUriBase + SarifUtilities.V1_0_0 + ".json", uri.ToString()); - } - - [Fact] - public void ConvertToSchemaUriTestVCurrent() - { - Uri uri = SarifVersion.Current.ConvertToSchemaUri(); - Assert.Equal(SarifUtilities.SarifSchemaUri, uri.ToString()); - } - } -} diff --git a/src/Test.UnitTests.Sarif/Core/ArtifactLocationTests.cs b/src/Test.UnitTests.Sarif/Core/ArtifactLocationTests.cs index b848c1735..5c10e9d99 100644 --- a/src/Test.UnitTests.Sarif/Core/ArtifactLocationTests.cs +++ b/src/Test.UnitTests.Sarif/Core/ArtifactLocationTests.cs @@ -11,6 +11,9 @@ namespace Microsoft.CodeAnalysis.Test.UnitTests.Sarif.Core { public class ArtifactLocationTests { + private const string ProjectRootBaseId = "PROJECT_ROOT"; + private const string SourceRootBaseId = "SOURCE_ROOT"; + [Fact] public void TryReconstructAbsoluteUri_WhenInputUriIsNull_ReturnsFalse() { @@ -42,12 +45,12 @@ public void TryReconstructAbsoluteUri_WhenInputUriResolvesDirectly_ReturnsTrueWi var artifactLocation = new ArtifactLocation { Uri = new Uri("README.md", UriKind.Relative), - UriBaseId = "PROJECT_ROOT" + UriBaseId = ProjectRootBaseId }; var originalUriBaseIds = new Dictionary { - ["PROJECT_ROOT"] = new ArtifactLocation + [ProjectRootBaseId] = new ArtifactLocation { Uri = new Uri("file://c:/code/sarif-sdk/", UriKind.Absolute) } @@ -65,20 +68,20 @@ public void TryReconstructAbsoluteUri_WhenInputResolvesIndirectly_ReturnsTrueWit var artifactLocation = new ArtifactLocation { Uri = new Uri("Sarif/CopyrightNotice.txt", UriKind.Relative), - UriBaseId = "SOURCE_ROOT" + UriBaseId = SourceRootBaseId }; var originalUriBaseIds = new Dictionary { - ["PROJECT_ROOT"] = new ArtifactLocation + [ProjectRootBaseId] = new ArtifactLocation { Uri = new Uri("file://c:/code/sarif-sdk/", UriKind.Absolute) }, - ["SOURCE_ROOT"] = new ArtifactLocation + [SourceRootBaseId] = new ArtifactLocation { Uri = new Uri("src/", UriKind.Relative), - UriBaseId = "PROJECT_ROOT" + UriBaseId = ProjectRootBaseId } }; @@ -94,15 +97,15 @@ public void TryReconstructAbsoluteUri_WhenChainedUriBaseIdIsAbsent_ReturnsFalse( var artifactLocation = new ArtifactLocation { Uri = new Uri("Sarif/CopyrightNotice.txt", UriKind.Relative), - UriBaseId = "SOURCE_ROOT" + UriBaseId = SourceRootBaseId }; var originalUriBaseIds = new Dictionary { - ["SOURCE_ROOT"] = new ArtifactLocation + [SourceRootBaseId] = new ArtifactLocation { Uri = new Uri("src/", UriKind.Relative), - UriBaseId = "PROJECT_ROOT" // But originalUriBaseIds["PROJECT_ROOT"] is absent. + UriBaseId = ProjectRootBaseId // But originalUriBaseIds[ProjectRootBaseId] is absent. } }; @@ -117,12 +120,12 @@ public void TryReconstructAbsoluteUri_WhenChainedUriIsAbsent_ReturnsFalse() var artifactLocation = new ArtifactLocation { Uri = new Uri("Sarif/CopyrightNotice.txt", UriKind.Relative), - UriBaseId = "SOURCE_ROOT" + UriBaseId = SourceRootBaseId }; var originalUriBaseIds = new Dictionary { - ["PROJECT_ROOT"] = new ArtifactLocation + [ProjectRootBaseId] = new ArtifactLocation { Description = new Message { @@ -131,10 +134,10 @@ public void TryReconstructAbsoluteUri_WhenChainedUriIsAbsent_ReturnsFalse() // But Uri is absent. }, - ["SOURCE_ROOT"] = new ArtifactLocation + [SourceRootBaseId] = new ArtifactLocation { Uri = new Uri("src/", UriKind.Relative), - UriBaseId = "PROJECT_ROOT" // But originalUriBaseIds["PROJECT_ROOT"] is absent. + UriBaseId = ProjectRootBaseId // But originalUriBaseIds[ProjectRootBaseId] is absent. } }; @@ -142,5 +145,31 @@ public void TryReconstructAbsoluteUri_WhenChainedUriIsAbsent_ReturnsFalse() wasResolved.Should().BeFalse(); } + + [Fact] + public void TryReconstructAbsoluteUri_WhenBaseUriDoesNotEndWithSlash_EnsuresSlashIsPresent() + { + var artifactLocation = new ArtifactLocation + { + Uri = new Uri("src/Sarif/CopyrightNotice.txt", UriKind.Relative), + UriBaseId = ProjectRootBaseId + }; + + var originalUriBaseIds = new Dictionary + { + [ProjectRootBaseId] = new ArtifactLocation + { + // It's invalid SARIF for a base URI not to end with a slash, but we shouldn't + // fail to resolve a URI just because some tool didn't do that. + Uri = new Uri("file://c:/code/sarif-sdk"), + UriBaseId = ProjectRootBaseId + } + }; + + bool wasResolved = artifactLocation.TryReconstructAbsoluteUri(originalUriBaseIds, out Uri resolvedUri); + + wasResolved.Should().BeTrue(); + resolvedUri.Should().Be(new Uri("file://c:/code/sarif-sdk/src/Sarif/CopyrightNotice.txt", UriKind.Absolute)); + } } } diff --git a/src/Test.UnitTests.Sarif/SarifUtilitiesTests.cs b/src/Test.UnitTests.Sarif/SarifUtilitiesTests.cs new file mode 100644 index 000000000..5a7fe430b --- /dev/null +++ b/src/Test.UnitTests.Sarif/SarifUtilitiesTests.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Castle.DynamicProxy.Generators; + +using FluentAssertions; + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Text; + +using Xunit; + +namespace Microsoft.CodeAnalysis.Sarif +{ + public class SarifUtilitiesTests + { + [Fact] + public void ConvertToSchemaUriTestV100() + { + Uri uri = SarifVersion.OneZeroZero.ConvertToSchemaUri(); + Assert.Equal(SarifUtilities.SarifSchemaUriBase + SarifUtilities.V1_0_0 + ".json", uri.ToString()); + } + + [Fact] + public void ConvertToSchemaUriTestVCurrent() + { + Uri uri = SarifVersion.Current.ConvertToSchemaUri(); + Assert.Equal(SarifUtilities.SarifSchemaUri, uri.ToString()); + } + + private class GetEncodingFromNameTestCase + { + internal string TestCaseName { get; } + internal string EncodingName { get; } + internal Encoding ExpectedEncoding { get; } + + internal GetEncodingFromNameTestCase( + string testCaseName, + string encodingName, + Encoding expectedEncoding) + { + TestCaseName = testCaseName; + EncodingName = encodingName; + ExpectedEncoding = expectedEncoding; + } + } + + private static readonly IReadOnlyCollection s_getEncodingFromNameTestCases = new ReadOnlyCollection( + new List + { + new GetEncodingFromNameTestCase( + testCaseName: "Valid encoding name", + encodingName: "UTF-8", + expectedEncoding: Encoding.UTF8), + new GetEncodingFromNameTestCase( + testCaseName: "Invalid encoding name", + encodingName: "INVALID", + expectedEncoding: null), + new GetEncodingFromNameTestCase( + testCaseName: "Null encoding name", + encodingName: null, + expectedEncoding: null) + }); + + [Fact] + public void GetEncodingFromNameProducesExpectedEncoding() + { + var sb = new StringBuilder(); + foreach (GetEncodingFromNameTestCase testCase in s_getEncodingFromNameTestCases) + { + Encoding actualEncoding = SarifUtilities.GetEncodingFromName(testCase.EncodingName); + if (actualEncoding != testCase.ExpectedEncoding) + { + sb.AppendLine($" {testCase.TestCaseName}"); + } + } + + sb.Length.Should().Be(0, + $"expected all test cases to pass, but the following test cases failed:\n{sb}"); + } + } +} diff --git a/src/Test.UnitTests.Sarif/TestData/JsonConverters/UriConverterTests.json b/src/Test.UnitTests.Sarif/TestData/JsonConverters/UriConverterTests.json index 893d0ec96..bfed7c1e8 100644 --- a/src/Test.UnitTests.Sarif/TestData/JsonConverters/UriConverterTests.json +++ b/src/Test.UnitTests.Sarif/TestData/JsonConverters/UriConverterTests.json @@ -5,5 +5,6 @@ "nonEmptyUriList": [ "https://www.example.com/page1", "https://www.example.com/page2" - ] + ], + "fileSchemeUri": "file:///C:/test/file.c" } \ No newline at end of file diff --git a/src/Test.UnitTests.Sarif/UriConverterTests.cs b/src/Test.UnitTests.Sarif/UriConverterTests.cs index 64d657850..8c5485921 100644 --- a/src/Test.UnitTests.Sarif/UriConverterTests.cs +++ b/src/Test.UnitTests.Sarif/UriConverterTests.cs @@ -10,7 +10,6 @@ using Microsoft.CodeAnalysis.Sarif; using Newtonsoft.Json; using Xunit; -using Xunit.Sdk; namespace Microsoft.CodeAnalysis.Test.UnitTests.Sarif { @@ -37,6 +36,10 @@ private class TestClass [DataMember(Name = "nonEmptyUriList", IsRequired = false, EmitDefaultValue = false)] [JsonConverter(typeof(Microsoft.CodeAnalysis.Sarif.Readers.UriConverter))] public IList NonEmptyUriList { get; set; } + + [DataMember(Name = "fileSchemeUri", IsRequired = false, EmitDefaultValue = false)] + [JsonConverter(typeof(Microsoft.CodeAnalysis.Sarif.Readers.UriConverter))] + public Uri FileSchemeUri { get; set; } } private class SingleUri @@ -58,6 +61,7 @@ public void ReadJson_ConvertsStringsToUriObjects() testObject.NonEmptyUriList.Select(uri => uri.OriginalString).Should().ContainInOrder( "https://www.example.com/page1", "https://www.example.com/page2"); + testObject.FileSchemeUri.OriginalString.Should().Be("file:///C:/test/file.c"); } [Fact] @@ -73,7 +77,8 @@ public void WriteJson_ConvertsUriObjectsToStrings() { new Uri("https://www.example.com/page1"), new Uri("https://www.example.com/page2") - } + }, + FileSchemeUri = new Uri(@"C:\test\file.c") }; var settings = new JsonSerializerSettings diff --git a/src/Test.UnitTests.Sarif/Visitors/InsertOptionalDataVisitorTests.cs b/src/Test.UnitTests.Sarif/Visitors/InsertOptionalDataVisitorTests.cs index 05f2c4ceb..2efddb942 100644 --- a/src/Test.UnitTests.Sarif/Visitors/InsertOptionalDataVisitorTests.cs +++ b/src/Test.UnitTests.Sarif/Visitors/InsertOptionalDataVisitorTests.cs @@ -5,21 +5,22 @@ using System.Collections.Generic; using System.IO; using FluentAssertions; + +using Microsoft.CodeAnalysis.Sarif.Driver; using Microsoft.CodeAnalysis.Sarif.Writers; +using Microsoft.CodeAnalysis.Test.Utilities.Sarif; + using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; namespace Microsoft.CodeAnalysis.Sarif.Visitors { - - public class InsertOptionalDataVisitorTests : FileDiffingUnitTests, IClassFixture + public class InsertOptionalDataVisitorTests : FileDiffingUnitTests, IClassFixture { - public class InsertOptionalDataVisitorTestsFixture : DeletesOutputsDirectoryOnClassInitializationFixture { } - private OptionallyEmittedData _currentOptionallyEmittedData; - public InsertOptionalDataVisitorTests(ITestOutputHelper outputHelper, InsertOptionalDataVisitorTestsFixture fixture) : base(outputHelper) + public InsertOptionalDataVisitorTests(ITestOutputHelper outputHelper, DeletesOutputsDirectoryOnClassInitializationFixture _) : base(outputHelper) { } @@ -170,12 +171,72 @@ public void InsertOptionalDataVisitor_PersistsAll() } [Fact] - public void InsertOptionalDataVisitorTests_ContextRegionSnippets_DoesNotFail_TopLevelOriginalUriBaseIdUriMissing() + public void InsertOptionalDataVisitor_ContextRegionSnippets_DoesNotFail_TopLevelOriginalUriBaseIdUriMissing() { RunTest("TopLevelOriginalUriBaseIdUriMissing.sarif", OptionallyEmittedData.ContextRegionSnippets); } + [Fact] + public void InsertOptionalDataVisitor_CanVisitIndividualResultsInASuppliedRun() + { + const string TestFileContents = +@"One +Two +Three"; + const string ExpectedSnippet = "Two"; + + using (var tempFile = new TempFile(".txt")) + { + string tempFilePath = tempFile.Name; + string tempFileName = Path.GetFileName(tempFilePath); + string tempFileDirectory = Path.GetDirectoryName(tempFilePath); + + File.WriteAllText(tempFilePath, TestFileContents); + + var run = new Run + { + OriginalUriBaseIds = new Dictionary + { + [TestData.TestRootBaseId] = new ArtifactLocation + { + Uri = new Uri(tempFileDirectory, UriKind.Absolute) + } + }, + Results = new List + { + new Result + { + Locations = new List + { + new Location + { + PhysicalLocation = new PhysicalLocation + { + ArtifactLocation = new ArtifactLocation + { + Uri = new Uri(tempFileName, UriKind.Relative), + UriBaseId = TestData.TestRootBaseId + }, + Region = new Region + { + StartLine = 2 + } + } + } + } + } + } + }; + + var visitor = new InsertOptionalDataVisitor(OptionallyEmittedData.RegionSnippets, run); + + visitor.VisitResult(run.Results[0]); + + run.Results[0].Locations[0].PhysicalLocation.Region.Snippet.Text.Should().Be(ExpectedSnippet); + } + } + private const int RuleIndex = 0; private const string RuleId = nameof(RuleId); private const string NotificationId = nameof(NotificationId); @@ -236,7 +297,7 @@ private static Run CreateBasicRunForMessageStringLookupTesting() } [Fact] - public void InsertOptionalDataVisitorTests_FlattensMessageStringsInResult() + public void InsertOptionalDataVisitor_FlattensMessageStringsInResult() { Run run = CreateBasicRunForMessageStringLookupTesting(); @@ -286,7 +347,7 @@ public void InsertOptionalDataVisitorTests_FlattensMessageStringsInResult() } [Fact] - public void InsertOptionalDataVisitorTests_FlattensMessageStringsInNotification() + public void InsertOptionalDataVisitor_FlattensMessageStringsInNotification() { Run run = CreateBasicRunForMessageStringLookupTesting(); @@ -358,7 +419,7 @@ public void InsertOptionalDataVisitorTests_FlattensMessageStringsInNotification( } [Fact] - public void InsertOptionalDataVisitorTests_FlattensMessageStringsInFix() + public void InsertOptionalDataVisitor_FlattensMessageStringsInFix() { Run run = CreateBasicRunForMessageStringLookupTesting(); @@ -430,7 +491,7 @@ public void InsertOptionalDataVisitorTests_FlattensMessageStringsInFix() } [Fact] - public void InsertOptionalDataVisitorTests_ResolvesOriginalUriBaseIds() + public void InsertOptionalDataVisitor_ResolvesOriginalUriBaseIds() { string inputFileName = "InsertOptionalDataVisitor.txt"; string testDirectory = GetTestDirectory("InsertOptionalDataVisitor") + @"\"; @@ -478,20 +539,5 @@ public void InsertOptionalDataVisitorTests_ResolvesOriginalUriBaseIds() run.OriginalUriBaseIds.Should().Equal(originalUriBaseIds); run.Artifacts[0].Contents.Text.Should().Be(File.ReadAllText(Path.Combine(testDirectory, inputFileName))); } - - private static string FormatFailureReason(string failureOutput) - { - string message = "the rewritten file should matched the supplied SARIF. "; - message += failureOutput + Environment.NewLine; - - message = "If the actual output is expected, generate new baselines by setting s_rebaseline == true in the test code and rerunning."; - return message; - } - - private string NormalizeOptionallyEmittedDataToString(OptionallyEmittedData optionallyEmittedData) - { - string result = optionallyEmittedData.ToString(); - return result.Replace(", ", "+"); - } } } \ No newline at end of file diff --git a/src/Test.UnitTests.Sarif/Writers/SarifLoggerTests.cs b/src/Test.UnitTests.Sarif/Writers/SarifLoggerTests.cs index 1dc9281c1..52fd9557b 100644 --- a/src/Test.UnitTests.Sarif/Writers/SarifLoggerTests.cs +++ b/src/Test.UnitTests.Sarif/Writers/SarifLoggerTests.cs @@ -11,6 +11,7 @@ using FluentAssertions; using Microsoft.CodeAnalysis.Sarif.Driver; using Microsoft.CodeAnalysis.Sarif.Writers; +using Microsoft.CodeAnalysis.Test.Utilities.Sarif; using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; @@ -19,6 +20,7 @@ namespace Microsoft.CodeAnalysis.Sarif { public class SarifLoggerTests : JsonTests { + private const string TempFileBaseId = "TEMP_ROOT"; private static string GetResourceContents(string resourceName) => ResourceExtractor.GetResourceText($"SarifLogger.{resourceName}"); @@ -365,8 +367,6 @@ public void SarifLogger_WritesFileData() string logText = sb.ToString(); - string fileDataKey = new Uri(file).AbsoluteUri; - SarifLog sarifLog = JsonConvert.DeserializeObject(logText); sarifLog.Runs[0].Artifacts[0].Hashes.Keys.Count.Should().Be(3); sarifLog.Runs[0].Artifacts[0].Hashes["md5"].Should().Be("4B9DC12934390387862CC4AB5E4A2159"); @@ -374,6 +374,134 @@ public void SarifLogger_WritesFileData() sarifLog.Runs[0].Artifacts[0].Hashes["sha-256"].Should().Be("0953D7B3ADA7FED683680D2107EE517A9DBEC2D0AF7594A91F058D104B7A2AEB"); } + [Fact] + public void SarifLogger_WritesFileContents_EvenWhenLocationUsesUriBaseId() + { + var sb = new StringBuilder(); + + // Create a temporary file whose extension signals that it is textual. + // This ensures that the ArtifactContents.Text property, rather than + // the Binary property, is populated, so the test of the Text property + // at the end will work. + using (var tempFile = new TempFile(".txt")) + { + string tempFilePath = tempFile.Name; + string tempFileDirectory = Path.GetDirectoryName(tempFilePath); + string tempFileName = Path.GetFileName(tempFilePath); + + File.WriteAllText(tempFilePath, "#include \"windows.h\";"); + + var run = new Run + { + // To get text contents, we also need to specify an encoding that + // Encoding.GetEncoding() will accept. + DefaultEncoding = "UTF-8" + }; + + var analysisTargets = new List + { + tempFilePath + }; + + using (var textWriter = new StringWriter(sb)) + { + // Create a logger that inserts artifact contents. + using (var sarifLogger = new SarifLogger( + textWriter, + run: run, + analysisTargets: analysisTargets, + dataToInsert: OptionallyEmittedData.TextFiles)) + { + } + + // The logger should have populated the artifact contents. + string logText = sb.ToString(); + SarifLog sarifLog = JsonConvert.DeserializeObject(logText); + + sarifLog.Runs[0].Artifacts[0].Contents?.Text.Should().NotBeNullOrEmpty(); + } + } + } + + [Fact] + public void SarifLogger_WritesFileContentsForAnalysisTargets() + { + var sb = new StringBuilder(); + + // Create a temporary file whose extension signals that it is textual. + // This ensures that the ArtifactContents.Text property, rather than + // the Binary property, is populated, so the test of the Text property + // at the end will work. + using (var tempFile = new TempFile(".txt")) + { + string tempFilePath = tempFile.Name; + string tempFileDirectory = Path.GetDirectoryName(tempFilePath); + string tempFileName = Path.GetFileName(tempFilePath); + + File.WriteAllText(tempFilePath, "#include \"windows.h\";"); + + var run = new Run + { + OriginalUriBaseIds = new Dictionary + { + [TempFileBaseId] = new ArtifactLocation + { + Uri = new Uri(tempFileDirectory, UriKind.Absolute) + } + }, + + // To get text contents, we also need to specify an encoding that + // Encoding.GetEncoding() will accept. + DefaultEncoding = "UTF-8" + }; + + var rule = new ReportingDescriptor + { + Id = TestData.TestRuleId + }; + + // Create a result that refers to an artifact whose location is specified + // by a relative reference together with a uriBaseId. + var result = new Result + { + RuleId = rule.Id, + Message = new Message { Text = "Testing." }, + Locations = new List + { + new Location + { + PhysicalLocation = new PhysicalLocation + { + ArtifactLocation = new ArtifactLocation + { + Uri = new Uri(tempFileName, UriKind.Relative), + UriBaseId = TempFileBaseId + } + } + } + } + }; + + using (var textWriter = new StringWriter(sb)) + { + // Create a logger that inserts artifact contents. + using (var sarifLogger = new SarifLogger( + textWriter, + run: run, + dataToInsert: OptionallyEmittedData.TextFiles)) + { + sarifLogger.Log(rule, result); + } + + // The logger should have populated the artifact contents. + string logText = sb.ToString(); + SarifLog sarifLog = JsonConvert.DeserializeObject(logText); + + sarifLog.Runs[0].Artifacts[0].Contents?.Text.Should().NotBeNullOrEmpty(); + } + } + } + [Fact] public void SarifLogger_WritesFileDataWithUnrecognizedEncoding() { @@ -552,7 +680,7 @@ public void SarifLogger_ScrapesFilesFromResult() } [Fact] - public void SarifLogger_DoNotScrapeFilesFromNotifications() + public void SarifLogger_DoesNotScrapeFilesFromNotifications() { var sb = new StringBuilder(); @@ -724,6 +852,108 @@ public void SarifLogger_AcceptsSubrulesInResultRuleId() } } + [Fact] + public void SarifLogger_EnhancesRunWithInvocation() + { + // Start off with a run that doesn't contain any Invocations. + var run = new Run(); + + var sb = new StringBuilder(); + + using (var textWriter = new StringWriter(sb)) + { + using (var sarifLogger = new SarifLogger( + textWriter, + run: run)) + { + } + } + + string logText = sb.ToString(); + SarifLog sarifLog = JsonConvert.DeserializeObject(logText); + + // The logger should have added an Invocation. + sarifLog.Runs[0].Invocations?.Count.Should().Be(1); + } + + [Fact] + public void SarifLogger_EnhancesRunWithAdditionalAnalysisTargets() + { + // Start off with a run that contains some artifacts. + var run = new Run + { + Artifacts = new List + { + new Artifact + { + Location = new ArtifactLocation { Uri = new Uri("1.c", UriKind.Relative) } + }, + new Artifact + { + Location = new ArtifactLocation { Uri = new Uri("2.c", UriKind.Relative) } + } + } + }; + + // Pass in additional analysis targets. + var analysisTargets = new List + { + "3.c", + "4.c" + }; + + var sb = new StringBuilder(); + + using (var textWriter = new StringWriter(sb)) + { + using (var sarifLogger = new SarifLogger( + textWriter, + run: run, + analysisTargets: analysisTargets)) + { + } + } + + string logText = sb.ToString(); + SarifLog sarifLog = JsonConvert.DeserializeObject(logText); + + // The logger should have merged the analysis targets into the existing Artifacts array. + IList artifacts = sarifLog.Runs[0].Artifacts; + artifacts.Count.Should().Be(4); + } + + [Fact] + public void SarifLogger_AcceptsOverrideOfDefaultEncoding() + { + const string Utf8 = "UTF-8"; + const string Utf7 = "UTF-7"; + + // Start off with a run that specifies the default file encoding. + var run = new Run + { + DefaultEncoding = Utf8 + }; + + var sb = new StringBuilder(); + + using (var textWriter = new StringWriter(sb)) + { + // Create a logger that uses that run but specifies a different encoding. + using (var sarifLogger = new SarifLogger( + textWriter, + run: run, + defaultFileEncoding: Utf7)) + { + } + } + + string logText = sb.ToString(); + SarifLog sarifLog = JsonConvert.DeserializeObject(logText); + + // The logger accepted the override for default file encoding. + sarifLog.Runs[0].DefaultEncoding.Should().Be(Utf7); + } + private void LogSimpleResult(SarifLogger sarifLogger) { ReportingDescriptor rule = new ReportingDescriptor { Id = "RuleId" }; diff --git a/src/Test.Utilities.Sarif/DeletesOutputsDirectoryOnClassInitializationFixture.cs b/src/Test.Utilities.Sarif/DeletesOutputsDirectoryOnClassInitializationFixture.cs index c23303a1d..1e1982b99 100644 --- a/src/Test.Utilities.Sarif/DeletesOutputsDirectoryOnClassInitializationFixture.cs +++ b/src/Test.Utilities.Sarif/DeletesOutputsDirectoryOnClassInitializationFixture.cs @@ -25,7 +25,7 @@ namespace Microsoft.CodeAnalysis.Sarif /// it wants to override the virtual TypeUnderTest or OutputFolderPath properties, but there seems /// no good reason to do this. /// - public abstract class DeletesOutputsDirectoryOnClassInitializationFixture + public class DeletesOutputsDirectoryOnClassInitializationFixture { protected virtual string TypeUnderTest => this.GetType().Name.Substring(0, this.GetType().Name.Length - "TestsFixture".Length); diff --git a/src/Test.Utilities.Sarif/TestData.cs b/src/Test.Utilities.Sarif/TestData.cs index 152ef5e73..67e8231e5 100644 --- a/src/Test.Utilities.Sarif/TestData.cs +++ b/src/Test.Utilities.Sarif/TestData.cs @@ -16,6 +16,7 @@ public static class TestData public const string TestMessageStringId = "testMessageStringId"; public const string TestAnalysisTarget = @"C:\dir\file"; public const string NotActuallyASecret = nameof(NotActuallyASecret); + public const string TestRootBaseId = "TEST_ROOT"; public const string AutomationDetailsGuid = "D41BF9F2-225D-4254-984E-DFD659702E4D"; public const string ConverterName = "TestConverter";