Skip to content

Commit

Permalink
SLVS-1746 Change logic so that the diff view window shows all suggest…
Browse files Browse the repository at this point in the history
…ed changes that are selectable (#5966)
  • Loading branch information
1 parent d2dce51 commit 86c2eec
Show file tree
Hide file tree
Showing 16 changed files with 819 additions and 345 deletions.
246 changes: 246 additions & 0 deletions src/IssueViz.UnitTests/Editor/TextViewEditorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2025 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 Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
using NSubstitute.ExceptionExtensions;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.IssueVisualization.Editor;
using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models;
using SonarLint.VisualStudio.TestInfrastructure;

namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.Editor;

[TestClass]
public class TextViewEditorTests
{
private readonly List<ChangesDto> oneChange = [CreateChangesDto(1, 1, "var a=1;")];
private readonly List<ChangesDto> twoChanges = [CreateChangesDto(1, 1, "var a=1;"), CreateChangesDto(2, 2, "var b=0;")];
private IIssueSpanCalculator issueSpanCalculator;
private ILogger logger;
private TextViewEditor testSubject;
private ITextBuffer textBuffer;
private ITextBufferFactoryService textBufferFactoryService;
private ITextEdit textEdit;
private ITextView textView;

[TestInitialize]
public void TestInitialize()
{
issueSpanCalculator = Substitute.For<IIssueSpanCalculator>();
textBufferFactoryService = Substitute.For<ITextBufferFactoryService>();
logger = Substitute.For<ILogger>();
logger.ForContext(Arg.Any<string[]>()).Returns(logger);
testSubject = new TextViewEditor(issueSpanCalculator, logger, textBufferFactoryService);

MockCalculateSpan();
MockIssueSpanCalculatorIsSameHash(true);
MockTextBuffer();
}

[TestMethod]
public void MefCtor_CheckIsExported() =>
MefTestHelpers.CheckTypeCanBeImported<TextViewEditor, ITextViewEditor>(
MefTestHelpers.CreateExport<IIssueSpanCalculator>(),
MefTestHelpers.CreateExport<ILogger>(),
MefTestHelpers.CreateExport<ITextBufferFactoryService>());

[TestMethod]
public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent<TextViewEditor>();

[TestMethod]
public void Ctor_SetsContext() => logger.Received(1).ForContext(nameof(TextViewEditor));

[TestMethod]
public void ApplyChanges_OneChange_AppliesChange()
{
var suggestedChange = oneChange[0];

var applied = testSubject.ApplyChanges(textBuffer, oneChange, false);

applied.Should().BeTrue();
Received.InOrder(() =>
{
textBuffer.CreateEdit();
issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), suggestedChange.beforeLineRange.startLine, suggestedChange.beforeLineRange.endLine);
textEdit.Replace(Arg.Any<Span>(), Arg.Any<string>());
textEdit.Apply();
textEdit.Dispose();
});
}

[TestMethod]
public void ApplyChanges_OneChange_OriginalTextChanged_DoesNotApplyChangeWhenAbortIsTrue()
{
MockIssueSpanCalculatorIsSameHash(false);

var result = testSubject.ApplyChanges(textBuffer, oneChange, true);

result.Should().BeFalse();
textEdit.DidNotReceiveWithAnyArgs().Replace(default, default);
textEdit.DidNotReceiveWithAnyArgs().Apply();
textEdit.Received(1).Dispose();
}

[TestMethod]
public void ApplyChanges_OneChange_OriginalTextChanged_ApplyChangesWhenAbortIsFalse()
{
MockIssueSpanCalculatorIsSameHash(false);

var result = testSubject.ApplyChanges(textBuffer, oneChange, false);

result.Should().BeTrue();
textEdit.ReceivedWithAnyArgs(1).Replace(default, default);
textEdit.Received(1).Apply();
textEdit.Received(1).Dispose();
}

[TestMethod]
public void ApplyChanges_TwoChanges_CallsTextEditApplyOnce()
{
testSubject.ApplyChanges(textBuffer, twoChanges, false);

issueSpanCalculator.Received(2).CalculateSpan(Arg.Any<ITextSnapshot>(), Arg.Any<int>(), Arg.Any<int>());
textEdit.Received(2).Replace(Arg.Any<Span>(), Arg.Any<string>());
textEdit.Received(1).Apply();
}

///// <summary>
///// The changes are applied from bottom to top to avoid changing the line numbers
///// of the changes that are below the current change.
///// This is important when the change is more lines than the original line range.
///// </summary>
[TestMethod]
public void ApplyChanges_WhenMoreThanOneFixes_ApplyThemFromBottomToTop()
{
testSubject.ApplyChanges(textBuffer, twoChanges, false);

Received.InOrder(() =>
{
issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), 2, 2);
issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), 1, 1);
});
}

