@@ 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;
+
+/// <summary>
+/// Tests basics of contract system.
+/// </summary>
+public class ContractTests
+{
+ /// <summary>
+ /// Tests that the contract is executed.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_GetsExecuted()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, DefaultStates, NoErrors>(contractor, DefaultStates.None)
+ .SetMoveAction(DefaultStates.None, mock.Setup, DefaultStates.Requested)
+ .SetMoveFilter<ContractData<long>>(DefaultStates.Requested, DefaultStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(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);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_ResponseObtained()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, DefaultStates, NoErrors>(contractor, DefaultStates.None)
+ .SetMoveAction(DefaultStates.None, mock.Setup, DefaultStates.Requested)
+ .SetMoveFilter<ContractData<long>>(DefaultStates.Requested, DefaultStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(DefaultStates.ResponseObtained, d => d.Data)
+ .Build();
+
+ await contract.OnlyExecuteAsync();
+ contract.Register();
+ await contractor.Update(new ContractData<long>(5));
+ contract.CurrentState.ShouldBe(DefaultStates.ResponseObtained);
+ contract.Data.ShouldBe(5);
+
+ await contractor.Update(new ContractData<long>(10));
+ contract.Data.ShouldBe(5);
+ contract.Unregister();
+
+ mock.ExecutedTimes.ShouldBe(1);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_WaitFor()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, DefaultStates, NoErrors>(contractor, DefaultStates.None)
+ .SetMoveAction(DefaultStates.None, mock.Setup, DefaultStates.Requested)
+ .SetMoveFilter<ContractData<long>>(DefaultStates.Requested, DefaultStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(DefaultStates.ResponseObtained, d => d.Data)
+ .Build();
+
+ Task.Run
+ (
+ async () =>
+ {
+ await Task.Delay(500);
+ await contractor.Update(new ContractData<long>(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);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_WaitForMoreStates()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, ContractMultipleStates, NoErrors>
+ (contractor, ContractMultipleStates.None)
+ .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested)
+ .SetMoveFilter<ContractData<long>>
+ (ContractMultipleStates.Requested, ContractMultipleStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(ContractMultipleStates.ResponseObtained, d => d.Data)
+ .SetMoveFilter<ContractData<bool>>
+ (ContractMultipleStates.ResponseObtained, c => c.Data, ContractMultipleStates.AfterResponseObtained)
+ .Build();
+
+ Task.Run
+ (
+ async () =>
+ {
+ await Task.Delay(500);
+ await contractor.Update(new ContractData<long>(15));
+ await Task.Delay(200);
+ await contractor.Update(new ContractData<bool>(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);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_MoreStatesFollowed()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, ContractMultipleStates, NoErrors>
+ (contractor, ContractMultipleStates.None)
+ .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested)
+ .SetMoveFilter<ContractData<long>>
+ (ContractMultipleStates.Requested, ContractMultipleStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(ContractMultipleStates.ResponseObtained, d => d.Data)
+ .SetMoveFilter<ContractData<bool>>
+ (ContractMultipleStates.ResponseObtained, c => c.Data, ContractMultipleStates.AfterResponseObtained)
+ .Build();
+
+ await contract.OnlyExecuteAsync();
+ contract.Register();
+ await contractor.Update(new ContractData<long>(15));
+ await contractor.Update(new ContractData<bool>(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);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_WaitForTimeout()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, ContractMultipleStates, NoErrors>
+ (contractor, ContractMultipleStates.None)
+ .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested)
+ .SetMoveFilter<ContractData<long>>
+ (ContractMultipleStates.Requested, ContractMultipleStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(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<long>(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);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_MultipleContracts()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract1 = new ContractBuilder<long, ContractMultipleStates, NoErrors>
+ (contractor, ContractMultipleStates.None)
+ .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested)
+ .SetMoveFilter<ContractData<long>>
+ (
+ ContractMultipleStates.Requested,
+ d => d.Data > 10 && d.Data < 20,
+ ContractMultipleStates.ResponseObtained
+ )
+ .SetFillData<ContractData<long>>(ContractMultipleStates.ResponseObtained, d => d.Data)
+ .Build();
+
+ var contract2 = new ContractBuilder<long, ContractMultipleStates, NoErrors>
+ (contractor, ContractMultipleStates.None)
+ .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested)
+ .SetMoveFilter<ContractData<long>>
+ (ContractMultipleStates.Requested, d => d.Data > 20, ContractMultipleStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(ContractMultipleStates.ResponseObtained, d => d.Data)
+ .Build();
+
+ await contract1.OnlyExecuteAsync();
+ await contract2.OnlyExecuteAsync();
+
+ Task.Run
+ (
+ async () =>
+ {
+ await Task.Delay(500);
+ await contractor.Update(new ContractData<long>(15));
+ await Task.Delay(500);
+ await contractor.Update(new ContractData<long>(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);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_ErrorsFired()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, ContractMultipleStates, ContractError>
+ (contractor, ContractMultipleStates.None)
+ .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested)
+ .SetMoveFilter<ContractData<long>>
+ (ContractMultipleStates.Requested, ContractMultipleStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(ContractMultipleStates.ResponseObtained, d => d.Data)
+ .SetError<ContractData<long>>
+ (
+ 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<long>(15));
+ }
+ );
+
+ var result = await contract.WaitForAsync(ContractMultipleStates.AfterResponseObtained);
+ result.IsSuccess.ShouldBeFalse();
+ result.Error.ShouldBeOfType<ContractError<ContractError>>();
+ ((ContractError<ContractError>)result.Error).Error.ShouldBe(ContractError.Error1);
+ contract.CurrentState.ShouldBe(ContractMultipleStates.Requested);
+ contract.IsRegistered.ShouldBeFalse();
+ mock.ExecutedTimes.ShouldBe(1);
+ }
+}
+
+/// <summary>
+/// A class for verifying setup was called.
+/// </summary>
+public class MockClass
+{
+ /// <summary>
+ /// Gets the number of times <see cref="Setup"/> was called.
+ /// </summary>
+ public int ExecutedTimes { get; private set; }
+
+ /// <summary>
+ /// Gets whether <see cref="Executed"/> was executed.
+ /// </summary>
+ public bool Executed { get; private set; }
+
+ /// <summary>
+ /// Sets Executed to true..
+ /// </summary>
+ /// <param name="data">The data. should be null.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ public Task<Result<bool>> 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<bool>.FromSuccess(true));
+ }
+}<
\ No newline at end of file