~ruther/NosSmooth

7a30c95d162ec3a3ce1691f7b72b20e515a7c785 — Rutherther 2 years ago 972cf47
tests(core): add tests for contracts
A Tests/NosSmooth.Core.Tests/Contracts/ContractData.cs => Tests/NosSmooth.Core.Tests/Contracts/ContractData.cs +28 -0
@@ 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;

/// <summary>
/// Data for updating a contract.
/// </summary>
/// <typeparam name="T">The type of the data.</typeparam>
public class ContractData<T>
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ContractData{T}"/> class.
    /// </summary>
    /// <param name="data">The data to pass.</param>
    public ContractData(T data)
    {
        Data = data;
    }

    /// <summary>
    /// Gets the data.
    /// </summary>
    public T Data { get; }
}
\ No newline at end of file

A Tests/NosSmooth.Core.Tests/Contracts/ContractError.cs => Tests/NosSmooth.Core.Tests/Contracts/ContractError.cs +18 -0
@@ 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;

/// <summary>
/// Errors for a contract.
/// </summary>
public enum ContractError
{
    /// <summary>
    /// An error.
    /// </summary>
    Error1
}
\ No newline at end of file

A Tests/NosSmooth.Core.Tests/Contracts/ContractMultipleStates.cs => Tests/NosSmooth.Core.Tests/Contracts/ContractMultipleStates.cs +33 -0
@@ 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;

/// <summary>
/// Extends DefaultStates to have more states to test.
/// </summary>
public enum ContractMultipleStates
{
    /// <summary>
    /// Initial state.
    /// </summary>
    None,

    /// <summary>
    /// Contract executed, request issued.
    /// </summary>
    Requested,

    /// <summary>
    /// A response was obtained.
    /// </summary>
    ResponseObtained,

    /// <summary>
    /// Something else happening after obtaining first response.
    /// </summary>
    AfterResponseObtained,
}
\ No newline at end of file

A Tests/NosSmooth.Core.Tests/Contracts/ContractTests.cs => Tests/NosSmooth.Core.Tests/Contracts/ContractTests.cs +360 -0
@@ 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

M Tests/NosSmooth.Core.Tests/NosSmooth.Core.Tests.csproj => Tests/NosSmooth.Core.Tests/NosSmooth.Core.Tests.csproj +2 -0
@@ 10,6 10,8 @@
    <ItemGroup>
        <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
        <PackageReference Include="Moq" Version="4.18.4" />
        <PackageReference Include="Shouldly" Version="4.1.0" />
        <PackageReference Include="xunit" Version="2.4.1" />
        <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

Do not follow this link