[TestMethod]
public void ApplyChanges_TwoChanges_OriginalTextChangedForOneOfTheChanges_DoesNotApplyChangeWhenAbortIsTrue()
{
issueSpanCalculator.IsSameHash(Arg.Any<SnapshotSpan>(), Arg.Any<string>()).Returns(true, false);

var result = testSubject.ApplyChanges(textBuffer, twoChanges, true);

result.Should().BeFalse();
textEdit.Received(1).Replace(Arg.Any<Span>(), Arg.Any<string>());
textEdit.DidNotReceiveWithAnyArgs().Apply();
textEdit.Received(1).Dispose();
}

[TestMethod]
public void ApplyChanges_WhenApplyingChangeAndExceptionIsThrown_ShouldDisposeEdit()
{
FailWhenApplyingEdit();

var act = () => testSubject.ApplyChanges(textBuffer, oneChange, false);

act.Should().Throw<Exception>();
textEdit.DidNotReceiveWithAnyArgs().Replace(default, default);
textEdit.Received().Dispose();
}

[TestMethod]
public void FocusLine_MovesToCaretAndEnsuresVisible()
{
var line = MockLineView(2);

testSubject.FocusLine(textView, 2);

Received.InOrder(() =>
{
textView.TextBuffer.CurrentSnapshot.GetLineFromLineNumber(2);
textView.Caret.MoveTo(line.Start);
textView.ViewScroller.EnsureSpanVisible(line.Extent);
});
}

[TestMethod]
public void FocusLine_WhenException_DoesNotThrowAndLogs()
{
var reason = "line does not exist";
textView.TextBuffer.CurrentSnapshot.GetLineFromLineNumber(2).Throws(new Exception(reason));

var act = () => testSubject.FocusLine(textView, 2);

act.Should().NotThrow();
logger.Received(1).LogVerbose(Resources.FocusLineFailed, 2, reason);
}

[TestMethod]
public void CreateTextBuffer_ShouldReturnTextBuffer()
{
var text = "some text";
var contentType = Substitute.For<IContentType>();
var expectedTextBuffer = Substitute.For<ITextBuffer>();
textBufferFactoryService.CreateTextBuffer(text, contentType).Returns(expectedTextBuffer);

var result = testSubject.CreateTextBuffer(text, contentType);

textBufferFactoryService.Received(1).CreateTextBuffer(text, contentType);
result.Should().Be(expectedTextBuffer);
}

private void MockIssueSpanCalculatorIsSameHash(bool isSameHash) => issueSpanCalculator.IsSameHash(Arg.Any<SnapshotSpan>(), Arg.Any<string>()).Returns(isSameHash);

private void MockCalculateSpan() => issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), Arg.Any<int>(), Arg.Any<int>()).Returns(_ => CreateMockedSnapshotSpan("some text"));

private static ChangesDto CreateChangesDto(
int startLine,
int endLine,
string before,
string after = "") =>
new(new LineRangeDto(startLine, endLine), before, after);

private void MockTextBuffer()
{
textBuffer = Substitute.For<ITextBuffer>();
textEdit = Substitute.For<ITextEdit>();
textBuffer.CreateEdit().Returns(textEdit);
textView = Substitute.For<ITextView>();
}

private ITextSnapshotLine MockLineView(int lineNumber)
{
var line = Substitute.For<ITextSnapshotLine>();
textView.TextBuffer.CurrentSnapshot.GetLineFromLineNumber(lineNumber).Returns(line);
return line;
}

private static SnapshotSpan CreateMockedSnapshotSpan(string text)
{
var mockTextSnapshot = Substitute.For<ITextSnapshot>();
mockTextSnapshot.Length.Returns(text.Length + 9999);

return new SnapshotSpan(mockTextSnapshot, new Span(0, text.Length));
}

private void FailWhenApplyingEdit() =>
issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), Arg.Any<int>(), Arg.Any<int>())
.Throws(new Exception());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2025 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 SonarLint.VisualStudio.Core.WPF;
using SonarLint.VisualStudio.IssueVisualization.FixSuggestion.DiffView;
using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models;

namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.FixSuggestion.DiffView;

