Skip to content

Commit

Permalink
Add IssueMatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
georgii-borovinskikh-sonarsource committed Nov 29, 2023
1 parent 0e75982 commit fe5d47f
Show file tree
Hide file tree
Showing 5 changed files with 336 additions and 148 deletions.
162 changes: 162 additions & 0 deletions src/ConnectedMode.UnitTests/IssueMatcherTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System;
using SonarLint.VisualStudio.Core.Suppressions;
using SonarLint.VisualStudio.TestInfrastructure;
using SonarQube.Client.Models;

namespace SonarLint.VisualStudio.ConnectedMode.UnitTests;

[TestClass]
public class IssueMatcherTests
{
[TestMethod]
public void MefCtor_CheckIsExported()
{
MefTestHelpers.CheckTypeCanBeImported<IssueMatcher, IIssueMatcher>();
}

[TestMethod]
public void MefCtor_CheckIsSingleton()
{
MefTestHelpers.CheckIsSingletonMefComponent<IssueMatcher>();
}

[DataTestMethod]
[DataRow("CorrectRuleId", 1, "CorrectHash", true)] // exact matches
[DataRow("correctRULEID", 1, "CorrectHash", true)] // rule-id is case-insensitive
[DataRow("CorrectRuleId", 1, "wrong hash", true)] // matches on line
[DataRow("CorrectRuleId", 9999, "CorrectHash", true)] // matches on hash only
[DataRow("CorrectRuleId", 2, "correcthash", false)] // hash is case-sensitive
[DataRow("CorrectRuleId", 2, "wrong hash", false)] // wrong line and hash
[DataRow("CorrectRuleId", null, null, false)] // server file issue
[DataRow("wrong rule Id", 1, "CorrectHash", false)]
public void IsMatch_MatchesBasedOnAllParameters(string serverRuleId, int? serverIssueLine,
string serverHash, bool expectedResult)
{
var issueToMatch = CreateIssueToMatch("CorrectRuleId", 1, "CorrectHash");
var serverIssue = CreateServerIssue(serverRuleId, serverIssueLine, serverHash);

CreateTestSubject().IsGoodMatch(issueToMatch, serverIssue).Should().Be(expectedResult);
}

[DataTestMethod]
[DataRow("CorrectRuleId", null, null, true)] // exact matches
[DataRow("CorrectRuleId", null, "hash", true)] // hash should be ignored for file-level issues
[DataRow("WrongRuleId", null, null, false)] // wrong rule
[DataRow("CorrectRuleId", 1, "hash", false)] // not a file issue
[DataRow("CorrectRuleId", 999, null, false)] // not a file issue - should not match a file issue, even though the hash is the same
public void IsMatch_FileLevelIssue(string serverRuleId, int? serverIssueLine,
string serverHash, bool expectedResult)
{
// File issues have line number of 0 and an empty hash
var issueToMatch = CreateIssueToMatch("CorrectRuleId", null, null);
var serverIssue = CreateServerIssue(serverRuleId, serverIssueLine, serverHash);

CreateTestSubject().IsGoodMatch(issueToMatch, serverIssue);
}

[TestMethod]
// Module-level issues i.e. no file
[DataRow(null, null, true)]
[DataRow(null, "", true)]
[DataRow("", null, true)]
[DataRow("", "", true)]

// Module-level issues should not match non-module-level issues
[DataRow(@"any.txt", "", false)]
[DataRow(@"any.txt", null, false)]
[DataRow("", @"c:\any.txt", false)]
[DataRow(null, @"c:\any.txt", false)]

// File issues
[DataRow(@"same.txt", @"c:\same.txt", true)]
[DataRow(@"SAME.TXT", @"c:\same.txt", true)]
[DataRow(@"same.TXT", @"c:\XXXsame.txt", false)] // partial file name -> should not match
[DataRow(@"differentExt.123", @"a:\differentExt.999", false)] // different extension -> should not match
[DataRow(@"aaa\partial\file.cs", @"d:\partial\file.cs", false)]
// Only matching the local path tail, so the same server path can match multiple local files
[DataRow(@"partial\file.cs", @"c:\aaa\partial\file.cs", true)]
[DataRow(@"partial\file.cs", @"c:\aaa\bbb\partial\file.cs", true)]
[DataRow(@"partial\file.cs", @"c:\aaa\bbb\ccc\partial\file.cs", true)]
public void IsMatch_CheckFileComparisons(string serverFilePath, string localFilePath, bool expected)
{
var issueToMatch = CreateIssueToMatch("111", 0, "hash", filePath: localFilePath);

var serverIssue = CreateServerIssue("111", 0, "hash", filePath: serverFilePath);

CreateTestSubject().IsGoodMatch(issueToMatch, serverIssue).Should().Be(expected);
}

[TestMethod]
public void FindMatchOrDefault_FindsFirstMatch()
{
var ruleId = "111";
var startLine = 0;
var issueToMatch = CreateIssueToMatch(ruleId, startLine, null, @"c:\root\dir\file.cs");
var serverPath = @"dir\file.cs";

var correctServerIssue = CreateServerIssue(ruleId, startLine, null, serverPath);

CreateTestSubject().GetFirstMatchFromSameFileOrNull(issueToMatch, new[]
{
CreateServerIssue("222", startLine, null, serverPath),
CreateServerIssue(ruleId, 111, null, serverPath),
correctServerIssue,
CreateServerIssue(ruleId, startLine, null, serverPath) // finds only the firs match
}).Should().BeSameAs(correctServerIssue);
}

[TestMethod]
public void FindMatchOrDefault_NoServerIssues_ReturnsNull()
{
var issueToMatch = CreateIssueToMatch("1", 1, "1");

CreateTestSubject().GetFirstMatchFromSameFileOrNull(issueToMatch, Array.Empty<SonarQubeIssue>()).Should().BeNull();
}

private IssueMatcher CreateTestSubject()
{
return new IssueMatcher();
}

private IFilterableIssue CreateIssueToMatch(string ruleId, int? startLine, string lineHash, string filePath = null) =>
new TestFilterableIssue
{
RuleId = ruleId,
StartLine = startLine,
LineHash = lineHash,
FilePath = filePath
};

private SonarQubeIssue CreateServerIssue(string ruleId, int? startLine, string lineHash,
string filePath = null)
{
var sonarQubeIssue = new SonarQubeIssue(null, filePath, lineHash, null, null, ruleId, false, SonarQubeIssueSeverity.Info,
DateTimeOffset.MinValue, DateTimeOffset.MinValue,
startLine.HasValue
? new IssueTextRange(startLine.Value, 1, 1, 1)
: null,
flows: null);

return sonarQubeIssue;
}
}
148 changes: 38 additions & 110 deletions src/ConnectedMode.UnitTests/Suppressions/SuppressedIssueMatcherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,22 @@ public class SuppressedIssueMatcherTests
{
private SuppressedIssueMatcher testSubject;
private Mock<IServerIssuesStore> mockServerIssuesStore;
private Mock<IIssueMatcher> issueMatcherMock;

[TestInitialize]
public void TestInitialize()
{
mockServerIssuesStore = new Mock<IServerIssuesStore>();
testSubject = new SuppressedIssueMatcher(mockServerIssuesStore.Object);
issueMatcherMock = new Mock<IIssueMatcher>();
testSubject = new SuppressedIssueMatcher(mockServerIssuesStore.Object, issueMatcherMock.Object);
}

[TestMethod]
public void MefCtor_CheckIsExported()
{
MefTestHelpers.CheckTypeCanBeImported<SuppressedIssueMatcher, ISuppressedIssueMatcher>(
MefTestHelpers.CreateExport<IServerIssuesStore>());
MefTestHelpers.CreateExport<IServerIssuesStore>(),
MefTestHelpers.CreateExport<IIssueMatcher>());
}

