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
68 changes: 43 additions & 25 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,50 @@ on:
pull_request:
branches: [ "main" ]

jobs:
build:
jobs:
build-and-analyze:
name: 'Build & Analyze'
permissions: write-all
permissions: write-all
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore -warnaserror
- name: Test
run: dotnet test --no-build --verbosity normal
- name: JetBrains ReSharper Inspect Code
uses: JetBrains/ReSharper-InspectCode@v0.8
with:
solution: 'Oma.WndwCtrl.sln'
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4.5.0
with:
name: 'Upload Code Inspection Result'
path: 'results.sarif.json'
if-no-files-found: error
retention-days: 7
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore -warnaserror
- name: JetBrains ReSharper Inspect Code
uses: JetBrains/ReSharper-InspectCode@v0.8
with:
solution: 'Oma.WndwCtrl.sln'
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4.5.0
with:
name: 'Upload Code Inspection Result'
path: 'results.sarif.json'
if-no-files-found: error
retention-days: 7

test-matrix:
name: 'Run Test Matrix'
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore -warnaserror
- name: Test
run: dotnet test --no-build --verbosity normal

Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
using FluentAssertions;
using FluentAssertions.Execution;
using LanguageExt;
using Oma.WndwCtrl.Abstractions.Errors;
using Oma.WndwCtrl.Abstractions.Extensions;
using Oma.WndwCtrl.Abstractions.Model;
using Oma.WndwCtrl.Core.Executors.Commands;
using Oma.WndwCtrl.Core.Mocks;
using Oma.WndwCtrl.Core.Model.Commands;

namespace Oma.WndwCtrl.Core.IntegrationTests.Executors.Commands;

