From 7a30c95d162ec3a3ce1691f7b72b20e515a7c785 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Thu, 19 Jan 2023 18:26:28 +0100 Subject: [PATCH] tests(core): add tests for contracts --- .../Contracts/ContractData.cs | 28 ++ .../Contracts/ContractError.cs | 18 + .../Contracts/ContractMultipleStates.cs | 33 ++ .../Contracts/ContractTests.cs | 360 ++++++++++++++++++ .../NosSmooth.Core.Tests.csproj | 2 + 5 files changed, 441 insertions(+) create mode 100644 Tests/NosSmooth.Core.Tests/Contracts/ContractData.cs create mode 100644 Tests/NosSmooth.Core.Tests/Contracts/ContractError.cs create mode 100644 Tests/NosSmooth.Core.Tests/Contracts/ContractMultipleStates.cs create mode 100644 Tests/NosSmooth.Core.Tests/Contracts/ContractTests.cs diff --git a/Tests/NosSmooth.Core.Tests/Contracts/ContractData.cs b/Tests/NosSmooth.Core.Tests/Contracts/ContractData.cs new file mode 100644 index 0000000..25afaad --- /dev/null +++ b/Tests/NosSmooth.Core.Tests/Contracts/ContractData.cs @@ -0,0 +1,28 @@ +// +// ContractData.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace NosSmooth.Core.Tests.Contracts; + +/// +/// Data for updating a contract. +/// +/// The type of the data. +public class ContractData +{ + /// + /// Initializes a new instance of the class. + /// + /// The data to pass. + public ContractData(T data) + { + Data = data; + } + + /// + /// Gets the data. + /// + public T Data { get; } +} \ No newline at end of file diff --git a/Tests/NosSmooth.Core.Tests/Contracts/ContractError.cs b/Tests/NosSmooth.Core.Tests/Contracts/ContractError.cs new file mode 100644 index 0000000..8d88bf7 --- /dev/null +++ b/Tests/NosSmooth.Core.Tests/Contracts/ContractError.cs @@ -0,0 +1,18 @@ +// +// ContractError.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace NosSmooth.Core.Tests.Contracts; + +/// +/// Errors for a contract. +/// +public enum ContractError +{ + /// + /// An error. + /// + Error1 +} \ No newline at end of file diff --git a/Tests/NosSmooth.Core.Tests/Contracts/ContractMultipleStates.cs b/Tests/NosSmooth.Core.Tests/Contracts/ContractMultipleStates.cs new file mode 100644 index 0000000..7daf732 --- /dev/null +++ b/Tests/NosSmooth.Core.Tests/Contracts/ContractMultipleStates.cs @@ -0,0 +1,33 @@ +// +// ContractMultipleStates.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace NosSmooth.Core.Tests.Contracts; + +/// +/// Extends DefaultStates to have more states to test. +/// +public enum ContractMultipleStates +{ + /// + /// Initial state. + /// + None, + + /// + /// Contract executed, request issued. + /// + Requested, + + /// + /// A response was obtained. + /// + ResponseObtained, + + /// + /// Something else happening after obtaining first response. + /// + AfterResponseObtained, +} \ No newline at end of file diff --git a/Tests/NosSmooth.Core.Tests/Contracts/ContractTests.cs b/Tests/NosSmooth.Core.Tests/Contracts/ContractTests.cs new file mode 100644 index 0000000..1e3cd32 --- /dev/null +++ b/Tests/NosSmooth.Core.Tests/Contracts/ContractTests.cs @@ -0,0 +1,360 @@ +// +// ContractTests.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using NosSmooth.Core.Contracts; +using Remora.Results; +using Shouldly; +using Xunit; + +namespace NosSmooth.Core.Tests.Contracts; + +/// +/// Tests basics of contract system. +/// +public class ContractTests +{ + /// + /// Tests that the contract is executed. + /// + /// A representing the asynchronous operation. + [Fact] + public async Task Test_GetsExecuted() + { + var contractor = new Contractor(); + var mock = new MockClass(); + + var contract = new ContractBuilder(contractor, DefaultStates.None) + .SetMoveAction(DefaultStates.None, mock.Setup, DefaultStates.Requested) + .SetMoveFilter>(DefaultStates.Requested, DefaultStates.ResponseObtained) + .SetFillData>(DefaultStates.ResponseObtained, d => d.Data) + .Build(); + + contract.CurrentState.ShouldBe(DefaultStates.None); + await contract.OnlyExecuteAsync(); + contract.CurrentState.ShouldBe(DefaultStates.Requested); + mock.Executed.ShouldBeTrue(); + mock.ExecutedTimes.ShouldBe(1); + } + + /// + /// Tests that the contract response is obtained. + /// + /// A representing the asynchronous operation. + [Fact] + public async Task Test_ResponseObtained() + { + var contractor = new Contractor(); + var mock = new MockClass(); + + var contract = new ContractBuilder(contractor, DefaultStates.None) + .SetMoveAction(DefaultStates.None, mock.Setup, DefaultStates.Requested) + .SetMoveFilter>(DefaultStates.Requested, DefaultStates.ResponseObtained) + .SetFillData>(DefaultStates.ResponseObtained, d => d.Data) + .Build(); + + await contract.OnlyExecuteAsync(); + contract.Register(); + await contractor.Update(new ContractData(5)); + contract.CurrentState.ShouldBe(DefaultStates.ResponseObtained); + contract.Data.ShouldBe(5); + + await contractor.Update(new ContractData(10)); + contract.Data.ShouldBe(5); + contract.Unregister(); + + mock.ExecutedTimes.ShouldBe(1); + } + + /// + /// Tests that the contract response is obtained. + /// + /// A representing the asynchronous operation. + [Fact] + public async Task Test_WaitFor() + { + var contractor = new Contractor(); + var mock = new MockClass(); + + var contract = new ContractBuilder(contractor, DefaultStates.None) + .SetMoveAction(DefaultStates.None, mock.Setup, DefaultStates.Requested) + .SetMoveFilter>(DefaultStates.Requested, DefaultStates.ResponseObtained) + .SetFillData>(DefaultStates.ResponseObtained, d => d.Data) + .Build(); + + Task.Run + ( + async () => + { + await Task.Delay(500); + await contractor.Update(new ContractData(15)); + } + ); + + var result = await contract.WaitForAsync(DefaultStates.ResponseObtained); + result.IsSuccess.ShouldBeTrue(); + result.Entity.ShouldBe(15); + contract.IsRegistered.ShouldBeFalse(); // trust the contract for now. + contract.CurrentState.ShouldBe(DefaultStates.ResponseObtained); + mock.ExecutedTimes.ShouldBe(1); + } + + /// + /// Tests that the contract response is obtained. + /// + /// A representing the asynchronous operation. + [Fact] + public async Task Test_WaitForMoreStates() + { + var contractor = new Contractor(); + var mock = new MockClass(); + + var contract = new ContractBuilder + (contractor, ContractMultipleStates.None) + .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested) + .SetMoveFilter> + (ContractMultipleStates.Requested, ContractMultipleStates.ResponseObtained) + .SetFillData>(ContractMultipleStates.ResponseObtained, d => d.Data) + .SetMoveFilter> + (ContractMultipleStates.ResponseObtained, c => c.Data, ContractMultipleStates.AfterResponseObtained) + .Build(); + + Task.Run + ( + async () => + { + await Task.Delay(500); + await contractor.Update(new ContractData(15)); + await Task.Delay(200); + await contractor.Update(new ContractData(true)); + } + ); + + var result = await contract.WaitForAsync(ContractMultipleStates.AfterResponseObtained); + result.IsSuccess.ShouldBeTrue(); + result.Entity.ShouldBe(15); + contract.IsRegistered.ShouldBeFalse(); // trust the contract for now. + contract.CurrentState.ShouldBe(ContractMultipleStates.AfterResponseObtained); + mock.ExecutedTimes.ShouldBe(1); + } + + /// + /// Tests that the contract response is obtained. + /// + /// A representing the asynchronous operation. + [Fact] + public async Task Test_MoreStatesFollowed() + { + var contractor = new Contractor(); + var mock = new MockClass(); + + var contract = new ContractBuilder + (contractor, ContractMultipleStates.None) + .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested) + .SetMoveFilter> + (ContractMultipleStates.Requested, ContractMultipleStates.ResponseObtained) + .SetFillData>(ContractMultipleStates.ResponseObtained, d => d.Data) + .SetMoveFilter> + (ContractMultipleStates.ResponseObtained, c => c.Data, ContractMultipleStates.AfterResponseObtained) + .Build(); + + await contract.OnlyExecuteAsync(); + contract.Register(); + await contractor.Update(new ContractData(15)); + await contractor.Update(new ContractData(true)); + contract.Unregister(); + var result = await contract.WaitForAsync(ContractMultipleStates.AfterResponseObtained); + result.IsSuccess.ShouldBeTrue(); + result.Entity.ShouldBe(15); + contract.IsRegistered.ShouldBeFalse(); // trust the contract for now. + contract.CurrentState.ShouldBe(ContractMultipleStates.AfterResponseObtained); + mock.ExecutedTimes.ShouldBe(1); + } + + /// + /// Tests that the contract response is obtained. + /// + /// A representing the asynchronous operation. + [Fact] + public async Task Test_WaitForTimeout() + { + var contractor = new Contractor(); + var mock = new MockClass(); + + var contract = new ContractBuilder + (contractor, ContractMultipleStates.None) + .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested) + .SetMoveFilter> + (ContractMultipleStates.Requested, ContractMultipleStates.ResponseObtained) + .SetFillData>(ContractMultipleStates.ResponseObtained, d => d.Data) + .SetTimeout + ( + ContractMultipleStates.ResponseObtained, + TimeSpan.FromMilliseconds(100), + ContractMultipleStates.AfterResponseObtained + ) + .Build(); + + Task.Run + ( + async () => + { + await Task.Delay(500); + await contractor.Update(new ContractData(15)); + } + ); + + var result = await contract.WaitForAsync(ContractMultipleStates.AfterResponseObtained); + result.IsSuccess.ShouldBeTrue(); + result.Entity.ShouldBe(15); + contract.IsRegistered.ShouldBeFalse(); // trust the contract for now. + contract.CurrentState.ShouldBe(ContractMultipleStates.AfterResponseObtained); + mock.ExecutedTimes.ShouldBe(1); + } + + /// + /// Tests that the contract response is obtained. + /// + /// A representing the asynchronous operation. + [Fact] + public async Task Test_MultipleContracts() + { + var contractor = new Contractor(); + var mock = new MockClass(); + + var contract1 = new ContractBuilder + (contractor, ContractMultipleStates.None) + .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested) + .SetMoveFilter> + ( + ContractMultipleStates.Requested, + d => d.Data > 10 && d.Data < 20, + ContractMultipleStates.ResponseObtained + ) + .SetFillData>(ContractMultipleStates.ResponseObtained, d => d.Data) + .Build(); + + var contract2 = new ContractBuilder + (contractor, ContractMultipleStates.None) + .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested) + .SetMoveFilter> + (ContractMultipleStates.Requested, d => d.Data > 20, ContractMultipleStates.ResponseObtained) + .SetFillData>(ContractMultipleStates.ResponseObtained, d => d.Data) + .Build(); + + await contract1.OnlyExecuteAsync(); + await contract2.OnlyExecuteAsync(); + + Task.Run + ( + async () => + { + await Task.Delay(500); + await contractor.Update(new ContractData(15)); + await Task.Delay(500); + await contractor.Update(new ContractData(25)); + } + ); + + var results = await Task.WhenAll + ( + contract1.WaitForAsync(ContractMultipleStates.ResponseObtained), + contract2.WaitForAsync(ContractMultipleStates.ResponseObtained) + ); + results[0].IsSuccess.ShouldBeTrue(); + results[0].Entity.ShouldBe(15); + results[1].IsSuccess.ShouldBeTrue(); + results[1].Entity.ShouldBe(25); + contract1.CurrentState.ShouldBe(ContractMultipleStates.ResponseObtained); + contract2.CurrentState.ShouldBe(ContractMultipleStates.ResponseObtained); + mock.ExecutedTimes.ShouldBe(2); + } + + /// + /// Tests that the contract response is obtained. + /// + /// A representing the asynchronous operation. + [Fact] + public async Task Test_ErrorsFired() + { + var contractor = new Contractor(); + var mock = new MockClass(); + + var contract = new ContractBuilder + (contractor, ContractMultipleStates.None) + .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested) + .SetMoveFilter> + (ContractMultipleStates.Requested, ContractMultipleStates.ResponseObtained) + .SetFillData>(ContractMultipleStates.ResponseObtained, d => d.Data) + .SetError> + ( + ContractMultipleStates.Requested, + d => + { + if (d.Data == 15) + { + return ContractError.Error1; + } + + return null; + } + ) + .Build(); + + Task.Run + ( + async () => + { + await Task.Delay(500); + await contractor.Update(new ContractData(15)); + } + ); + + var result = await contract.WaitForAsync(ContractMultipleStates.AfterResponseObtained); + result.IsSuccess.ShouldBeFalse(); + result.Error.ShouldBeOfType>(); + ((ContractError)result.Error).Error.ShouldBe(ContractError.Error1); + contract.CurrentState.ShouldBe(ContractMultipleStates.Requested); + contract.IsRegistered.ShouldBeFalse(); + mock.ExecutedTimes.ShouldBe(1); + } +} + +/// +/// A class for verifying setup was called. +/// +public class MockClass +{ + /// + /// Gets the number of times was called. + /// + public int ExecutedTimes { get; private set; } + + /// + /// Gets whether was executed. + /// + public bool Executed { get; private set; } + + /// + /// Sets Executed to true.. + /// + /// The data. should be null. + /// The cancellation token used for cancelling the operation. + /// A representing the asynchronous operation. + public Task> Setup(object? data, CancellationToken ct) + { + if (data is not null) + { + throw new ArgumentException("Should be null.", nameof(data)); + } + + Executed = true; + ExecutedTimes++; + return Task.FromResult(Result.FromSuccess(true)); + } +} \ No newline at end of file diff --git a/Tests/NosSmooth.Core.Tests/NosSmooth.Core.Tests.csproj b/Tests/NosSmooth.Core.Tests/NosSmooth.Core.Tests.csproj index f02be11..3cf80f0 100644 --- a/Tests/NosSmooth.Core.Tests/NosSmooth.Core.Tests.csproj +++ b/Tests/NosSmooth.Core.Tests/NosSmooth.Core.Tests.csproj @@ -10,6 +10,8 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive -- 2.48.1