[TestMethod]
Expand All @@ -53,134 +56,70 @@ public void MatchExists_NullIssue_Throws()
act.Should().ThrowExactly<ArgumentNullException>().And.ParamName.Should().Be("issue");
}

[DataTestMethod]
[DataRow("CorrectRuleId", 1, "CorrectHash", true)] // exact matches
[DataRow("correctRULEID", 1, "CorrectHash", true)] // rule-id is case-insensitive
[DataRow("CorrectRuleId", 1, "wrong hash", true)] // matches on line
[DataRow("CorrectRuleId", 9999, "CorrectHash", true)] // matches on hash only
[DataRow("CorrectRuleId", 2, "correcthash", false)] // hash is case-sensitive
[DataRow("CorrectRuleId", 2, "wrong hash", false)] // wrong line and hash
[DataRow("CorrectRuleId", null, null, false)] // server file issue
[DataRow("wrong rule Id", 1, "CorrectHash", false)]
public void MatchExists_LocalNonFileIssue_SingleServerIssue(string serverRuleId, int? serverIssueLine, string serverHash, bool expectedResult)
{
var issueToMatch = CreateIssueToMatch("CorrectRuleId", 1, "CorrectHash");
ConfigureServerIssues(CreateServerIssue(serverRuleId, serverIssueLine, serverHash, isSuppressed: true));

// Act and assert
testSubject.SuppressionExists(issueToMatch).Should().Be(expectedResult);
}