public sealed class CliCommandExecutorTests : IDisposable
{
private const string DEFAULT_OUTPUT_TEXT = "Hello World!";

private readonly CliCommandExecutor _instance = new();
private readonly CancellationToken _xunitCancelToken = TestContext.Current.CancellationToken;

private Either<FlowError, CommandOutcome>? _result;

public void Dispose()
{
_result?.Dispose();
}

[Fact]
public async Task SanityCheck()
{
CliCommand command = CreateCommand();

Func<Task<Either<FlowError, CommandOutcome>>> act = async () =>
await _instance.ExecuteAsync(command, _xunitCancelToken);

await act.Should().NotThrowAsync();
}

[Fact]
public async Task ShouldSetSuccessTrueIfExitCodeIsZero()
{
CliCommand command = CreateCommand();

_result = await _instance.ExecuteAsync(command, _xunitCancelToken);

SatisfiesRight(
outcome => outcome.Success.Should().BeTrue()
);
}

[Fact]
public async Task ShouldSetSuccessFalseIfExitCodeIsNonZero()
{
CliCommand command = CreateCommand(exitCode: 1);

_result = await _instance.ExecuteAsync(command, _xunitCancelToken);

SatisfiesRight(
outcome => outcome.Success.Should().BeFalse()
);
}

[Fact]
public async Task ShouldSetSuccessFalseIfStdErrIsWritten()
{
CliCommand command = CreateCommand(target: CliInvocationOptions.OutputTarget.StdErr);

_result = await _instance.ExecuteAsync(command, _xunitCancelToken);

SatisfiesRight(
outcome => outcome.Success.Should().BeFalse()
);
}

[Fact]
public async Task ShouldReturnStdErrIfWrittenTo()
{
CliCommand command = CreateCommand(target: CliInvocationOptions.OutputTarget.StdErr);

_result = await _instance.ExecuteAsync(command, _xunitCancelToken);

SatisfiesRight(
outcome => outcome.OutcomeRaw.Should().Be(DEFAULT_OUTPUT_TEXT)
);
}

[Fact]
public async Task ShouldHandleWritesToBothStreams()
{
CliCommand command = CreateCommand(
target: CliInvocationOptions.OutputTarget.StdOut | CliInvocationOptions.OutputTarget.StdErr
);

_result = await _instance.ExecuteAsync(command, _xunitCancelToken);

SatisfiesRight(
outcome => outcome.Success.Should().BeFalse(),
outcome => outcome.OutcomeRaw.Should().Be(DEFAULT_OUTPUT_TEXT)
);
}

[Fact(Timeout = 2_000)]
public async Task ShouldHandleAlternatingWrites()
{
string text = GetMultiLineText(lineCount: 10);

CliCommand command = CreateCommand(
text,
CliInvocationOptions.OutputTarget.StdOut | CliInvocationOptions.OutputTarget.StdErr
);

_result = await _instance.ExecuteAsync(command, _xunitCancelToken);

SatisfiesRight(
outcome => outcome.Success.Should().BeFalse(),
outcome => outcome.OutcomeRaw.Should().Be(text)
);
}

[Fact(Timeout = 2_000)]
public async Task ShouldNotTimeout()
{
string text = GetMultiLineText(lineCount: 1_000);

CliCommand command = CreateCommand(
text,
CliInvocationOptions.OutputTarget.StdOut | CliInvocationOptions.OutputTarget.StdErr
);

_result = await _instance.ExecuteAsync(command, _xunitCancelToken);

SatisfiesRight(
outcome => outcome.Success.Should().BeFalse(),
outcome => outcome.OutcomeRaw.Should().Be(text)
);
}

private void SatisfiesRight(params Action<CommandOutcome>[] assertions)
{
using AssertionScope scope = new();

_result.Should().NotBeNull();

_result?.Match(
Right: val =>
{
foreach (Action<CommandOutcome> assertion in assertions) assertion(val);
},
Left: val => val.Should().BeNull()
);
}

private static string GetMultiLineText(int lineCount = 1_000)
{
string text = string.Join(
Environment.NewLine,
Enumerable.Range(start: 0, lineCount).Select(num => $"This is line {num}.")
);

return text;
}

private static CliCommand CreateCommand(
string text = DEFAULT_OUTPUT_TEXT,
CliInvocationOptions.OutputTarget target = CliInvocationOptions.OutputTarget.StdOut,
int exitCode = 0
)
{
CliCommand command = new()
{
FileName = "./Oma.WndwCtrl.Core.Mocks",
Arguments = new CliInvocationOptions
{
Text = text,
TargetStreams = target,
ExitCode = exitCode,
}.ToString(),
};

return command;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector"/>
<PackageReference Include="FluentAssertions"/>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<PackageReference Include="xunit.v3"/>
<PackageReference Include="xunit.runner.visualstudio"/>
</ItemGroup>

<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\.unitTests\Oma.WndwCtrl.Core.Mocks\Oma.WndwCtrl.Core.Mocks.csproj"/>
<ProjectReference Include="..\..\Oma.WndwCtrl.Core\Oma.WndwCtrl.Core.csproj"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
Expand All @@ -13,9 +14,9 @@ public sealed class MessageBusIntegrationTests : IAsyncLifetime
private readonly CancellationToken _cancelToken = TestContext.Current.CancellationToken;

private readonly List<ServiceProvider> _consumerProviders = [];
private readonly ServiceProvider _serviceProvider = SetUpMessageBusContainer();

private Task? _consumerTask;
private readonly ConcurrentBag<Task> _consumerTasks = [];
private readonly ServiceProvider _serviceProvider = SetUpMessageBusContainer();

private IMessageBus? _messageBus;

Expand Down Expand Up @@ -61,7 +62,7 @@ public async Task ShouldFanOutMessages()

private async Task WaitForConsumerCompletion()
{
await (_consumerTask ?? Task.CompletedTask);
await Task.WhenAll(_consumerTasks);
}

[Fact]
Expand Down Expand Up @@ -96,7 +97,7 @@ public async Task ShouldRouteMessagesByType()
IMessageConsumer otherC = AddConsumerToContainer<OtherMessage>(services);

ServiceProvider provider = services.BuildServiceProvider();
_consumerTask = provider.StartConsumersAsync(MessageBus, _cancelToken);
_consumerTasks.Add(provider.StartConsumersAsync(MessageBus, _cancelToken));

await MessageBus.SendAsync(new DummyMessage(), _cancelToken);
await MessageBus.SendAsync(new DummyMessage(), _cancelToken);
Expand Down Expand Up @@ -271,7 +272,7 @@ private IMessageConsumer SetUpConsumerContainer<TMessage>(
ServiceProvider result = services.BuildServiceProvider();
_consumerProviders.Add(result);

_consumerTask = result.StartConsumersAsync(MessageBus, cancelToken ?? _cancelToken);
_consumerTasks.Add(result.StartConsumersAsync(MessageBus, cancelToken ?? _cancelToken));

return messageConsumer;
}
Expand All @@ -286,6 +287,7 @@ private static IMessageConsumer AddConsumerToContainer<TMessage>(

IMessageConsumer<TMessage> messageConsumer = Substitute.For<IMessageConsumer<TMessage>>();

messageConsumer.IsSubscribedTo(Arg.Any<IMessage>()).Returns(returnThis: false);
messageConsumer.IsSubscribedTo(Arg.Any<TMessage>()).Returns(returnThis: true);

services.AddMessageConsumer<IMessageConsumer<TMessage>, TMessage>(
Expand Down
Loading