[TestClass]
public class ChangeViewModelTests
{
private readonly ChangesDto changeDto = new(new LineRangeDto(1, 2), string.Empty, "var a=1;");
private ChangeViewModel testSubject;

[TestInitialize]
public void TestInitialize() => testSubject = new ChangeViewModel(changeDto, false);

[TestMethod]
public void ViewModel_InheritsViewModelBase() => testSubject.Should().BeAssignableTo<ViewModelBase>();

[TestMethod]
public void Ctor_InitializesProperties()
{
testSubject.ChangeDto.Should().Be(changeDto);
testSubject.IsSelected.Should().BeFalse();
}

[TestMethod]
public void IsSelected_SetValue_RaisesPropertyChanged()
{
var eventRaised = false;
testSubject.PropertyChanged += (sender, args) => eventRaised = true;

testSubject.IsSelected = true;

eventRaised.Should().BeTrue();
}

[TestMethod]
public void Line_PointToStartLineOfBeforeChange() => testSubject.Line.Should().Be(changeDto.beforeLineRange.startLine.ToString());
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,40 +19,41 @@
*/

using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Utilities;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.IssueVisualization.Editor;
using SonarLint.VisualStudio.IssueVisualization.FixSuggestion.DiffView;
using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models;
using SonarLint.VisualStudio.TestInfrastructure;

namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.FixSuggestion.DiffView;

[TestClass]
public class DiffViewServiceTests
{
private readonly FixSuggestionDetails fixSuggestionDetails = new(1, 3, "C://somePath/myFile.cs");
private readonly List<ChangesDto> twoChangesDtos = [CreateChangeDto(1, 1, "var a=1;"), CreateChangeDto(2, 2, "var b=0;")];
private IDiffViewToolWindowPane diffViewToolWindowPane;
private DiffViewService testSubject;
private ITextBufferFactoryService textBufferFactoryService;
private ITextBuffer textBuffer;
private ITextViewEditor textViewEditor;
private IToolWindowService toolWindowService;

[TestInitialize]
public void TestInitialize()
{
toolWindowService = Substitute.For<IToolWindowService>();
textBufferFactoryService = Substitute.For<ITextBufferFactoryService>();
textViewEditor = Substitute.For<ITextViewEditor>();
diffViewToolWindowPane = Substitute.For<IDiffViewToolWindowPane>();
toolWindowService.GetToolWindow<DiffViewToolWindowPane, IDiffViewToolWindowPane>().Returns(diffViewToolWindowPane);
textBuffer = Substitute.For<ITextBuffer>();

testSubject = new DiffViewService(
toolWindowService,
textBufferFactoryService);
testSubject = new DiffViewService(toolWindowService, textViewEditor);
}

[TestMethod]
public void MefCtor_CheckIsExported() =>
MefTestHelpers.CheckTypeCanBeImported<DiffViewService, IDiffViewService>(
MefTestHelpers.CreateExport<IToolWindowService>(),
MefTestHelpers.CreateExport<ITextBufferFactoryService>());
MefTestHelpers.CreateExport<ITextViewEditor>());

[TestMethod]
public void MefCtor_CheckIsNonShared() => MefTestHelpers.CheckIsNonSharedMefComponent<DiffViewService>();
Expand All @@ -63,34 +64,21 @@ public void MefCtor_CheckIsExported() =>
[TestMethod]
public void ShowDiffView_CallsShowDiffWithCorrectParameters()
{
var before = CreateChangeModel("int a=1;");
var after = CreateChangeModel("var a=1;");
var expectedBeforeTextBuffer = MockTextBuffer(before);
var expectedAfterTextBuffer = MockTextBuffer(after);
testSubject.ShowDiffView(textBuffer, twoChangesDtos);

testSubject.ShowDiffView(fixSuggestionDetails, before, after);

diffViewToolWindowPane.Received(1).ShowDiff(fixSuggestionDetails, expectedBeforeTextBuffer, expectedAfterTextBuffer);
diffViewToolWindowPane.Received(1).ShowDiff(Arg.Is<DiffViewViewModel>(vm => vm.TextBuffer == textBuffer && vm.ChangeViewModels.Select(x => x.ChangeDto).SequenceEqual(twoChangesDtos)));
}

[TestMethod]
[DataRow(true)]
[DataRow(false)]
public void ShowDiffView_ReturnsResultFromToolWindowPane(bool expectedResult)
public void ShowDiffView_ReturnsResultFromToolWindowPane()
{
diffViewToolWindowPane.ShowDiff(fixSuggestionDetails, Arg.Any<ITextBuffer>(), Arg.Any<ITextBuffer>()).Returns(expectedResult);

var applied = testSubject.ShowDiffView(fixSuggestionDetails, CreateChangeModel(string.Empty), CreateChangeModel(";"));
List<ChangesDto> expectedChangeDtos = [twoChangesDtos[0]];
diffViewToolWindowPane.ShowDiff(Arg.Any<DiffViewViewModel>()).Returns(expectedChangeDtos);

applied.Should().Be(expectedResult);
}
var acceptedChangeDtos = testSubject.ShowDiffView(textBuffer, twoChangesDtos);

private ITextBuffer MockTextBuffer(ChangeModel change)
{
var textBuffer = Substitute.For<ITextBuffer>();
textBufferFactoryService.CreateTextBuffer(change.Text, change.ContentType).Returns(textBuffer);
return textBuffer;
acceptedChangeDtos.Should().BeEquivalentTo(expectedChangeDtos);
}

private static ChangeModel CreateChangeModel(string text) => new(text, Substitute.For<IContentType>());
private static ChangesDto CreateChangeDto(int beforeLine, int afterLine, string after) => new(new LineRangeDto(beforeLine, afterLine), string.Empty, after);
}
Loading

0 comments on commit 86c2eec

Please sign in to comment.