[DataTestMethod]
[DataRow("CorrectRuleId", null, null, true)] // exact matches
[DataRow("CorrectRuleId", null, "hash", true)] // hash should be ignored for file-level issues
[DataRow("WrongRuleId", null, null, false)] // wrong rule
[DataRow("CorrectRuleId", 1, "hash", false)] // not a file issue
[DataRow("CorrectRuleId", 999, null, false)] // not a file issue - should not match a file issue, even though the hash is the same
public void MatchExists_LocalFileIssue_SingleServerIssue(string serverRuleId, int? serverIssueLine, string serverHash, bool expectedResult)
{
// File issues have line number of 0 and an empty hash
var issueToMatch = CreateIssueToMatch("CorrectRuleId", null, null);
ConfigureServerIssues(CreateServerIssue(serverRuleId, serverIssueLine, serverHash, expectedResult));

// Act and assert
testSubject.SuppressionExists(issueToMatch).Should().Be(expectedResult);
}

[TestMethod]
public void MatchExists_NoServerIssues_ReturnsFalse()
{
// Arrange
var issueToMatch = CreateIssueToMatch("rule1", 1, "hash1");
var issueToMatch = CreateIssueToMatch();
ConfigureServerIssues(Array.Empty<SonarQubeIssue>());

// Act and assert
testSubject.SuppressionExists(issueToMatch).Should().BeFalse();

issueMatcherMock.VerifyNoOtherCalls();
}

[DataTestMethod]
[DataRow("aaa", 222, "aaa hash", true)]
[DataRow("bbb", 333, "bbb hash", true)]
[DataRow("ccc", 444, "ccc hash", true)]
[DataRow("xxx", 111, "xxx hash", false)]
public void MatchExists_MultipleServerIssues(string localRuleId, int localIssueLine, string localHash, bool expectedResult)
[DataRow(0)]
[DataRow(1)]
[DataRow(2)]
[DataRow(-1)]
public void MatchExists_MultipleServerIssues(int indexOfServerIssue)
{
// Arrange
var issueToMatch = CreateIssueToMatch(localRuleId, localIssueLine, localHash);
var issueToMatch = CreateIssueToMatch();
var sonarQubeIssues = new []
{
CreateServerIssue(),
CreateServerIssue(),
CreateServerIssue()
};
var hasMatch = indexOfServerIssue != -1;
if (hasMatch)
{
issueMatcherMock.Setup(x => x.IsGoodMatch(issueToMatch, sonarQubeIssues[indexOfServerIssue])).Returns(true);
}

ConfigureServerIssues(
CreateServerIssue("aaa", 222, "aaa hash", isSuppressed: true),
CreateServerIssue("bbb", 333, "bbb hash", isSuppressed: true),
CreateServerIssue("ccc", 444, "ccc hash", isSuppressed: true));
ConfigureServerIssues(sonarQubeIssues);

// Act and assert
testSubject.SuppressionExists(issueToMatch).Should().Be(expectedResult);
testSubject.SuppressionExists(issueToMatch).Should().Be(hasMatch);
}

