Skip to content

Commit

Permalink
Feature: Add Test Sequencer (#3587)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisPulman committed Jul 16, 2023
1 parent 3f11619 commit 3ede17e
Show file tree
Hide file tree
Showing 36 changed files with 830 additions and 304 deletions.
Expand Up @@ -32,7 +32,7 @@ public void CheckEmptyFileReturnsNoFailures()
[Fact]
public void ShouldGiveAnErrorWhenClassDoesNotImplement()
{
var test = @"
const string test = @"
using System;
using System.Collections.Generic;
using System.Linq;
Expand Down Expand Up @@ -68,7 +68,7 @@ public class TypeName
[Fact]
public void ShouldNotGiveAnErrorWhenClassInherits()
{
var test = @"
const string test = @"
using System;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -95,7 +95,7 @@ public class TypeName : ReactiveObject
[Fact]
public void ShouldNotGiveAnErrorWhenClassImplements()
{
var test = @"
const string test = @"
using System;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -122,7 +122,7 @@ public class TypeName : IReactiveObject
[Fact]
public void ShouldGiveErrorForNonAutoProperty()
{
var test = @"
const string test = @"
using System;
using System.Collections.Generic;
using System.Linq;
Expand Down
1 change: 0 additions & 1 deletion src/ReactiveUI.Testing.Tests/TestFixture.cs
Expand Up @@ -30,7 +30,6 @@ public class TestFixture
/// <summary>
/// Gets or sets the variables.
/// </summary>
[SuppressMessage("Design", "CA2227: Read only dictionary", Justification = "Used in mock.")]
public Dictionary<string, string>? Variables { get; set; }
}
}
42 changes: 42 additions & 0 deletions src/ReactiveUI.Testing.Tests/TestSequencerTests.cs
@@ -0,0 +1,42 @@
// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Xunit;

namespace ReactiveUI.Testing.Tests
{
/// <summary>
/// TestSequencerTests.
/// </summary>
public class TestSequencerTests
{
/// <summary>
/// Shoulds the execute tests in order.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task Should_Execute_Tests_In_Order()
{
using var testSequencer = new TestSequencer();
var subject = new Subject<Unit>();
subject.Subscribe(async _ => await testSequencer.AdvancePhaseAsync());

Assert.Equal(0, testSequencer.CurrentPhase);
Assert.Equal(0, testSequencer.CompletedPhases);
subject.OnNext(Unit.Default);
Assert.Equal(1, testSequencer.CurrentPhase);
Assert.Equal(0, testSequencer.CompletedPhases);
await testSequencer.AdvancePhaseAsync("Phase 1");
Assert.Equal(1, testSequencer.CurrentPhase);
Assert.Equal(1, testSequencer.CompletedPhases);
subject.OnNext(Unit.Default);
Assert.Equal(2, testSequencer.CurrentPhase);
Assert.Equal(1, testSequencer.CompletedPhases);
await testSequencer.AdvancePhaseAsync("Phase 2");
Assert.Equal(2, testSequencer.CurrentPhase);
Assert.Equal(2, testSequencer.CompletedPhases);
}
}
}
84 changes: 84 additions & 0 deletions src/ReactiveUI.Testing/TestSequencer.cs
@@ -0,0 +1,84 @@
// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace ReactiveUI.Testing
{
/// <summary>
/// Test Sequencer.
/// </summary>
/// <seealso cref="System.IDisposable" />
public class TestSequencer : IDisposable
{
private readonly Barrier _phaseSync;
private bool _disposedValue;

/// <summary>
/// Initializes a new instance of the <see cref="TestSequencer"/> class.
/// </summary>
public TestSequencer() => _phaseSync = new(2);

/// <summary>
/// Gets the number of completed phases.
/// </summary>
/// <value>
/// The completed phases.
/// </value>
public long CompletedPhases => _phaseSync.CurrentPhaseNumber;

/// <summary>
/// Gets the current phase.
/// </summary>
/// <value>
/// The current phase.
/// </value>
public long CurrentPhase { get; private set; }

/// <summary>
/// Advances this phase instance.
/// </summary>
/// <param name="comment">The comment for Test visual identification Purposes only.</param>
/// <returns>
/// A <see cref="Task" /> representing the asynchronous operation.
/// </returns>
#pragma warning disable RCS1163 // Unused parameter.
public Task AdvancePhaseAsync(string comment = "")
#pragma warning restore RCS1163 // Unused parameter.
{
if (_phaseSync.ParticipantCount == _phaseSync.ParticipantsRemaining)
{
CurrentPhase = CompletedPhases + 1;
}

return Task.Run(() => _phaseSync?.SignalAndWait(CancellationToken.None));
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_phaseSync.Dispose();
}

_disposedValue = true;
}
}
}
}
Expand Up @@ -43,4 +43,13 @@ namespace ReactiveUI.Testing
where T : System.Reactive.Concurrency.IScheduler { }
public static System.IDisposable WithScheduler(System.Reactive.Concurrency.IScheduler scheduler) { }
}
public class TestSequencer : System.IDisposable
{
public TestSequencer() { }
public long CompletedPhases { get; }
public long CurrentPhase { get; }
public System.Threading.Tasks.Task AdvancePhaseAsync(string comment = "") { }
public void Dispose() { }
protected virtual void Dispose(bool disposing) { }
}
}
Expand Up @@ -43,4 +43,13 @@ namespace ReactiveUI.Testing
where T : System.Reactive.Concurrency.IScheduler { }
public static System.IDisposable WithScheduler(System.Reactive.Concurrency.IScheduler scheduler) { }
}
public class TestSequencer : System.IDisposable
{
public TestSequencer() { }
public long CompletedPhases { get; }
public long CurrentPhase { get; }
public System.Threading.Tasks.Task AdvancePhaseAsync(string comment = "") { }
public void Dispose() { }
protected virtual void Dispose(bool disposing) { }
}
}
Expand Up @@ -43,4 +43,13 @@ namespace ReactiveUI.Testing
where T : System.Reactive.Concurrency.IScheduler { }
public static System.IDisposable WithScheduler(System.Reactive.Concurrency.IScheduler scheduler) { }
}
public class TestSequencer : System.IDisposable
{
public TestSequencer() { }
public long CompletedPhases { get; }
public long CurrentPhase { get; }
public System.Threading.Tasks.Task AdvancePhaseAsync(string comment = "") { }
public void Dispose() { }
protected virtual void Dispose(bool disposing) { }
}
}
21 changes: 12 additions & 9 deletions src/ReactiveUI.Tests/Activation/ActivatingViewTests.cs
Expand Up @@ -26,9 +26,10 @@ public void ActivatingViewSmokeTest()
using (locator.WithResolver())
{
var vm = new ActivatingViewModel();
var fixture = new ActivatingView();

fixture.ViewModel = vm;
var fixture = new ActivatingView
{
ViewModel = vm
};
Assert.Equal(0, vm.IsActiveCount);
Assert.Equal(0, fixture.IsActiveCount);

Expand Down Expand Up @@ -56,9 +57,10 @@ public void NullingViewModelDeactivateIt()
using (locator.WithResolver())
{
var vm = new ActivatingViewModel();
var fixture = new ActivatingView();

fixture.ViewModel = vm;
var fixture = new ActivatingView
{
ViewModel = vm
};
Assert.Equal(0, vm.IsActiveCount);
Assert.Equal(0, fixture.IsActiveCount);

Expand Down Expand Up @@ -151,9 +153,10 @@ public void CanUnloadAndLoadViewAgain()
using (locator.WithResolver())
{
var vm = new ActivatingViewModel();
var fixture = new ActivatingView();

fixture.ViewModel = vm;
var fixture = new ActivatingView
{
ViewModel = vm
};
Assert.Equal(0, vm.IsActiveCount);
Assert.Equal(0, fixture.IsActiveCount);

Expand Down
4 changes: 1 addition & 3 deletions src/ReactiveUI.Tests/BindingTypeConvertersTest.cs
Expand Up @@ -7,8 +7,6 @@

namespace ReactiveUI.Tests
{
using System;

/// <summary>
/// Tests for binding type converters.
/// </summary>
Expand Down Expand Up @@ -66,7 +64,7 @@ public void EqualityTypeConverterDoReferenceCastNullableToValueType()
[Fact]
public void EqualityTypeConverterDoReferenceCastShouldConvertValueTypes()
{
var doubleValue = 1.0;
const double doubleValue = 1.0;
var result = EqualityTypeConverter.DoReferenceCast(doubleValue, typeof(double));
Assert.Equal(doubleValue, result);
}
Expand Down
24 changes: 10 additions & 14 deletions src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs
Expand Up @@ -1199,25 +1199,21 @@ public async Task ReactiveCommandCreateFromTaskHandlesTaskExceptionAsync()
[Fact]
public async Task ReactiveCommandExecutesFromInvokeCommand()
{
var semaphore = new SemaphoreSlim(0);
var command = ReactiveCommand.Create(() => semaphore.Release());
using var testSequencer = new TestSequencer();

var command = ReactiveCommand.Create(async () => await testSequencer.AdvancePhaseAsync("Phase 1"));
var result = 0;

// False, True, False
command.IsExecuting.Subscribe(_ => result++);

Observable.Return(Unit.Default)
.InvokeCommand(command);

var result = 0;
var task = semaphore.WaitAsync();
if (await Task.WhenAny(Task.Delay(TimeSpan.FromMilliseconds(100)), task).ConfigureAwait(true) == task)
{
result = 1;
}
else
{
result = -1;
}
await testSequencer.AdvancePhaseAsync("Phase 1");
Assert.Equal(3, result);

await Task.Delay(200).ConfigureAwait(false);
Assert.Equal(1, result);
testSequencer.Dispose();
}
}
}
24 changes: 15 additions & 9 deletions src/ReactiveUI.Tests/Locator/DefaultViewLocatorTests.cs
Expand Up @@ -13,7 +13,7 @@ namespace ReactiveUI.Tests
public class DefaultViewLocatorTests
{
/// <summary>
/// Tests that the the default name of the view model is replaced with view when determining the service.
/// Tests that the default name of the view model is replaced with view when determining the service.
/// </summary>
[Fact]
public void ByDefaultViewModelIsReplacedWithViewWhenDeterminingTheServiceName()
Expand Down Expand Up @@ -70,9 +70,11 @@ public void ViewModelToViewNamingConventionCanBeCustomized()

using (resolver.WithResolver())
{
var fixture = new DefaultViewLocator();
fixture.ViewModelToViewFunc =
viewModelName => viewModelName.Replace("ViewModel", "WithWeirdConvention");
var fixture = new DefaultViewLocator
{
ViewModelToViewFunc =
viewModelName => viewModelName.Replace("ViewModel", "WithWeirdConvention")
};
var vm = new FooViewModel();

var result = fixture.ResolveView(vm);
Expand Down Expand Up @@ -254,9 +256,11 @@ public void NoErrorIsRaisedIfATypeCannotBeFound()

using (resolver.WithResolver())
{
var fixture = new DefaultViewLocator();
fixture.ViewModelToViewFunc = viewModelName =>
"DoesNotExist, " + typeof(DefaultViewLocatorTests).Assembly.FullName;
var fixture = new DefaultViewLocator
{
ViewModelToViewFunc = viewModelName =>
"DoesNotExist, " + typeof(DefaultViewLocatorTests).Assembly.FullName
};
var vm = new FooViewModel();

var result = fixture.ResolveView(vm);
Expand Down Expand Up @@ -386,8 +390,10 @@ public void CanOverrideNameResolutionFunc()

using (resolver.WithResolver())
{
var fixture = new DefaultViewLocator();
fixture.ViewModelToViewFunc = x => x.Replace("ViewModel", "CustomView");
var fixture = new DefaultViewLocator
{
ViewModelToViewFunc = x => x.Replace("ViewModel", "CustomView")
};
var vm = new RoutableFooViewModel();

var result = fixture.ResolveView<IRoutableViewModel>(vm);
Expand Down

0 comments on commit 3ede17e

Please sign in to comment.