Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions UndoRedo.Test/UndoRedoStackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,9 @@
stack.AddCommand(third);

// startIndex + count overflowed to a negative length before #105
CollectionAssert.AreEqual(new[] { second, third }, stack.GetCommandsInRange(1, int.MaxValue).ToList());

Check warning on line 255 in UndoRedo.Test/UndoRedoStackTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_UndoRedo&issues=AaDltqWZaZA5WhIAZcq0&open=AaDltqWZaZA5WhIAZcq0&pullRequest=107
CollectionAssert.AreEqual(new[] { third }, stack.GetCommandsInRange(2, int.MaxValue).ToList());

Check warning on line 256 in UndoRedo.Test/UndoRedoStackTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_UndoRedo&issues=AaDltqWZaZA5WhIAZcq1&open=AaDltqWZaZA5WhIAZcq1&pullRequest=107
CollectionAssert.AreEqual(new[] { second }, stack.GetCommandsInRange(1, 1).ToList());

Check warning on line 257 in UndoRedo.Test/UndoRedoStackTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_UndoRedo&issues=AaDltqWZaZA5WhIAZcq2&open=AaDltqWZaZA5WhIAZcq2&pullRequest=107
}

private sealed class MockNavigationProvider : INavigationProvider
Expand Down Expand Up @@ -844,10 +844,10 @@
stack.Execute(new DelegateCommand("C", () => value++, () => value--));
stack.Execute(new DelegateCommand("D", () => value++, () => value--)); // Trims A, moving the save point to -1
Assert.IsNotNull(heldBoundary);
Assert.AreEqual(3, stack.GetCommandsToUndo(heldBoundary).Count(), "The held boundary should resolve to the save point's current position");

Check warning on line 847 in UndoRedo.Test/UndoRedoStackTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.HasCount' instead of 'Assert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_UndoRedo&issues=AaDltqWZaZA5WhIAZcq4&open=AaDltqWZaZA5WhIAZcq4&pullRequest=107

// Act
bool result = await stack.UndoToSaveBoundaryAsync(heldBoundary, navigateToLastChange: false).ConfigureAwait(false);

Check warning on line 850 in UndoRedo.Test/UndoRedoStackTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_UndoRedo&issues=AaDltqWZaZA5WhIAZcq3&open=AaDltqWZaZA5WhIAZcq3&pullRequest=107

// Assert
Assert.IsTrue(result, "UndoToSaveBoundary should resolve a boundary held across a trim");
Expand All @@ -866,14 +866,14 @@
stack.Execute(new DelegateCommand("B", () => value++, () => value--));
stack.MarkAsSaved();
SaveBoundary removedBoundary = stack.SaveBoundaries[0];
await stack.UndoAsync(navigateToChange: false).ConfigureAwait(false);

Check warning on line 869 in UndoRedo.Test/UndoRedoStackTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_UndoRedo&issues=AaDltqWZaZA5WhIAZcq5&open=AaDltqWZaZA5WhIAZcq5&pullRequest=107
await stack.UndoAsync(navigateToChange: false).ConfigureAwait(false);

Check warning on line 870 in UndoRedo.Test/UndoRedoStackTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_UndoRedo&issues=AaDltqWZaZA5WhIAZcq6&open=AaDltqWZaZA5WhIAZcq6&pullRequest=107
stack.Execute(new DelegateCommand("C", () => value++, () => value--)); // Branches, discarding the save point
stack.Execute(new DelegateCommand("D", () => value++, () => value--));
stack.Execute(new DelegateCommand("E", () => value++, () => value--));

// Act
bool result = await stack.UndoToSaveBoundaryAsync(removedBoundary, navigateToLastChange: false).ConfigureAwait(false);

Check warning on line 876 in UndoRedo.Test/UndoRedoStackTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_UndoRedo&issues=AaDltqWZaZA5WhIAZcq7&open=AaDltqWZaZA5WhIAZcq7&pullRequest=107

// Assert
Assert.IsFalse(result, "UndoToSaveBoundary should reject a boundary that no longer exists");
Expand Down Expand Up @@ -1179,6 +1179,35 @@
}
}

public static IEnumerable<object[]> NavigationTimeoutsOutsideCancelAfterRange =>
[
[TimeSpan.MaxValue],
[TimeSpan.FromDays(60)],
[TimeSpan.FromSeconds(-5)],
[TimeSpan.MinValue],
];

[TestMethod]
[DynamicData(nameof(NavigationTimeoutsOutsideCancelAfterRange))]
public async Task UndoRedoAsync_NavigationTimeoutOutsideCancelAfterRange_AppliesAndNavigatesWithoutThrowing(TimeSpan timeout)
{
// Arrange
UndoRedoService stack = new(new StackManager(), new SaveBoundaryManager(), new CommandMerger(),
new UndoRedoOptions(DefaultNavigationTimeout: timeout));
MockNavigationProvider navigationProvider = new();
stack.SetNavigationProvider(navigationProvider);
int value = 0;
stack.Execute(new DelegateCommand("Increment", () => value++, () => value--, navigationContext: "editor"));

// Act & Assert: CancelAfter rejects these values, which surfaced after the change was applied
Assert.IsTrue(await stack.UndoAsync().ConfigureAwait(false));

Check warning on line 1203 in UndoRedo.Test/UndoRedoStackTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_UndoRedo&issues=AaDjhNHnAQ53IhMwPk4h&open=AaDjhNHnAQ53IhMwPk4h&pullRequest=107
Assert.AreEqual(0, value);
Assert.AreEqual("editor", navigationProvider.LastNavigatedContext, "Navigation should run with no timeout");

Assert.IsTrue(await stack.RedoAsync().ConfigureAwait(false));

Check warning on line 1207 in UndoRedo.Test/UndoRedoStackTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_UndoRedo&issues=AaDjhNHnAQ53IhMwPk4i&open=AaDjhNHnAQ53IhMwPk4i&pullRequest=107
Assert.AreEqual(1, value);
}

private sealed class ThrowingNavigationProvider : INavigationProvider
{
public Task<bool> NavigateToAsync(string context, CancellationToken cancellationToken = default) =>
Expand Down
18 changes: 15 additions & 3 deletions UndoRedo/Services/UndoRedoService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@
private SaveBoundary? FindLiveSaveBoundary(SaveBoundary saveBoundary) =>
_saveBoundaryManager.SaveBoundaries.FirstOrDefault(boundary => boundary.IsSameSavePointAs(saveBoundary));

/// <summary>
/// The longest navigation timeout <see cref="CancellationTokenSource.CancelAfter(TimeSpan)"/> accepts on every target framework.
/// </summary>
private static readonly TimeSpan MaxNavigationTimeout = TimeSpan.FromMilliseconds(int.MaxValue);

/// <summary>
/// Navigates to where a change was made, after the undo or redo has already been applied.
/// Navigation is best effort: any failure is swallowed, because an exception here would tell the
Expand All @@ -290,12 +295,19 @@
return;
}

using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(_options.EffectiveNavigationTimeout);

try
{
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

// CancelAfter throws for negative delays and for delays past its limit, which is
// int.MaxValue ms on older targets. Treat those as "no timeout" rather than failing here.
TimeSpan timeout = _options.EffectiveNavigationTimeout;
if (timeout > TimeSpan.Zero && timeout <= MaxNavigationTimeout)
{
cts.CancelAfter(timeout);
}

await _navigationProvider.NavigateToAsync(navigationContext!, cts.Token).ConfigureAwait(false);

Check warning on line 310 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 310 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 310 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 310 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 310 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 310 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 310 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 310 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 310 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 310 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception)
Expand Down
Loading