diff --git a/src/Sarif/FileRegionsCache.cs b/src/Sarif/FileRegionsCache.cs index 2e206014d..243197220 100644 --- a/src/Sarif/FileRegionsCache.cs +++ b/src/Sarif/FileRegionsCache.cs @@ -159,6 +159,8 @@ public Region ConstructMultilineContextSnippet(Region inputRegion, Uri uri, stri return null; } + fileText ??= newLineIndex.Text; + // Generating full inputRegion to prevent issues. Region originalRegion = this.PopulateTextRegionProperties(inputRegion, uri, populateSnippet: true, fileText); diff --git a/src/Test.UnitTests.Sarif/FileRegionsCacheTests.cs b/src/Test.UnitTests.Sarif/FileRegionsCacheTests.cs index 1c5492d6e..092e7d6e8 100644 --- a/src/Test.UnitTests.Sarif/FileRegionsCacheTests.cs +++ b/src/Test.UnitTests.Sarif/FileRegionsCacheTests.cs @@ -459,51 +459,73 @@ public void FileRegionsCache_PopulatesContextRegions() int bsl = FileRegionsCache.BIGSNIPPETLENGTH; int ssl = FileRegionsCache.SMALLSNIPPETLENGTH; - string[] tests = - { - $"{new string(padding, 0)}{sentinel}{new string(padding, 0)}", - $"{new string(padding, 0)}{sentinel}{new string(padding, 3)}", - $"{new string(padding, 3)}{sentinel}{new string(padding, 0)}", - $"{new string(padding, 3)}{sentinel}{new string(padding, 3)}", - $"{new string(padding, ssl)}{sentinel}{new string(padding, ssl)}", - $"{new string(padding, 1)}{sentinel}{new string(padding, ssl)}", - $"{new string(padding, ssl)}{sentinel}{new string(padding, 1)}", - $"{new string(padding, ssl)}{sentinel}{new string(padding, 0)}", - $"{new string(padding, 0)}{sentinel}{new string(padding, ssl)}", - $"{new string(padding, 0)}{sentinel}{new string(padding, 0)}", - $"{new string(padding, bsl)}{sentinel}{new string(padding, bsl)}", - $"{new string(padding, 10)}{sentinel}{new string(padding, bsl)}", - $"{new string(padding, bsl)}{sentinel}{new string(padding, 10)}", - $"{new string(padding, bsl)}{sentinel}{new string(padding, 0)}", - $"{new string(padding, 0)}{sentinel}{new string(padding, bsl)}", - $"{new string(padding, 0)}{sentinel}{new string(padding, 0)}", - }; - - var context = new StringBuilder(); - int iteration = 0; - - // DEBUGGING THESE TESTS: these tests do not accumulate all outputs and report - // them, instead they break on the first failure. A failure will report a - // message like so: - // - // Expected contextRegion.Snippet not to be because 'baz' snippet exists - // (iteration 12, while processing char-based region type for value 'abaza'). - // - // Observe the iteration value (in this case 12). Set a conditional breakpoint - // below when the iteration variable equals this value and you can debug the - // relevant failure. - - foreach (string test in tests) + // Prepend a newline (or not!) in front of every sentinel region. + foreach (string pr in new string[] { null, Environment.NewLine }) { - var cache = new FileRegionsCache(); - var uri = new Uri(@"c:\temp\DoesNotExist\" + Guid.NewGuid().ToString() + ".cpp"); + // Post-fix a newline (or not!) in front of every sentinel region. + foreach (string po in new string[] { null, Environment.NewLine }) + { + string[] tests = + { + $"{new string(padding, 0)}{pr}{sentinel}{po}{new string(padding, 0)}", + $"{new string(padding, 0)}{pr}{sentinel}{po}{new string(padding, 3)}", + $"{new string(padding, 3)}{pr}{sentinel}{po}{new string(padding, 0)}", + $"{new string(padding, 3)}{pr}{sentinel}{po}{new string(padding, 3)}", + $"{new string(padding, ssl)}{pr}{sentinel}{po}{new string(padding, ssl)}", + $"{new string(padding, 1)}{pr}{sentinel}{po}{new string(padding, ssl)}", + $"{new string(padding, ssl)}{pr}{sentinel}{po}{new string(padding, 1)}", + $"{new string(padding, ssl)}{pr}{sentinel}{po}{new string(padding, 0)}", + $"{new string(padding, 0)}{pr}{sentinel}{po}{new string(padding, ssl)}", + $"{new string(padding, 0)}{pr}{sentinel}{po}{new string(padding, 0)}", + $"{new string(padding, bsl)}{pr}{sentinel}{po}{new string(padding, bsl)}", + $"{new string(padding, 10)}{pr}{sentinel}{po}{new string(padding, bsl)}", + $"{new string(padding, bsl)}{pr}{sentinel}{po}{new string(padding, 10)}", + $"{new string(padding, bsl)}{pr}{sentinel}{po}{new string(padding, 0)}", + $"{new string(padding, 0)}{pr}{sentinel}{po}{new string(padding, bsl)}", + $"{new string(padding, 0)}{pr}{sentinel}{po}{new string(padding, 0)}", + $"{new string(padding, 0)}{pr}{sentinel}{po}{new string(padding, 0)}", + $"{new string(padding, 0)}{pr}{sentinel}{po}{new string(padding, 3)}", + $"{new string(padding, 3)}{pr}{sentinel}{po}{new string(padding, 0)}", + $"{new string(padding, 3)}{pr}{sentinel}{po}{new string(padding, 3)}", + $"{new string(padding, ssl)}{pr}{sentinel}{po}{new string(padding, ssl)}", + $"{new string(padding, 1)}{pr}{sentinel}{po}{new string(padding, ssl)}", + $"{new string(padding, ssl)}{pr}{sentinel}{po}{new string(padding, 1)}", + $"{new string(padding, ssl)}{pr}{sentinel}{po}{new string(padding, 0)}", + $"{new string(padding, 0)}{pr}{sentinel}{po}{new string(padding, ssl)}", + $"{new string(padding, 0)}{pr}{sentinel}{po}{new string(padding, 0)}", + $"{new string(padding, bsl)}{pr}{sentinel}{po}{new string(padding, bsl)}", + $"{new string(padding, 10)}{pr}{sentinel}{po}{new string(padding, bsl)}", + $"{new string(padding, bsl)}{pr}{sentinel}{po}{new string(padding, 10)}", + $"{new string(padding, bsl)}{pr}{sentinel}{po}{new string(padding, 0)}", + $"{new string(padding, 0)}{pr}{sentinel}{po}{new string(padding, bsl)}", + $"{new string(padding, 0)}{pr}{sentinel}{po}{new string(padding, 0)}", + }; + + var context = new StringBuilder(); + int iteration = 0; + + // DEBUGGING THESE TESTS: these tests do not accumulate all outputs and report + // them, instead they break on the first failure. A failure will report a + // message like so: + // + // Expected contextRegion.Snippet not to be because 'baz' snippet exists + // (iteration 12, while processing char-based region type for value 'abaza'). + // + // Observe the iteration value (in this case 12). Set a conditional breakpoint + // below when the iteration variable equals this value and you can debug the + // relevant failure. + + foreach (string test in tests) + { + var cache = new FileRegionsCache(); + var uri = new Uri(@"c:\temp\DoesNotExist\" + Guid.NewGuid().ToString() + ".cpp"); - int index = test.IndexOf(sentinel); + int index = test.IndexOf(sentinel); - // The FileRegions code takes two discrete code paths depending on whether - // the input variable is char-offset based or uses the start line convention. - var regions = new Region[] - { + // The FileRegions code takes two discrete code paths depending on whether + // the input variable is char-offset based or uses the start line convention. + var regions = new Region[] + { new Region { CharOffset = index, @@ -515,29 +537,31 @@ public void FileRegionsCache_PopulatesContextRegions() StartColumn = index + 1, EndColumn = index + sentinel.Length + 1, } - }; + }; - string charBased = "char-based"; - string lineBased = "line-based"; + string charBased = "char-based"; + string lineBased = "line-based"; - foreach (Region region in regions) - { - context.Clear(); - context.Append($"(iteration {iteration}, while processing {(region.StartLine == 1 ? lineBased : charBased)} region type for value '{test}')"); - - // First, we populate the region and text snippet for the actual test finding. - Region actual = cache.PopulateTextRegionProperties(region, uri, populateSnippet: true, test); - - actual.Snippet.Should().NotBeNull($"'{sentinel}' snippet exists {context}"); - actual.Snippet.Text?.Should().Be($"{sentinel}", $"region snippet did not match {context}"); - - // Now, we attempt to produce a context region. - Region contextRegion = cache.ConstructMultilineContextSnippet(actual, uri, test); - contextRegion.Snippet.Should().NotBeNull($"'{sentinel}' snippet exists {context}"); - contextRegion.Snippet.Text.Contains(sentinel).Should().BeTrue($"context region should encapsulate finding {context}"); - } + foreach (Region region in regions) + { + context.Clear(); + context.Append($"(iteration {iteration}, while processing {(region.StartLine == 1 ? lineBased : charBased)} region type for value '{test}')"); + + // First, we populate the region and text snippet for the actual test finding. + Region actual = cache.PopulateTextRegionProperties(region, uri, populateSnippet: true, test); + + actual.Snippet.Should().NotBeNull($"'{sentinel}' snippet exists {context}"); + actual.Snippet.Text?.Should().Be($"{sentinel}", $"region snippet did not match {context}"); - iteration++; + // Now, we attempt to produce a context region. + Region contextRegion = cache.ConstructMultilineContextSnippet(actual, uri, test); + contextRegion.Snippet.Should().NotBeNull($"'{sentinel}' snippet exists {context}"); + contextRegion.Snippet.Text.Contains(sentinel).Should().BeTrue($"context region should encapsulate finding {context}"); + } + + iteration++; + } + } } } @@ -690,14 +714,14 @@ public void FileRegionsCache_IncreasingToLeftAndRight() CharOffset = 114, CharLength = 600, }; - + var fileRegionsCache = new FileRegionsCache(); region = fileRegionsCache.PopulateTextRegionProperties(region, uri, true, fileContent); Region multilineRegion = fileRegionsCache.ConstructMultilineContextSnippet(region, uri); - // 114 (charoffset) + 600 (charlength) + 128 (grabbing right content) - multilineRegion.CharLength.Should().Be(114 + 600 + 128); + // 114 (charoffset) + 600 (charlength) + left-side + remainder of 128 chars. + multilineRegion.CharLength.Should().Be(114 + 600 + (128 - 114)); } [Fact]