[DataTestMethod]
[DataRow("CorrectRuleId", null, null, true, true)]
[DataRow("CorrectRuleId", null, null, false, false)]
public void MatchExists_ResultDependsOnSuppressionState(string serverRuleId, int? serverIssueLine, string serverHash, bool isSuppressed, bool expectedResult)
public void MatchExists_ResultDependsOnSuppressionState(string serverRuleId, int? serverIssueLine,
string serverHash, bool isSuppressed, bool expectedResult)
{
// File issues have line number of 0 and an empty hash
var issueToMatch = CreateIssueToMatch("CorrectRuleId", null, null);
ConfigureServerIssues(CreateServerIssue(serverRuleId, serverIssueLine, serverHash, isSuppressed));
var issueToMatch = CreateIssueToMatch();
ConfigureServerIssues(CreateServerIssue(isSuppressed));
issueMatcherMock
.Setup(x => x.IsGoodMatch(It.IsAny<IFilterableIssue>(), It.IsAny<SonarQubeIssue>()))
.Returns(true);

// Act and assert
testSubject.SuppressionExists(issueToMatch).Should().Be(expectedResult);
}

[TestMethod]
// Module-level issues i.e. no file
[DataRow(null, null, true)]
[DataRow(null, "", true)]
[DataRow("", null, true)]
[DataRow("", "", true)]

// Module-level issues should not match non-module-level issues
[DataRow(@"any.txt", "", false)]
[DataRow(@"any.txt", null, false)]
[DataRow("", @"c:\any.txt", false)]
[DataRow(null, @"c:\any.txt", false)]

// File issues
[DataRow(@"same.txt", @"c:\same.txt", true)]
[DataRow(@"SAME.TXT", @"c:\same.txt", true)]
[DataRow(@"same.TXT", @"c:\XXXsame.txt", false)] // partial file name -> should not match
[DataRow(@"differentExt.123", @"a:\differentExt.999", false)] // different extension -> should not match
[DataRow(@"aaa\partial\file.cs", @"d:\partial\file.cs", false)]
// Only matching the local path tail, so the same server path can match multiple local files
[DataRow(@"partial\file.cs", @"c:\aaa\partial\file.cs", true)]
[DataRow(@"partial\file.cs", @"c:\aaa\bbb\partial\file.cs", true)]
[DataRow(@"partial\file.cs", @"c:\aaa\bbb\ccc\partial\file.cs", true)]
public void SuppressionExists_CheckFileComparisons(string serverFilePath, string localFilePath, bool expected)
{
var issueToMatch = CreateIssueToMatch("111", 0, "hash", filePath: localFilePath);

ConfigureServerIssues(CreateServerIssue("111", 0, "hash", true, filePath: serverFilePath));

// Act and assert
testSubject.SuppressionExists(issueToMatch).Should().Be(expected);
}

private IFilterableIssue CreateIssueToMatch(string ruleId, int? startLine, string lineHash,
string filePath = null)
=> new TestFilterableIssue
{
RuleId = ruleId,
StartLine = startLine,
LineHash = lineHash,
FilePath = filePath
};
private IFilterableIssue CreateIssueToMatch() => Mock.Of<IFilterableIssue>();

private SonarQubeIssue CreateServerIssue(string ruleId, int? startLine, string lineHash, bool isSuppressed,
string filePath = null)
private SonarQubeIssue CreateServerIssue(bool isSuppressed = true)
{
var sonarQubeIssue = new SonarQubeIssue(null, filePath, lineHash, null, null, ruleId, false, SonarQubeIssueSeverity.Info,
var sonarQubeIssue = new SonarQubeIssue(null, default, default, null, null, default, false, SonarQubeIssueSeverity.Info,
DateTimeOffset.MinValue, DateTimeOffset.MinValue,
startLine.HasValue
? new IssueTextRange(startLine.Value, 1, 1, 1)
: null,
null,
flows: null);

sonarQubeIssue.IsResolved = isSuppressed;
Expand All @@ -194,16 +133,5 @@ private void ConfigureServerIssues(
mockServerIssuesStore
.Setup(x => x.Get()).Returns(issuesToReturn);
}

private class TestFilterableIssue : IFilterableIssue
{
public string RuleId { get; set; }
public string LineHash { get; set; }
public int? StartLine { get; set; }
public string FilePath { get; set; }

// Not expecting the other property to be used
public string WholeLineText => throw new NotImplementedException();
}
}
}
Loading

0 comments on commit fe5d47f

Please sign in to comment.