diff --git a/src/ReleaseHistory.md b/src/ReleaseHistory.md index c31b155c5..ffbaf72b1 100644 --- a/src/ReleaseHistory.md +++ b/src/ReleaseHistory.md @@ -9,6 +9,7 @@ * BUGFIX: Fix `ArgumentException` when `--recurse` is enabled and two file target specifiers generates the same file path. [#2438](https://github.com/microsoft/sarif-sdk/pull/2438) * BUGFIX: Fix 'InvalidOperationException' with message `Collection was modified; enumeration operation may not execute` in `MultithreadedAnalyzeCommandBase`, which is raised when analyzing with the `--hashes` switch. [#2447](https://github.com/microsoft/sarif-sdk/pull/2447) * BUGFIX: Fix `Merge` command produces empty SARIF file in Linux when providing file name only without path. [#2408](https://github.com/microsoft/sarif-sdk/pull/2408) +* FEATURE: Add `--sort-results` argument to the `rewrite` command to get sorted SARIF results. [#2422](https://github.com/microsoft/sarif-sdk/pull/2422) * BUGFIX: Fix `NullReferenceException` when filing work item with a SARIF file which has no filable results. [#2412](https://github.com/microsoft/sarif-sdk/pull/2412) * BUGFIX: Fix missing `endLine` and `endColumn` properties and remove vulnerable packages for ESLint SARIF formatter. [#2458](https://github.com/microsoft/sarif-sdk/pull/2458) diff --git a/src/Sarif.Multitool.Library/RewriteCommand.cs b/src/Sarif.Multitool.Library/RewriteCommand.cs index f2675e34f..c98afc99c 100644 --- a/src/Sarif.Multitool.Library/RewriteCommand.cs +++ b/src/Sarif.Multitool.Library/RewriteCommand.cs @@ -59,6 +59,11 @@ public int Run(RewriteOptions options) SarifLog reformattedLog = new RemoveOptionalDataVisitor(dataToRemove).VisitSarifLog(actualLog); reformattedLog = new InsertOptionalDataVisitor(dataToInsert, originalUriBaseIds, insertProperties: options.InsertProperties).VisitSarifLog(reformattedLog); + if (options.SortResults) + { + reformattedLog = new SortingVisitor().VisitSarifLog(reformattedLog); + } + if (options.SarifOutputVersion == SarifVersion.OneZeroZero) { var visitor = new SarifCurrentToVersionOneVisitor(); diff --git a/src/Sarif.Multitool.Library/RewriteOptions.cs b/src/Sarif.Multitool.Library/RewriteOptions.cs index a98e70b64..b51fab304 100644 --- a/src/Sarif.Multitool.Library/RewriteOptions.cs +++ b/src/Sarif.Multitool.Library/RewriteOptions.cs @@ -10,5 +10,11 @@ namespace Microsoft.CodeAnalysis.Sarif.Multitool [Verb("rewrite", HelpText = "Enrich a SARIF file with additional data.")] public class RewriteOptions : SingleFileOptionsBase { + [Option( + 's', + "sort-results", + Default = false, + HelpText = "Sort results in the final output file.")] + public bool SortResults { get; set; } } } diff --git a/src/Sarif/Comparers/ArtifactComparer.cs b/src/Sarif/Comparers/ArtifactComparer.cs new file mode 100644 index 000000000..f651a4294 --- /dev/null +++ b/src/Sarif/Comparers/ArtifactComparer.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class ArtifactComparer : IComparer + { + internal static readonly ArtifactComparer Instance = new ArtifactComparer(); + + public int Compare(Artifact left, Artifact right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = MessageComparer.Instance.Compare(left.Description, right.Description); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = ArtifactLocationComparer.Instance.Compare(left.Location, right.Location); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.ParentIndex.CompareTo(right.ParentIndex); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Offset.CompareTo(right.Offset); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Length.CompareTo(right.Length); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Roles.CompareTo(right.Roles); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.MimeType, right.MimeType); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = ArtifactContentComparer.Instance.Compare(left.Contents, right.Contents); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Encoding, right.Encoding); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.SourceLanguage, right.SourceLanguage); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Hashes.DictionaryCompares(right.Hashes); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.LastModifiedTimeUtc.CompareTo(right.LastModifiedTimeUtc); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/ArtifactContentComparer.cs b/src/Sarif/Comparers/ArtifactContentComparer.cs new file mode 100644 index 000000000..9ef7f9ae3 --- /dev/null +++ b/src/Sarif/Comparers/ArtifactContentComparer.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class ArtifactContentComparer : IComparer + { + internal static readonly ArtifactContentComparer Instance = new ArtifactContentComparer(); + + public int Compare(ArtifactContent left, ArtifactContent right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = string.Compare(left.Text, right.Text); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Binary, right.Binary); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = MultiformatMessageStringComparer.Instance.Compare(left.Rendered, right.Rendered); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/ArtifactLocationComparer.cs b/src/Sarif/Comparers/ArtifactLocationComparer.cs new file mode 100644 index 000000000..2d9313387 --- /dev/null +++ b/src/Sarif/Comparers/ArtifactLocationComparer.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class ArtifactLocationComparer : IComparer + { + internal static readonly ArtifactLocationComparer Instance = new ArtifactLocationComparer(); + + public int Compare(ArtifactLocation left, ArtifactLocation right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = left.Uri.UriCompares(right.Uri); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.UriBaseId, right.UriBaseId); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Index.CompareTo(right.Index); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = MessageComparer.Instance.Compare(left.Description, right.Description); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/CodeFlowComparer.cs b/src/Sarif/Comparers/CodeFlowComparer.cs new file mode 100644 index 000000000..7596b73d7 --- /dev/null +++ b/src/Sarif/Comparers/CodeFlowComparer.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class CodeFlowComparer : IComparer + { + internal static readonly CodeFlowComparer Instance = new CodeFlowComparer(); + + public int Compare(CodeFlow left, CodeFlow right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = MessageComparer.Instance.Compare(left.Message, right.Message); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.ThreadFlows.ListCompares(right.ThreadFlows, ThreadFlowComparer.Instance); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/ComparerHelpers.cs b/src/Sarif/Comparers/ComparerHelpers.cs new file mode 100644 index 000000000..e40d7e0a7 --- /dev/null +++ b/src/Sarif/Comparers/ComparerHelpers.cs @@ -0,0 +1,168 @@ +// 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.Linq; + +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal static class ComparerExtensions + { + /// Compare 2 object by references. + /// Return value 'true' presents you can get a definite compare result. + /// The out parameter 'result' presents compare result: + /// 0 if both objects are the same or both are null. + /// -1 if the first object is null and the second object is not null. + /// 1 if the first object is not null and the second object is null. + /// Return value 'false' presents the need to compare objects other properties to get the final result. + public static bool TryReferenceCompares(this object left, object right, out int result) + { + result = 0; + + if (object.ReferenceEquals(left, right)) + { + return true; + } + + if (left == null) + { + result = -1; + return true; + } + + if (right == null) + { + result = 1; + return true; + } + + return false; + } + + public static int ListCompares(this IList left, IList right) where T : IComparable + { + return CompareListHelper(left, right, (a, b) => a.CompareTo(b)); + } + + public static int ListCompares(this IList left, IList right, IComparer comparer) + { + if (comparer == null) + { + throw new ArgumentNullException(nameof(comparer)); + } + + return CompareListHelper(left, right, comparer.Compare); + } + + private static int CompareListHelper(IList left, IList right, Func compareFunction) + { + if (compareFunction == null) + { + throw new ArgumentNullException(nameof(compareFunction)); + } + + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = left.Count.CompareTo(right.Count); + + if (compareResult != 0) + { + return compareResult; + } + + for (int i = 0; i < left.Count; ++i) + { + if (left[i].TryReferenceCompares(right[i], out compareResult) && compareResult != 0) + { + return compareResult; + } + + compareResult = compareFunction(left[i], right[i]); + + if (compareResult != 0) + { + return compareResult; + } + } + + return compareResult; + } + + public static int DictionaryCompares(this IDictionary left, IDictionary right) where T : IComparable + { + return DictionaryCompareHelper(left, right, (a, b) => a.CompareTo(b)); + } + + public static int DictionaryCompares(this IDictionary left, IDictionary right, IComparer comparer) + { + if (comparer == null) + { + throw new ArgumentNullException(nameof(comparer)); + } + + return DictionaryCompareHelper(left, right, comparer.Compare); + } + + private static int DictionaryCompareHelper(IDictionary left, IDictionary right, Func compareFunction) + { + if (compareFunction == null) + { + throw new ArgumentNullException(nameof(compareFunction)); + } + + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = left.Count.CompareTo(right.Count); + + if (compareResult != 0) + { + return compareResult; + } + + IList leftKeys = left.Keys.ToList(); + IList rightKeys = right.Keys.ToList(); + + for (int i = 0; i < leftKeys.Count; ++i) + { + compareResult = leftKeys[i].CompareTo(rightKeys[i]); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = compareFunction(left[leftKeys[i]], right[rightKeys[i]]); + + if (compareResult != 0) + { + return compareResult; + } + } + + return compareResult; + } + + public static int UriCompares(this Uri left, Uri right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + return left.OriginalString.CompareTo(right.OriginalString); + } + } +} diff --git a/src/Sarif/Comparers/LocationComparer.cs b/src/Sarif/Comparers/LocationComparer.cs new file mode 100644 index 000000000..8c89603ba --- /dev/null +++ b/src/Sarif/Comparers/LocationComparer.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class LocationComparer : IComparer + { + internal static readonly LocationComparer Instance = new LocationComparer(); + + public int Compare(Location left, Location right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = left.Id.CompareTo(right.Id); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = PhysicalLocationComparer.Instance.Compare(left.PhysicalLocation, right.PhysicalLocation); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.LogicalLocations.ListCompares(right.LogicalLocations, LogicalLocationComparer.Instance); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/LogicalLocationComparer.cs b/src/Sarif/Comparers/LogicalLocationComparer.cs new file mode 100644 index 000000000..107d6d779 --- /dev/null +++ b/src/Sarif/Comparers/LogicalLocationComparer.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class LogicalLocationComparer : IComparer + { + internal static readonly LogicalLocationComparer Instance = new LogicalLocationComparer(); + + public int Compare(LogicalLocation left, LogicalLocation right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = string.Compare(left.Name, right.Name); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Index.CompareTo(right.Index); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.FullyQualifiedName, right.FullyQualifiedName); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.DecoratedName, right.DecoratedName); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.ParentIndex.CompareTo(right.ParentIndex); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Kind, right.Kind); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/MessageComparer.cs b/src/Sarif/Comparers/MessageComparer.cs new file mode 100644 index 000000000..9d8f75718 --- /dev/null +++ b/src/Sarif/Comparers/MessageComparer.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class MessageComparer : IComparer + { + internal static readonly MessageComparer Instance = new MessageComparer(); + + public int Compare(Message left, Message right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = string.Compare(left.Text, right.Text); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Markdown, right.Markdown); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Id, right.Id); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Arguments.ListCompares(right.Arguments); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/MultiformatMessageStringComparer.cs b/src/Sarif/Comparers/MultiformatMessageStringComparer.cs new file mode 100644 index 000000000..d5055cf6e --- /dev/null +++ b/src/Sarif/Comparers/MultiformatMessageStringComparer.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class MultiformatMessageStringComparer : IComparer + { + internal static readonly MultiformatMessageStringComparer Instance = new MultiformatMessageStringComparer(); + + public int Compare(MultiformatMessageString left, MultiformatMessageString right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = string.Compare(left.Text, right.Text); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Markdown, right.Markdown); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/PhysicalLocationComparer.cs b/src/Sarif/Comparers/PhysicalLocationComparer.cs new file mode 100644 index 000000000..90c1e0bb9 --- /dev/null +++ b/src/Sarif/Comparers/PhysicalLocationComparer.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class PhysicalLocationComparer : IComparer + { + internal static readonly PhysicalLocationComparer Instance = new PhysicalLocationComparer(); + + public int Compare(PhysicalLocation left, PhysicalLocation right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = ArtifactLocationComparer.Instance.Compare(left.ArtifactLocation, right.ArtifactLocation); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = RegionComparer.Instance.Compare(left.Region, right.Region); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = RegionComparer.Instance.Compare(left.ContextRegion, right.ContextRegion); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/RegionComparer.cs b/src/Sarif/Comparers/RegionComparer.cs new file mode 100644 index 000000000..9f03675c2 --- /dev/null +++ b/src/Sarif/Comparers/RegionComparer.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class RegionComparer : IComparer + { + internal static readonly RegionComparer Instance = new RegionComparer(); + + public int Compare(Region left, Region right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = left.StartLine.CompareTo(right.StartLine); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.StartColumn.CompareTo(right.StartColumn); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.EndLine.CompareTo(right.EndLine); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.EndColumn.CompareTo(right.EndColumn); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.CharOffset.CompareTo(right.CharOffset); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.CharLength.CompareTo(right.CharLength); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.ByteOffset.CompareTo(right.ByteOffset); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.ByteLength.CompareTo(right.ByteLength); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/ReportingConfigurationComparer.cs b/src/Sarif/Comparers/ReportingConfigurationComparer.cs new file mode 100644 index 000000000..aa4d6973d --- /dev/null +++ b/src/Sarif/Comparers/ReportingConfigurationComparer.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class ReportingConfigurationComparer : IComparer + { + internal static readonly ReportingConfigurationComparer Instance = new ReportingConfigurationComparer(); + + public int Compare(ReportingConfiguration left, ReportingConfiguration right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = left.Enabled.CompareTo(right.Enabled); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Level.CompareTo(right.Level); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Rank.CompareTo(right.Rank); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/ReportingDescriptorComparer.cs b/src/Sarif/Comparers/ReportingDescriptorComparer.cs new file mode 100644 index 000000000..af0c7d0f0 --- /dev/null +++ b/src/Sarif/Comparers/ReportingDescriptorComparer.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class ReportingDescriptorComparer : IComparer + { + internal static readonly ReportingDescriptorComparer Instance = new ReportingDescriptorComparer(); + + public int Compare(ReportingDescriptor left, ReportingDescriptor right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = string.Compare(left.Id, right.Id); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.DeprecatedIds.ListCompares(right.DeprecatedIds); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Guid, right.Guid); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.DeprecatedGuids.ListCompares(right.DeprecatedGuids); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Name, right.Name); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.DeprecatedNames.ListCompares(right.DeprecatedNames); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = MultiformatMessageStringComparer.Instance.Compare(left.ShortDescription, right.ShortDescription); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = MultiformatMessageStringComparer.Instance.Compare(left.FullDescription, right.FullDescription); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = ReportingConfigurationComparer.Instance.Compare(left.DefaultConfiguration, right.DefaultConfiguration); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.HelpUri.UriCompares(right.HelpUri); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = MultiformatMessageStringComparer.Instance.Compare(left.Help, right.Help); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/ResultComparer.cs b/src/Sarif/Comparers/ResultComparer.cs new file mode 100644 index 000000000..f010dbcda --- /dev/null +++ b/src/Sarif/Comparers/ResultComparer.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.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class ResultComparer : IComparer + { + internal static readonly ResultComparer Instance = new ResultComparer(); + + public int Compare(Result left, Result right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = string.Compare(left.RuleId, right.RuleId); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.RuleIndex.CompareTo(right.RuleIndex); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Level.CompareTo(right.Level); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Kind.CompareTo(right.Kind); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = MessageComparer.Instance.Compare(left.Message, right.Message); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = ArtifactLocationComparer.Instance.Compare(left.AnalysisTarget, right.AnalysisTarget); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Locations.ListCompares(right.Locations, LocationComparer.Instance); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Guid, right.Guid); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.CorrelationGuid, right.CorrelationGuid); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.OccurrenceCount.CompareTo(right.OccurrenceCount); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.CodeFlows.ListCompares(right.CodeFlows, CodeFlowComparer.Instance); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.BaselineState.CompareTo(right.BaselineState); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Rank.CompareTo(right.Rank); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/RunComparer.cs b/src/Sarif/Comparers/RunComparer.cs new file mode 100644 index 000000000..4110a949e --- /dev/null +++ b/src/Sarif/Comparers/RunComparer.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class RunComparer : IComparer + { + internal static readonly RunComparer Instance = new RunComparer(); + + public int Compare(Run left, Run right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = left.Artifacts.ListCompares(right.Artifacts, ArtifactComparer.Instance); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = ToolComponentComparer.Instance.Compare(left?.Tool?.Driver, right?.Tool?.Driver); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Results.ListCompares(right.Results, ResultComparer.Instance); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/ThreadFlowComparer.cs b/src/Sarif/Comparers/ThreadFlowComparer.cs new file mode 100644 index 000000000..670c891f8 --- /dev/null +++ b/src/Sarif/Comparers/ThreadFlowComparer.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class ThreadFlowComparer : IComparer + { + internal static readonly ThreadFlowComparer Instance = new ThreadFlowComparer(); + + public int Compare(ThreadFlow left, ThreadFlow right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = string.Compare(left.Id, right.Id); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = MessageComparer.Instance.Compare(left.Message, right.Message); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Locations.ListCompares(right.Locations, ThreadFlowLocationComparer.Instance); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/ThreadFlowLocationComparer.cs b/src/Sarif/Comparers/ThreadFlowLocationComparer.cs new file mode 100644 index 000000000..c0d61f331 --- /dev/null +++ b/src/Sarif/Comparers/ThreadFlowLocationComparer.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class ThreadFlowLocationComparer : IComparer + { + internal static readonly ThreadFlowLocationComparer Instance = new ThreadFlowLocationComparer(); + + public int Compare(ThreadFlowLocation left, ThreadFlowLocation right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = left.Index.CompareTo(right.Index); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = LocationComparer.Instance.Compare(left.Location, right.Location); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Kinds.ListCompares(right.Kinds); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.NestingLevel.CompareTo(right.NestingLevel); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.ExecutionOrder.CompareTo(right.ExecutionOrder); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.ExecutionTimeUtc.CompareTo(right.ExecutionTimeUtc); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Importance.CompareTo(right.Importance); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Comparers/ToolComponentComparer.cs b/src/Sarif/Comparers/ToolComponentComparer.cs new file mode 100644 index 000000000..7f3cfbc2d --- /dev/null +++ b/src/Sarif/Comparers/ToolComponentComparer.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +/// +/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive +/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution. +/// Tracking by issue: https://github.com/microsoft/jschema/issues/141 +/// + +namespace Microsoft.CodeAnalysis.Sarif.Comparers +{ + internal class ToolComponentComparer : IComparer + { + internal static readonly ToolComponentComparer Instance = new ToolComponentComparer(); + + public int Compare(ToolComponent left, ToolComponent right) + { + int compareResult = 0; + + if (left.TryReferenceCompares(right, out compareResult)) + { + return compareResult; + } + + compareResult = string.Compare(left.Guid, right.Guid); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Name, right.Name); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Organization, right.Organization); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Product, right.Product); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.FullName, right.FullName); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.Version, right.Version); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.SemanticVersion, right.SemanticVersion); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = string.Compare(left.ReleaseDateUtc, right.ReleaseDateUtc); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.DownloadUri.UriCompares(right.DownloadUri); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.InformationUri.UriCompares(right.InformationUri); + + if (compareResult != 0) + { + return compareResult; + } + + compareResult = left.Rules.ListCompares(right.Rules, ReportingDescriptorComparer.Instance); + + if (compareResult != 0) + { + return compareResult; + } + + // Note: There may be other properties are not compared. + return compareResult; + } + } +} diff --git a/src/Sarif/Visitors/SortingVisitor.cs b/src/Sarif/Visitors/SortingVisitor.cs new file mode 100644 index 000000000..544cd6ddf --- /dev/null +++ b/src/Sarif/Visitors/SortingVisitor.cs @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; + +using Microsoft.CodeAnalysis.Sarif.Comparers; + +namespace Microsoft.CodeAnalysis.Sarif.Visitors +{ + public class SortingVisitor : SarifRewritingVisitor + { + private readonly IDictionary ruleIndexMap; + private readonly IDictionary artifactIndexMap; + + public SortingVisitor() + { + this.ruleIndexMap = new Dictionary(); + this.artifactIndexMap = new Dictionary(); + } + + public override SarifLog VisitSarifLog(SarifLog node) + { + SarifLog current = base.VisitSarifLog(node); + + if (current?.Runs != null) + { + current.Runs = current.Runs.OrderBy(r => r, RunComparer.Instance).ToList(); + } + + return current; + } + + public override Run VisitRun(Run node) + { + this.ruleIndexMap.Clear(); + this.artifactIndexMap.Clear(); + + if (node?.Artifacts != null) + { + node.Artifacts = this.SortAndBuildIndexMap(node?.Artifacts, ArtifactComparer.Instance, this.artifactIndexMap); + } + + Run current = base.VisitRun(node); + + if (current?.Results != null) + { + current.Results = current.Results.OrderBy(r => r, ResultComparer.Instance).ToList(); + } + + return current; + } + + public override ToolComponent VisitToolComponent(ToolComponent node) + { + if (node?.Rules != null) + { + node.Rules = this.SortAndBuildIndexMap(node?.Rules, ReportingDescriptorComparer.Instance, this.ruleIndexMap); + } + + return base.VisitToolComponent(node); + } + + public override Result VisitResult(Result node) + { + Result current = base.VisitResult(node); + + if (current != null) + { + if (current.RuleIndex != -1 && this.ruleIndexMap.TryGetValue(current.RuleIndex, out int newIndex)) + { + current.RuleIndex = newIndex; + } + + if (current.Locations != null) + { + current.Locations = current.Locations.OrderBy(r => r, LocationComparer.Instance).ToList(); + } + + if (current.CodeFlows != null) + { + current.CodeFlows = current.CodeFlows.OrderBy(r => r, CodeFlowComparer.Instance).ToList(); + } + } + + return current; + } + + public override CodeFlow VisitCodeFlow(CodeFlow node) + { + CodeFlow current = base.VisitCodeFlow(node); + + if (current?.ThreadFlows != null) + { + current.ThreadFlows = current.ThreadFlows.OrderBy(t => t, ThreadFlowComparer.Instance).ToList(); + } + + return current; + } + + public override ThreadFlow VisitThreadFlow(ThreadFlow node) + { + ThreadFlow current = base.VisitThreadFlow(node); + + if (current?.Locations != null) + { + current.Locations = current.Locations.OrderBy(t => t, ThreadFlowLocationComparer.Instance).ToList(); + } + + return current; + } + + public override Location VisitLocation(Location node) + { + Location current = base.VisitLocation(node); + + if (current?.LogicalLocations != null) + { + current.LogicalLocations = current.LogicalLocations.OrderBy(t => t, LogicalLocationComparer.Instance).ToList(); + } + + return current; + } + + public override ArtifactLocation VisitArtifactLocation(ArtifactLocation node) + { + ArtifactLocation current = base.VisitArtifactLocation(node); + + if (current.Index != -1 && this.artifactIndexMap.TryGetValue(current.Index, out int newIndex)) + { + current.Index = newIndex; + } + + return current; + } + + private IList SortAndBuildIndexMap(IList list, IComparer comparer, IDictionary indexMapping) + { + if (list != null) + { + IDictionary unsortedIndices = this.CacheListIndices(list); + + list = list.OrderBy(r => r, comparer).ToList(); + + this.MapNewIndices(list, unsortedIndices, indexMapping); + + unsortedIndices.Clear(); + } + + return list; + } + + private IDictionary CacheListIndices(IList list) + { + // Assume each item in the list is unique (has different reference). + // According to sarif-2.1.0-rtm.5.json, artifacts array of runs and rules array of toolComponent + // are defined as "uniqueItems". + var dict = new Dictionary(capacity: list.Count); + + for (int i = 0; i < list.Count; i++) + { + dict[list[i]] = i; + } + + return dict; + } + + private void MapNewIndices(IList newList, IDictionary oldIndices, IDictionary indexMapping) + { + for (int newIndex = 0; newIndex < newList.Count; newIndex++) + { + if (oldIndices.TryGetValue(newList[newIndex], out int oldIndex)) + { + indexMapping[oldIndex] = newIndex; + } + } + } + } +} diff --git a/src/Test.UnitTests.Sarif.Multitool/RewriteCommandTests.cs b/src/Test.UnitTests.Sarif.Multitool/RewriteCommandTests.cs new file mode 100644 index 000000000..3638c34a3 --- /dev/null +++ b/src/Test.UnitTests.Sarif.Multitool/RewriteCommandTests.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; + +using CommandLine; + +using FluentAssertions; + +using Xunit; + +namespace Microsoft.CodeAnalysis.Sarif.Multitool +{ + public class RewriteCommandTests + { + [Fact] + public void FileWorkItemsCommand_ArgumentsTests() + { + foreach (dynamic testCase in s_testCases) + { + this.VerifyCommandLineOptions(testCase.Args, testCase.Valid, testCase.ExpectedOptions); + } + } + + private void VerifyCommandLineOptions(IEnumerable args, bool valid, RewriteOptions expected) + { + Parser parser = Parser.Default; + ParserResult result = parser.ParseArguments(args); + + if (valid) + { + result.Should().BeOfType>(); + result.Should().NotBeNull(); + RewriteOptions parsedResult = ((Parsed)result).Value; + parsedResult.Should().NotBeNull(); + parsedResult.Should().BeOfType(); + parsedResult.Should().BeEquivalentTo(expected); + } + else + { + result.Should().BeOfType>(); + result.Should().NotBeNull(); + IEnumerable errors = ((NotParsed)result).Errors; + errors.Should().NotBeEmpty(); + } + } + + private static RewriteOptions SetEmptyValue(RewriteOptions option) + { + option.DataToInsert ??= new List(); + option.DataToRemove ??= new List(); + option.UriBaseIds ??= new List(); + option.InsertProperties ??= new List(); + return option; + } + + private static readonly dynamic[] s_testCases = + new[] { + new { + Title = "rewrite pass case", + Args = new string[] { + "test.sarif", + "--output", + "updated.sarif", + "--remove", + "VersionControlDetails;NondeterministicProperties", + "--sort-results", + "--force" + }, + Valid = true, + ExpectedOptions = SetEmptyValue(new RewriteOptions + { + InputFilePath = "test.sarif", + OutputFilePath = "updated.sarif", + DataToRemove = new List + { + OptionallyEmittedData.VersionControlDetails, + OptionallyEmittedData.NondeterministicProperties + }, + SortResults = true, + Force = true, + }), + }, + new { + Title = "pass case: different ordering", + Args = new string[] { + "--output", + "updated.sarif", + "test.sarif", + "--force", + "--sort-results", + }, + Valid = true, + ExpectedOptions = SetEmptyValue(new RewriteOptions + { + InputFilePath = "test.sarif", + OutputFilePath = "updated.sarif", + SortResults = true, + Force = true, + }), + }, + new { + Title = "pass case: use short argument name", + Args = new string[] { + "-o", + "updated.sarif", + "test.sarif", + "-s", + "-f" + }, + Valid = true, + ExpectedOptions = SetEmptyValue(new RewriteOptions + { + InputFilePath = "test.sarif", + OutputFilePath = "updated.sarif", + SortResults = true, + Force = true, + }), + }, + new { + Title = "fail case: argument value not provided", + Args = new string[] { + "test.sarif", + "--output", + "updated.sarif", + "--remove", + "--sort-results", + "--force" + }, + Valid = false, + ExpectedOptions = (RewriteOptions)null, + }, + new { + Title = "fail case: wrong arg name --sort-result(s)", + Args = new string[] { + "test.sarif", + "--output", + "updated.sarif", + "--sort-result", + "--force" + }, + Valid = false, + ExpectedOptions = (RewriteOptions)null, + }, + }; + } +} diff --git a/src/Test.UnitTests.Sarif/Comparers/ComparersTests.cs b/src/Test.UnitTests.Sarif/Comparers/ComparersTests.cs new file mode 100644 index 000000000..226faa5b3 --- /dev/null +++ b/src/Test.UnitTests.Sarif/Comparers/ComparersTests.cs @@ -0,0 +1,807 @@ +// 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.Linq; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.Sarif; +using Microsoft.CodeAnalysis.Sarif.Comparers; +using Microsoft.CodeAnalysis.Test.Utilities.Sarif; + +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.Test.UnitTests.Sarif.Comparers +{ + public class ComparersTests + { + private readonly ITestOutputHelper output; + + public ComparersTests(ITestOutputHelper outputHelper) + { + this.output = outputHelper; + } + + [Fact] + public void CompareList_Shuffle_Tests() + { + Random random = RandomSarifLogGenerator.GenerateRandomAndLog(this.output); + + IList originalList = Enumerable.Range(-100, 200).ToList(); + + IList shuffledList = originalList.ToList().Shuffle(random); + + int result = 0, newResult = 0; + + result = originalList.ListCompares(shuffledList); + result.Should().NotBe(0); + + newResult = shuffledList.ListCompares(originalList); + newResult.Should().Be(result * -1); + + IList sortedList = shuffledList.OrderBy(i => i).ToList(); + + result = originalList.ListCompares(sortedList); + result.Should().Be(0); + + newResult = originalList.ListCompares(sortedList); + newResult.Should().Be(0); + } + + [Fact] + public void CompareList_BothNull_Tests() + { + IList list1 = null; + IList list2 = null; + + list1.ListCompares(list2).Should().Be(0); + list2.ListCompares(list1).Should().Be(0); + } + + [Fact] + public void CompareList_CompareNullToNotNull_Tests() + { + IList list1 = null; + IList list2 = Enumerable.Range(-10, 20).ToList(); + + list1.ListCompares(list2).Should().Be(-1); + list2.ListCompares(list1).Should().Be(1); + } + + [Fact] + public void CompareList_DifferentCount_Tests() + { + IList list1 = Enumerable.Range(0, 11).ToList(); + IList list2 = Enumerable.Range(0, 10).ToList(); + + list1.ListCompares(list2).Should().Be(1); + list2.ListCompares(list1).Should().Be(-1); + } + + [Fact] + public void CompareList_SameCountDifferentElement_Tests() + { + IList list1 = Enumerable.Range(0, 10).ToList(); + IList list2 = Enumerable.Range(1, 10).ToList(); + + list1.ListCompares(list2).Should().Be(-1); + list2.ListCompares(list1).Should().Be(1); + } + + [Fact] + public void CompareList_WithNullComparer_Tests() + { + var tool = new ToolComponent { Guid = Guid.Empty.ToString() }; + + IList runs1 = new[] { new Run { Tool = new Tool { Driver = tool } } }; + IList runs2 = Array.Empty(); + + Action act = () => runs1.ListCompares(runs2, comparer: null); + act.Should().Throw(); + } + + [Fact] + public void CompareList_WithComparer_Tests() + { + IList run1 = new List + { + null, + new Run { Tool = new Tool { Driver = new ToolComponent { Guid = Guid.NewGuid().ToString() } } } + }; + + IList run2 = new List + { + new Run { Tool = new Tool { Driver = new ToolComponent { Guid = Guid.NewGuid().ToString() } } }, + null + }; + + int result = run1.ListCompares(run2, RunComparer.Instance); + result.Should().Be(-1); + + result = run2.ListCompares(run1, RunComparer.Instance); + result.Should().Be(1); + } + + [Fact] + public void CompareDictionary_Shuffle_Tests() + { + var dict1 = new Dictionary + { + { "b", "b" }, + { "c", "c" }, + { "d", "d" } + }; + + var dict2 = new Dictionary + { + { "d", "d" }, + { "c", "c" }, + { "b", "b" } + }; + + int result = dict1.DictionaryCompares(dict2); + result.Should().Be(-1); + + result = dict2.DictionaryCompares(dict1); + result.Should().Be(1); + + dict2 = new Dictionary + { + { "a", "a" }, + { "b", "b" }, + { "c", "c" } + }; + + result = dict1.DictionaryCompares(dict2); + result.Should().Be(1); + + result = dict2.DictionaryCompares(dict1); + result.Should().Be(-1); + + dict2 = new Dictionary + { + { "b", "a" }, + { "c", "c" }, + { "d", "d" } + }; + + result = dict1.DictionaryCompares(dict2); + result.Should().Be(1); + + result = dict2.DictionaryCompares(dict1); + result.Should().Be(-1); + } + + [Fact] + public void CompareDictionary_BothNull_Tests() + { + IDictionary dict1 = null; + IDictionary dict2 = null; + + dict1.DictionaryCompares(dict2).Should().Be(0); + dict2.DictionaryCompares(dict1).Should().Be(0); + } + + [Fact] + public void CompareDictionary_CompareNullToNotNull_Tests() + { + IDictionary dict1 = null; + IDictionary dict2 = new Dictionary() { { "a", "a" } }; + + dict1.DictionaryCompares(dict2).Should().Be(-1); + dict2.DictionaryCompares(dict1).Should().Be(1); + } + + [Fact] + public void CompareDictionary_DifferentCount_Tests() + { + var dict1 = new Dictionary() { { "a", "a" }, { "b", "b" } }; + var dict2 = new Dictionary() { { "c", "c" } }; + + dict1.DictionaryCompares(dict2).Should().Be(1); + dict2.DictionaryCompares(dict1).Should().Be(-1); + } + + [Fact] + public void CompareDictionary_SameCountDifferentElement_Tests() + { + var dict1 = new Dictionary() { { "a", "a" }, { "b", "b" } }; + var dict2 = new Dictionary() { { "c", "c" }, { "d", "d" } }; + + dict1.DictionaryCompares(dict2).Should().Be(-1); + dict2.DictionaryCompares(dict1).Should().Be(1); + + dict1 = new Dictionary() { { "a", "a" }, { "b", "b" }, { "c", "c" } }; + dict2 = new Dictionary() { { "c", "c" }, { "b", "b" }, { "a", "a" } }; + + dict1.DictionaryCompares(dict2).Should().Be(-1); + dict2.DictionaryCompares(dict1).Should().Be(1); + } + + [Fact] + public void CompareDictionary_WithNullComparer_Tests() + { + var loc1 = new Dictionary(); + var loc2 = new Dictionary(); + + Action act = () => loc1.DictionaryCompares(loc2, comparer: null); + act.Should().Throw(); + } + + [Fact] + public void CompareDictionary_WithComparer_Tests() + { + var loc1 = new Dictionary + { + { "1", null }, + { "2", new Location { Id = 2 } } + }; + + var loc2 = new Dictionary + { + { "1", new Location { Id = 1 } }, + { "2", new Location { Id = 2 } } + }; + + int result = loc1.DictionaryCompares(loc2, LocationComparer.Instance); + result.Should().Be(-1); + + result = loc2.DictionaryCompares(loc1, LocationComparer.Instance); + result.Should().Be(1); + } + + [Fact] + public void ArtifactContentComparer_Tests() + { + var list1 = new List(); + var list2 = new List(); + + list1.Add(null); + list2.Add(null); + list1.ListCompares(list2, ArtifactContentComparer.Instance).Should().Be(0); + + list1.Add(new ArtifactContent() { Text = "content 1" }); + list2.Add(new ArtifactContent() { Text = "content 2" }); + + list1.ListCompares(list2, ArtifactContentComparer.Instance).Should().Be(-1); + list2.ListCompares(list1, ArtifactContentComparer.Instance).Should().Be(1); + list1.Clear(); + list2.Clear(); + + list1.Add(new ArtifactContent() { Binary = "WUJDMTIz" }); + list2.Add(new ArtifactContent() { Binary = "QUJDMTIz" }); + + list1.ListCompares(list2, ArtifactContentComparer.Instance).Should().Be(1); + list2.ListCompares(list1, ArtifactContentComparer.Instance).Should().Be(-1); + list1.Clear(); + list2.Clear(); + + list1.Add(new ArtifactContent() { Text = "content 1", Rendered = new MultiformatMessageString { Markdown = "`markdown`" } }); + list2.Add(new ArtifactContent() { Text = "content 1", Rendered = new MultiformatMessageString { Markdown = "title" } }); + list1.ListCompares(list2, ArtifactContentComparer.Instance).Should().Be(-1); + list2.ListCompares(list1, ArtifactContentComparer.Instance).Should().Be(1); + } + + [Fact] + public void ReportingConfigurationComparer_Tests() + { + var rules1 = new List(); + var rules2 = new List(); + + rules1.Add(null); + rules2.Add(null); + + rules1.ListCompares(rules2, ReportingConfigurationComparer.Instance).Should().Be(0); + + rules1.Add(new ReportingConfiguration() { Rank = 26.648d }); + rules2.Add(new ReportingConfiguration() { Rank = 87.1d }); + + rules1.ListCompares(rules2, ReportingConfigurationComparer.Instance).Should().Be(-1); + rules2.ListCompares(rules1, ReportingConfigurationComparer.Instance).Should().Be(1); + + rules1.Insert(0, new ReportingConfiguration() { Level = FailureLevel.Error }); + rules2.Insert(0, new ReportingConfiguration() { Level = FailureLevel.Warning }); + + rules1.ListCompares(rules2, ReportingConfigurationComparer.Instance).Should().Be(1); + rules2.ListCompares(rules1, ReportingConfigurationComparer.Instance).Should().Be(-1); + + rules1.Insert(0, new ReportingConfiguration() { Enabled = false, Rank = 80d }); + rules2.Insert(0, new ReportingConfiguration() { Enabled = true, Rank = 80d }); + + rules1.ListCompares(rules2, ReportingConfigurationComparer.Instance).Should().Be(-1); + rules2.ListCompares(rules1, ReportingConfigurationComparer.Instance).Should().Be(1); + } + + [Fact] + public void ToolComponentComparer_Tests() + { + var list1 = new List(); + var list2 = new List(); + + list1.Add(null); + list2.Add(null); + + list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(0); + + list1.Insert(0, new ToolComponent() { Guid = Guid.Empty.ToString() }); + list2.Insert(0, new ToolComponent() { Guid = Guid.NewGuid().ToString() }); + + list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(-1); + list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(1); + + list1.Insert(0, new ToolComponent() { Name = "scan tool" }); + list2.Insert(0, new ToolComponent() { Name = "code scan tool" }); + + list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(1); + list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(-1); + + list1.Insert(0, new ToolComponent() { Organization = "MS", Name = "scan tool" }); + list2.Insert(0, new ToolComponent() { Organization = "Microsoft", Name = "scan tool" }); + + list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(1); + list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(-1); + + list1.Insert(0, new ToolComponent() { Product = "PREfast", Name = "scan tool" }); + list2.Insert(0, new ToolComponent() { Product = "prefast", Name = "scan tool" }); + + list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(1); + list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(-1); + + list1.Insert(0, new ToolComponent() { FullName = "Analysis Linter", Name = "scan tool" }); + list2.Insert(0, new ToolComponent() { FullName = "Analysis Linter Tool", Name = "scan tool" }); + + list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(-1); + list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(1); + + list1.Insert(0, new ToolComponent() { Version = "CWR-2022-01", Name = "scan tool" }); + list2.Insert(0, new ToolComponent() { Version = "CWR-2021-12", Name = "scan tool" }); + + list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(1); + list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(-1); + + list1.Insert(0, new ToolComponent() { SemanticVersion = "1.0.1", Name = "scan tool" }); + list2.Insert(0, new ToolComponent() { SemanticVersion = "1.0.3", Name = "scan tool" }); + + list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(-1); + list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(1); + + list1.Insert(0, new ToolComponent() { ReleaseDateUtc = "2/8/2022", Name = "scan tool" }); + list2.Insert(0, new ToolComponent() { ReleaseDateUtc = "1/1/2022", Name = "scan tool" }); + + list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(1); + list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(-1); + + list1.Insert(0, new ToolComponent() { DownloadUri = new Uri("https://example/download/v1"), Name = "scan tool" }); + list2.Insert(0, new ToolComponent() { DownloadUri = new Uri("https://example/download/v2"), Name = "scan tool" }); + + list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(-1); + list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(1); + + list1.Insert(0, new ToolComponent() { Rules = new ReportingDescriptor[] { new ReportingDescriptor { Id = "TESTRULE001" } }, Name = "scan tool" }); + list2.Insert(0, new ToolComponent() { Rules = new ReportingDescriptor[] { new ReportingDescriptor { Id = "TESTRULE002" } }, Name = "scan tool" }); + + list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(-1); + list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(1); + } + + [Fact] + public void ReportingDescriptorComparer_Tests() + { + var rules1 = new List(); + var rules2 = new List(); + + rules1.Add(null); + rules2.Add(null); + + rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(0); + + rules1.Insert(0, new ReportingDescriptor() { Id = "TestRule1" }); + rules2.Insert(0, new ReportingDescriptor() { Id = "TestRule2" }); + + rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(-1); + rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(1); + + rules1.Insert(0, new ReportingDescriptor() { DeprecatedIds = new string[] { "OldRuleId3" }, Id = "TestRule1" }); + rules2.Insert(0, new ReportingDescriptor() { DeprecatedIds = new string[] { "OldRuleId2" }, Id = "TestRule1" }); + + rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(1); + rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(-1); + + rules1.Insert(0, new ReportingDescriptor() { Guid = Guid.NewGuid().ToString(), Id = "TestRule1" }); + rules2.Insert(0, new ReportingDescriptor() { Guid = Guid.Empty.ToString(), Id = "TestRule1" }); + + rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(1); + rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(-1); + + rules1.Insert(0, new ReportingDescriptor() { DeprecatedIds = new string[] { Guid.Empty.ToString() }, Id = "TestRule1" }); + rules2.Insert(0, new ReportingDescriptor() { DeprecatedIds = new string[] { Guid.NewGuid().ToString() }, Id = "TestRule1" }); + + rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(-1); + rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(1); + + rules1.Insert(0, new ReportingDescriptor() { Name = "UnusedVariable", Id = "TestRule1" }); + rules2.Insert(0, new ReportingDescriptor() { Name = "", Id = "TestRule1" }); + + rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(1); + rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(-1); + + rules1.Insert(0, new ReportingDescriptor() { ShortDescription = new MultiformatMessageString { Text = "Remove unused variable" }, Id = "TestRule1" }); + rules2.Insert(0, new ReportingDescriptor() { ShortDescription = new MultiformatMessageString { Text = "Wrong description" }, Id = "TestRule1" }); + + rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(-1); + rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(1); + + rules1.Insert(0, new ReportingDescriptor() { FullDescription = new MultiformatMessageString { Text = "Remove unused variable" }, Id = "TestRule1" }); + rules2.Insert(0, new ReportingDescriptor() { FullDescription = new MultiformatMessageString { Text = "Wrong description" }, Id = "TestRule1" }); + + rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(-1); + rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(1); + + rules1.Insert(0, new ReportingDescriptor() { DefaultConfiguration = new ReportingConfiguration { Level = FailureLevel.Note }, Id = "TestRule1" }); + rules2.Insert(0, new ReportingDescriptor() { DefaultConfiguration = new ReportingConfiguration { Level = FailureLevel.Error }, Id = "TestRule1" }); + + rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(-1); + rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(1); + + rules1.Insert(0, new ReportingDescriptor() { HelpUri = new Uri("http://example.net/rule/id"), Id = "TestRule1" }); + rules2.Insert(0, new ReportingDescriptor() { HelpUri = new Uri("http://example.net"), Id = "TestRule1" }); + + rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(1); + rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(-1); + + rules1.Insert(0, new ReportingDescriptor() { Help = new MultiformatMessageString { Text = "Helping texts." }, Id = "TestRule1" }); + rules2.Insert(0, new ReportingDescriptor() { Help = new MultiformatMessageString { Text = "For customers." }, Id = "TestRule1" }); + + rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(1); + rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(-1); + } + + [Fact] + public void RegionComparer_Tests() + { + var regions1 = new List(); + var regions2 = new List(); + + regions1.Add(null); + regions2.Add(null); + + regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(0); + + regions1.Insert(0, new Region() { StartLine = 0, StartColumn = 0 }); + regions2.Insert(0, new Region() { StartLine = 1, StartColumn = 0 }); + + regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(-1); + regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(1); + + regions1.Insert(0, new Region() { StartLine = 0, StartColumn = 1 }); + regions2.Insert(0, new Region() { StartLine = 0, StartColumn = 0 }); + + regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(1); + regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(-1); + + regions1.Insert(0, new Region() { StartLine = 10, EndLine = 11, StartColumn = 0 }); + regions2.Insert(0, new Region() { StartLine = 10, EndLine = 10, StartColumn = 0 }); + + regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(1); + regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(-1); + + regions1.Insert(0, new Region() { StartLine = 10, EndLine = 10, StartColumn = 5, EndColumn = 23 }); + regions2.Insert(0, new Region() { StartLine = 10, EndLine = 10, StartColumn = 5, EndColumn = 7 }); + + regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(1); + regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(-1); + + regions1.Insert(0, new Region() { CharOffset = 100, CharLength = 30 }); + regions2.Insert(0, new Region() { CharOffset = 36, CharLength = 30 }); + + regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(1); + regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(-1); + + regions1.Insert(0, new Region() { CharOffset = 100, CharLength = 47 }); + regions2.Insert(0, new Region() { CharOffset = 100, CharLength = 326 }); + + regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(-1); + regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(1); + + regions1.Insert(0, new Region() { ByteOffset = 226, ByteLength = 11 }); + regions2.Insert(0, new Region() { ByteOffset = 1623, ByteLength = 11 }); + + regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(-1); + regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(1); + + regions1.Insert(0, new Region() { ByteOffset = 67, ByteLength = 9 }); + regions2.Insert(0, new Region() { ByteOffset = 67, ByteLength = 11 }); + + regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(-1); + regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(1); + } + + [Fact] + public void ArtifactComparer_Tests() + { + var artifacts1 = new List(); + var artifacts2 = new List(); + + artifacts1.Add(null); + artifacts2.Add(null); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(0); + + artifacts1.Insert(0, new Artifact() { Description = new Message { Text = "Represents for an artifact" } }); + artifacts2.Insert(0, new Artifact() { Description = new Message { Text = "A source file artifact" } }); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(1); + artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(-1); + + artifacts1.Insert(0, new Artifact() { Location = new ArtifactLocation { Index = 0 } }); + artifacts2.Insert(0, new Artifact() { Location = new ArtifactLocation { Index = 1 } }); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1); + artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1); + + artifacts1.Insert(0, new Artifact() { ParentIndex = 0 }); + artifacts2.Insert(0, new Artifact() { ParentIndex = 1 }); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1); + artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1); + + artifacts1.Insert(0, new Artifact() { Offset = 2 }); + artifacts2.Insert(0, new Artifact() { Offset = 1 }); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(1); + artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(-1); + + artifacts1.Insert(0, new Artifact() { Length = 102542 }); + artifacts2.Insert(0, new Artifact() { Length = -1 }); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(1); + artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(-1); + + artifacts1.Insert(0, new Artifact() { Roles = ArtifactRoles.AnalysisTarget | ArtifactRoles.Attachment }); + artifacts2.Insert(0, new Artifact() { Roles = ArtifactRoles.Policy }); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1); + artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1); + + artifacts1.Insert(0, new Artifact() { MimeType = "text" }); + artifacts2.Insert(0, new Artifact() { MimeType = "video" }); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1); + artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1); + + artifacts1.Insert(0, new Artifact() { Contents = new ArtifactContent { Text = "\"string\"" } }); + artifacts2.Insert(0, new Artifact() { Contents = new ArtifactContent { Text = "var result = 0;" } }); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1); + artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1); + + artifacts1.Insert(0, new Artifact() { Encoding = "UTF-16BE" }); + artifacts2.Insert(0, new Artifact() { Encoding = "UTF-16LE" }); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1); + artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1); + + artifacts1.Insert(0, new Artifact() { SourceLanguage = "html" }); + artifacts2.Insert(0, new Artifact() { SourceLanguage = "csharp/7" }); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(1); + artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(-1); + + artifacts1.Insert(0, new Artifact() { Hashes = new Dictionary { { "sha-256", "..." } } }); + artifacts2.Insert(0, new Artifact() { Hashes = new Dictionary { { "sha-512", "..." } } }); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1); + artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1); + + artifacts1.Insert(0, new Artifact() { LastModifiedTimeUtc = DateTime.UtcNow }); + artifacts2.Insert(0, new Artifact() { LastModifiedTimeUtc = DateTime.UtcNow.AddDays(-1) }); + + artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(1); + artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(-1); + } + + [Fact] + public void ThreadFlowComparer_Tests() + { + var threadFlow1 = new List(); + var threadFlow2 = new List(); + + threadFlow1.Add(null); + threadFlow2.Add(null); + + threadFlow1.ListCompares(threadFlow2, ThreadFlowComparer.Instance).Should().Be(0); + + threadFlow1.Insert(0, new ThreadFlow() { Id = "threadFlow1" }); + threadFlow2.Insert(0, new ThreadFlow() { Id = "threadFlow2" }); + + threadFlow1.ListCompares(threadFlow2, ThreadFlowComparer.Instance).Should().Be(-1); + threadFlow2.ListCompares(threadFlow1, ThreadFlowComparer.Instance).Should().Be(1); + + threadFlow1.Insert(0, new ThreadFlow() { Message = new Message { Id = "arg1" } }); + threadFlow2.Insert(0, new ThreadFlow() { Message = new Message { Id = "fileArg" } }); + + threadFlow1.ListCompares(threadFlow2, ThreadFlowComparer.Instance).Should().Be(-1); + threadFlow2.ListCompares(threadFlow1, ThreadFlowComparer.Instance).Should().Be(1); + + var loc1 = new Location + { + PhysicalLocation = new PhysicalLocation + { + ArtifactLocation = new ArtifactLocation + { + Uri = new Uri("path/to/file1.c", UriKind.Relative) + } + } + }; + + var loc2 = new Location + { + PhysicalLocation = new PhysicalLocation + { + ArtifactLocation = new ArtifactLocation + { + Uri = new Uri("path/to/file2.c", UriKind.Relative) + } + } + }; + + threadFlow1.Insert(0, new ThreadFlow() { Locations = new ThreadFlowLocation[] { new ThreadFlowLocation { Location = loc1 } } }); + threadFlow2.Insert(0, new ThreadFlow() { Locations = new ThreadFlowLocation[] { new ThreadFlowLocation { Location = loc2 } } }); + + threadFlow1.ListCompares(threadFlow2, ThreadFlowComparer.Instance).Should().Be(-1); + threadFlow2.ListCompares(threadFlow1, ThreadFlowComparer.Instance).Should().Be(1); + } + + [Fact] + public void ThreadFlowLocationComparer_Tests() + { + var locations1 = new List(); + var locations2 = new List(); + + locations1.Add(null); + locations2.Add(null); + + locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(0); + + var loc1 = new Location + { + PhysicalLocation = new PhysicalLocation + { + ArtifactLocation = new ArtifactLocation + { + Uri = new Uri("path/to/file1.c", UriKind.Relative) + } + } + }; + + var loc2 = new Location + { + PhysicalLocation = new PhysicalLocation + { + ArtifactLocation = new ArtifactLocation + { + Uri = new Uri("path/to/file2.c", UriKind.Relative) + } + } + }; + + locations1.Insert(0, new ThreadFlowLocation() { Location = loc1 }); + locations2.Insert(0, new ThreadFlowLocation() { Location = loc2 }); + + locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(-1); + locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(1); + + locations1.Insert(0, new ThreadFlowLocation() { Index = 2, Location = loc1 }); + locations2.Insert(0, new ThreadFlowLocation() { Index = 1, Location = loc2 }); + + locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(1); + locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(-1); + + locations1.Insert(0, new ThreadFlowLocation() { Kinds = new string[] { "memory" }, Location = loc1 }); + locations2.Insert(0, new ThreadFlowLocation() { Kinds = new string[] { "call", "branch" }, Location = loc2 }); + + locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(-1); + locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(1); + + locations1.Insert(0, new ThreadFlowLocation() { NestingLevel = 3 }); + locations2.Insert(0, new ThreadFlowLocation() { NestingLevel = 2 }); + + locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(1); + locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(-1); + + locations1.Insert(0, new ThreadFlowLocation() { ExecutionOrder = 2 }); + locations2.Insert(0, new ThreadFlowLocation() { ExecutionOrder = 1 }); + + locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(1); + locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(-1); + + locations1.Insert(0, new ThreadFlowLocation() { ExecutionTimeUtc = DateTime.UtcNow }); + locations2.Insert(0, new ThreadFlowLocation() { ExecutionTimeUtc = DateTime.UtcNow.AddHours(-2) }); + + locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(1); + locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(-1); + + locations1.Insert(0, new ThreadFlowLocation() { Importance = ThreadFlowLocationImportance.Essential }); + locations2.Insert(0, new ThreadFlowLocation() { Importance = ThreadFlowLocationImportance.Unimportant }); + + locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(-1); + locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(1); + } + + [Fact] + public void RunComparer_Tests() + { + var runs1 = new List(); + var runs2 = new List(); + + runs1.Add(null); + runs2.Add(null); + + runs1.ListCompares(runs2, RunComparer.Instance).Should().Be(0); + + runs1.Insert(0, new Run() { Artifacts = new Artifact[] { new Artifact { Description = new Message { Text = "artifact 1" } } } }); + runs2.Insert(0, new Run() { Artifacts = new Artifact[] { new Artifact { Description = new Message { Text = "artifact 2" } } } }); + + runs1.ListCompares(runs2, RunComparer.Instance).Should().Be(-1); + runs2.ListCompares(runs1, RunComparer.Instance).Should().Be(1); + + var tool1 = new Tool { Driver = new ToolComponent { Name = "PREFast", Version = "1.0" } }; + var tool2 = new Tool { Driver = new ToolComponent { Name = "PREFast", Version = "1.3" } }; + + runs1.Insert(0, new Run() { Tool = tool1 }); + runs2.Insert(0, new Run() { Tool = tool2 }); + + runs1.ListCompares(runs2, RunComparer.Instance).Should().Be(-1); + runs2.ListCompares(runs1, RunComparer.Instance).Should().Be(1); + + var result1 = new Result { RuleId = "CS001", Message = new Message { Text = "Issue of C# code" } }; + var result2 = new Result { RuleId = "IDE692", Message = new Message { Text = "Issue by IDE" } }; + + runs1.Insert(0, new Run() { Results = new Result[] { result1 } }); + runs2.Insert(0, new Run() { Results = new Result[] { result2 } }); + + runs1.ListCompares(runs2, RunComparer.Instance).Should().Be(-1); + runs2.ListCompares(runs1, RunComparer.Instance).Should().Be(1); + } + + [Fact] + public void ComparerHelp_CompareUir_Tests() + { + var testUris = new List<(Uri, int)>() + { + (null, -1), + (null, 0), + (new Uri(@"", UriKind.RelativeOrAbsolute), 1), + (new Uri(string.Empty, UriKind.RelativeOrAbsolute), 0), + (new Uri(@"file.ext", UriKind.RelativeOrAbsolute), 1), + (new Uri(@"C:\path\file.ext", UriKind.RelativeOrAbsolute), -1), + (new Uri(@"\\hostname\path\file.ext", UriKind.RelativeOrAbsolute), -1), + (new Uri(@"file:///C:/path/file.ext", UriKind.RelativeOrAbsolute), 1), + (new Uri(@"\\hostname\c:\path\file.ext", UriKind.RelativeOrAbsolute), -1), + (new Uri(@"/home/username/path/file.ext", UriKind.RelativeOrAbsolute), -1), + (new Uri(@"nfs://servername/folder/file.ext", UriKind.RelativeOrAbsolute), 1), + (new Uri(@"file://hostname/C:/path/file.ext", UriKind.RelativeOrAbsolute), -1), + (new Uri(@"file:///home/username/path/file.ext", UriKind.RelativeOrAbsolute), -1), + (new Uri(@"ftp://ftp.example.com/folder/file.ext", UriKind.RelativeOrAbsolute), 1), + (new Uri(@"smb://servername/Share/folder/file.ext", UriKind.RelativeOrAbsolute), 1), + (new Uri(@"dav://example.hostname.com/folder/file.ext", UriKind.RelativeOrAbsolute), -1), + (new Uri(@"file://hostname/home/username/path/file.ext", UriKind.RelativeOrAbsolute), 1), + (new Uri(@"ftp://username@ftp.example.com/folder/file.ext", UriKind.RelativeOrAbsolute), 1), + (new Uri(@"scheme://servername.example.com/folder/file.ext", UriKind.RelativeOrAbsolute), 1), + (new Uri(@"https://github.com/microsoft/sarif-sdk/file.ext", UriKind.RelativeOrAbsolute), -1), + (new Uri(@"ssh://username@servername.example.com/folder/file.ext", UriKind.RelativeOrAbsolute), 1), + (new Uri(@"scheme://username@servername.example.com/folder/file.ext", UriKind.RelativeOrAbsolute), -1), + (new Uri(@"https://github.com/microsoft/sarif-sdk/file.ext?some-query-string", UriKind.RelativeOrAbsolute), -1), + }; + + for (int i = 1; i < testUris.Count; i++) + { + int result = testUris[i].Item1.UriCompares(testUris[i - 1].Item1); + result.Should().Be(testUris[i].Item2); + } + } + } +} diff --git a/src/Test.UnitTests.Sarif/RandomSarifLogGenerator.cs b/src/Test.UnitTests.Sarif/RandomSarifLogGenerator.cs index 0d77102ae..8ec52b4b9 100644 --- a/src/Test.UnitTests.Sarif/RandomSarifLogGenerator.cs +++ b/src/Test.UnitTests.Sarif/RandomSarifLogGenerator.cs @@ -17,10 +17,10 @@ internal static class RandomSarifLogGenerator { public static string GeneratorBaseUri = @"C:\src\"; - public static Random GenerateRandomAndLog(ITestOutputHelper output, [CallerMemberName] string testName = "") + public static Random GenerateRandomAndLog(ITestOutputHelper output, [CallerMemberName] string testName = "", int? seed = null) { // Slightly roundabout. We want to randomly test this, but we also want to be able to repeat this if the test fails. - int randomSeed = (new Random()).Next(); + int randomSeed = seed ?? (new Random()).Next(); Random random = new Random(randomSeed); @@ -29,7 +29,7 @@ public static Random GenerateRandomAndLog(ITestOutputHelper output, [CallerMembe return random; } - public static SarifLog GenerateSarifLogWithRuns(Random randomGen, int runCount, int? resultCount = null) + public static SarifLog GenerateSarifLogWithRuns(Random randomGen, int runCount, int? resultCount = null, RandomDataFields dataFields = RandomDataFields.None) { SarifLog log = new SarifLog(); @@ -40,13 +40,13 @@ public static SarifLog GenerateSarifLogWithRuns(Random randomGen, int runCount, for (int i = 0; i < runCount; i++) { - log.Runs.Add(GenerateRandomRun(randomGen, resultCount)); + log.Runs.Add(GenerateRandomRun(randomGen, resultCount, dataFields)); } return log; } - public static Run GenerateRandomRun(Random random, int? resultCount = null) + public static Run GenerateRandomRun(Random random, int? resultCount = null, RandomDataFields dataFields = RandomDataFields.None) { List ruleIds = new List() { "TEST001", "TEST002", "TEST003", "TEST004", "TEST005" }; List filePaths = GenerateFakeFiles(GeneratorBaseUri, random.Next(20) + 1).Select(a => new Uri(a)).ToList(); @@ -64,7 +64,7 @@ public static Run GenerateRandomRun(Random random, int? resultCount = null) } }, Artifacts = GenerateFiles(filePaths), - Results = GenerateFakeResults(random, ruleIds, filePaths, results) + Results = GenerateFakeResults(random, ruleIds, filePaths, results, dataFields) }; } @@ -96,7 +96,7 @@ public static IEnumerable GenerateFakeFiles(string baseAddress, int coun return results; } - public static IList GenerateFakeResults(Random random, List ruleIds, List filePaths, int resultCount) + public static IList GenerateFakeResults(Random random, List ruleIds, List filePaths, int resultCount, RandomDataFields dataFields = RandomDataFields.None) { List results = new List(); for (int i = 0; i < resultCount; i++) @@ -118,9 +118,15 @@ public static IList GenerateFakeResults(Random random, List rule Uri = filePaths[fileIndex], Index = fileIndex }, - } + }, + LogicalLocations = dataFields.HasFlag(RandomDataFields.LogicalLocation) ? + GenerateLogicalLocations(random) : + null, } - } + }, + CodeFlows = dataFields.HasFlag(RandomDataFields.CodeFlow) ? + GenerateCodeFlows(random, filePaths, dataFields) : + null, }); } return results; @@ -161,5 +167,92 @@ public static IList GenerateRules(List ruleIds) } return rules; } + + public static IList GenerateCodeFlows(Random random, IList artifacts, RandomDataFields dataFields) + { + if (artifacts?.Any() != true) + { + return null; + } + + var codeFlow = new CodeFlow + { + Message = new Message { Text = "code flow message" }, + ThreadFlows = new[] + { + new ThreadFlow + { + Message = new Message { Text = "thread flow message" }, + Locations = dataFields.HasFlag(RandomDataFields.ThreadFlow) ? + GenerateThreadFlowLocations(random, artifacts) : + null, + }, + }, + }; + + return new[] { codeFlow }; + } + + public static IList GenerateThreadFlowLocations(Random random, IList artifacts) + { + var locations = new List(); + + for (int i = 0; i < random.Next(10) + 1; i++) + { + locations.Add(new ThreadFlowLocation + { + Importance = RandomEnumValue(random), + Location = new Location + { + PhysicalLocation = new PhysicalLocation + { + ArtifactLocation = new ArtifactLocation + { + Index = random.Next(artifacts.Count), + }, + Region = new Region + { + StartLine = random.Next(500), + StartColumn = random.Next(100), + }, + }, + }, + }); + } + + return locations; + } + + public static IList GenerateLogicalLocations(Random random) + { + var logicalLocations = new List(); + for (int i = 0; i < random.Next(5); i++) + { + logicalLocations.Add(new LogicalLocation + { + Name = $"Class{i}", + Index = i, + FullyQualifiedName = "namespaceA::namespaceB::namespaceC", + Kind = LogicalLocationKind.Type, + }); + } + + return logicalLocations; + } + + public static T RandomEnumValue(Random random) where T : Enum + { + Array enums = Enum.GetValues(typeof(T)); + return (T)enums.GetValue(random.Next(enums.Length)); + } + } + + [Flags] + public enum RandomDataFields + { + None = 0, + CodeFlow = 0b1, + ThreadFlow = 0b10, + LogicalLocation = 0b100, } } diff --git a/src/Test.UnitTests.Sarif/Visitors/SortingVisitorTests.cs b/src/Test.UnitTests.Sarif/Visitors/SortingVisitorTests.cs new file mode 100644 index 000000000..b35f4dd94 --- /dev/null +++ b/src/Test.UnitTests.Sarif/Visitors/SortingVisitorTests.cs @@ -0,0 +1,272 @@ +// 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.Linq; +using System.Text; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.Sarif; +using Microsoft.CodeAnalysis.Sarif.Visitors; +using Microsoft.CodeAnalysis.Test.Utilities.Sarif; + +using Newtonsoft.Json; + +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.Test.UnitTests.Sarif.Visitors +{ + public class SortingVisitorTests : FileDiffingUnitTests + { + private readonly Random random; + private readonly ITestOutputHelper _outputHelper; + + protected override string OutputFolderPath => Path.Combine(Path.GetDirectoryName(ThisAssembly.Location), "UnitTestOutput." + TypeUnderTest); + + public SortingVisitorTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + this._outputHelper = outputHelper; + this.random = RandomSarifLogGenerator.GenerateRandomAndLog(outputHelper); + } + + [Fact] + public void SortingVisitor_ShuffleTest() + { + bool areEqual; + + SarifLog originalLog = CreateTestSarifLog(this.random); + SarifLog shuffledLog1 = ShuffleSarifLog(originalLog, this.random); + SarifLog shuffledLog2 = ShuffleSarifLog(originalLog, this.random); + + areEqual = SarifLogEqualityComparer.Instance.Equals(originalLog, shuffledLog1); + areEqual.Should().BeFalse(); + + areEqual = SarifLogEqualityComparer.Instance.Equals(originalLog, shuffledLog2); + areEqual.Should().BeFalse(); + + areEqual = SarifLogEqualityComparer.Instance.Equals(shuffledLog1, shuffledLog2); + areEqual.Should().BeFalse(); + + SarifLog sortedLog1 = new SortingVisitor().VisitSarifLog(shuffledLog1); + SarifLog sortedLog2 = new SortingVisitor().VisitSarifLog(shuffledLog2); + + areEqual = this.VerifySarifLogAreSame(sortedLog1, sortedLog2); + areEqual.Should().BeTrue(); + + areEqual = SarifLogEqualityComparer.Instance.Equals(sortedLog1, sortedLog2); + areEqual.Should().BeTrue(); + + foreach (Run run in sortedLog1.Runs) + { + IList rules = run.Tool.Driver.Rules; + IList artifacts = run.Artifacts; + + foreach (Result result in run.Results) + { + if (result.RuleIndex != -1) + { + int ruleIndex = rules.IndexOf(rules.First(r => r.Id.Equals(result.RuleId))); + result.RuleIndex.Should().Be(ruleIndex); + } + + ArtifactLocation artifactLoc = result?.Locations?.First()?.PhysicalLocation?.ArtifactLocation; + + if (artifactLoc != null && artifactLoc.Index != -1) + { + artifactLoc.Uri.Should().Be(artifacts[artifactLoc.Index].Location.Uri); + } + } + } + } + + [Fact] + public void SortingVisitor_NullEmptyListTests() + { + // Create a log with all results have same values. + var sarifLog = new SarifLog + { + Runs = new[] + { + new Run + { + Results = new[] + { + new Result + { + RuleId = "TESTRULE001" + }, + new Result + { + RuleId = "TESTRULE001" + }, + new Result + { + RuleId = "TESTRULE001" + }, + }, + }, + }, + }; + + IList results = sarifLog.Runs[0].Results; + results[0].Locations = new[] { new Location { Message = new Message { Text = "test location" } } }; + results[1].Locations = null; + results[2].Locations = new List(); + + SarifLog sortedLog = new SortingVisitor().VisitSarifLog(sarifLog); + + results = sortedLog.Runs[0].Results; + results[0].Locations.Should().BeNull(); + results[1].Locations.Should().BeEmpty(); + results[2].Locations.Should().NotBeEmpty(); + } + + private static SarifLog CreateTestSarifLog(Random random) + { + SarifLog sarifLog = RandomSarifLogGenerator.GenerateSarifLogWithRuns( + randomGen: random, + runCount: random.Next(1, 5), + dataFields: RandomDataFields.CodeFlow | RandomDataFields.ThreadFlow | RandomDataFields.LogicalLocation); + + return sarifLog; + } + + private static SarifLog ShuffleSarifLog(SarifLog originalLog, Random random) + { + SarifLog logToBeShuffled = originalLog.DeepClone(); + + while (SarifLogEqualityComparer.Instance.Equals(logToBeShuffled, originalLog)) + { + foreach (Run run in logToBeShuffled?.Runs) + { + IList rules = run?.Tool?.Driver?.Rules; + IList results = run?.Results; + IList artifacts = run?.Artifacts; + + if (rules != null) + { + var ruleIndexMapping = new Dictionary(); + rules = rules.Shuffle(random); + run.Tool.Driver.Rules = rules; + + for (int i = 0; i < rules.Count; i++) + { + ruleIndexMapping[rules[i].Id] = i; + } + + foreach (Result result in results.Where(r => r.RuleIndex != -1)) + { + if (ruleIndexMapping.TryGetValue(result.RuleId, out int newIndex)) + { + result.RuleIndex = newIndex; + } + } + } + + if (artifacts != null) + { + var artifactIndexMapping = new Dictionary(); + var oldMapping = new Dictionary(); + + for (int i = 0; i < artifacts.Count; i++) + { + oldMapping[artifacts[i]] = i; + } + + artifacts = artifacts.Shuffle(random); + run.Artifacts = artifacts; + + for (int i = 0; i < artifacts.Count; i++) + { + if (oldMapping.TryGetValue(artifacts[i], out int oldIndex)) + { + artifactIndexMapping[oldIndex] = i; + } + } + + var locToUpdate = new List(); + + locToUpdate.AddRange( + results + .SelectMany(r => r.Locations) + .Select(l => l.PhysicalLocation.ArtifactLocation)); + + locToUpdate.AddRange( + results + .SelectMany(r => r.CodeFlows) + .SelectMany(c => c.ThreadFlows) + .SelectMany(t => t.Locations) + .Select(l => l.Location.PhysicalLocation.ArtifactLocation)); + + foreach (ArtifactLocation artifactLocation in locToUpdate.Where(l => l.Index != -1)) + { + if (artifactIndexMapping.TryGetValue(artifactLocation.Index, out int newIndex)) + { + artifactLocation.Index = newIndex; + } + } + } + + run.Results = results.Shuffle(random); + + foreach (Result result in run.Results) + { + result.CodeFlows = result.CodeFlows.Shuffle(random); + + foreach (CodeFlow codeFlow in result.CodeFlows) + { + codeFlow.ThreadFlows = codeFlow.ThreadFlows.Shuffle(random); + + foreach (ThreadFlow threadFlow in codeFlow.ThreadFlows) + { + threadFlow.Locations = threadFlow.Locations.Shuffle(random); + } + } + } + } + logToBeShuffled.Runs = logToBeShuffled.Runs.Shuffle(random); + } + + return logToBeShuffled; + } + + private bool VerifySarifLogAreSame(SarifLog first, SarifLog second) + { + string firstLogText = ReadSarifLogAsString(first); + string secondLogText = ReadSarifLogAsString(second); + + bool areEqual = AreEquivalent(firstLogText, secondLogText, out SarifLog _); + + if (!areEqual) + { + string firstLogFile = Path.Combine(OutputFolderPath, $"{Guid.NewGuid()}.sarif"); + string secondLogFile = Path.Combine(OutputFolderPath, $"{Guid.NewGuid()}.sarif"); + + File.WriteAllText(firstLogFile, firstLogText); + File.WriteAllText(secondLogFile, secondLogText); + + var sb = new StringBuilder(); + sb.AppendLine("The sorted Sarif logs did not match."); + sb.AppendLine("To compare all difference for this test suite:"); + sb.AppendLine(FileDiffingUnitTests.GenerateDiffCommand(TypeUnderTest, firstLogFile, secondLogFile)); + this._outputHelper.WriteLine(sb.ToString()); + } + + return areEqual; + } + + private static string ReadSarifLogAsString(SarifLog sarifLog) + { + var settings = new JsonSerializerSettings() + { + Formatting = Formatting.Indented + }; + + return JsonConvert.SerializeObject(sarifLog, settings); + } + } +} diff --git a/src/Test.Utilities.Sarif/TestUtilitiesExtensions.cs b/src/Test.Utilities.Sarif/TestUtilitiesExtensions.cs index 87dd9d33c..849a2d9f4 100644 --- a/src/Test.Utilities.Sarif/TestUtilitiesExtensions.cs +++ b/src/Test.Utilities.Sarif/TestUtilitiesExtensions.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis.Sarif; @@ -34,5 +36,21 @@ public static TestRuleBehaviors AccessibleWithinContextOnly(this TestRuleBehavio // specifies this from the context object that parameterizes the call. return behaviors & ~behaviors.AccessibleOutsideOfContextOnly(); } + + public static IList Shuffle(this IList list, Random random) + { + if (list == null) + { + return null; + } + + if (random == null) + { + // Random object with seed logged in test is required. + throw new ArgumentNullException(nameof(random)); + } + + return list.OrderBy(item => random.Next()).ToList(); + } } }