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
5 changes: 1 addition & 4 deletions samples/CliHostSampleApp/DemoCliCommand.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
using CreativeCoders.Cli.Core;
using JetBrains.Annotations;

[assembly: CliCommandGroup(["demo"], "Demo commands root group")]
[assembly: CliCommandGroup(["demo", "do"], "Demo do commands")]

namespace CliHostSampleApp;

[UsedImplicitly]
[CliCommand(["demo", "do", "list"]
[CliCommand([DemoCommandGroup.Name, "do"]
, AlternativeCommands = ["demo1"])]
public class DemoCliCommand : ICliCommand
{
Expand Down
4 changes: 2 additions & 2 deletions samples/CliHostSampleApp/DemoCliCommandWithOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace CliHostSampleApp;

[UsedImplicitly]
[CliCommand(["demo", "do", "something"], Name = "Demo2 command",
[CliCommand(["demo", "list"], Name = "Demo2 command",
Description = "Simple Demo command with options, that prints some text from options.")]
public class DemoCliCommandWithOptions : ICliCommand<DemoOptions>
{
Expand All @@ -15,4 +15,4 @@ public Task<CommandResult> ExecuteAsync(DemoOptions options)

return Task.FromResult(new CommandResult());
}
}
}
10 changes: 10 additions & 0 deletions samples/CliHostSampleApp/DemoCommandGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using CreativeCoders.Cli.Core;

[assembly: CliCommandGroup(["demo"], "Demo commands root group")]

namespace CliHostSampleApp;

public static class DemoCommandGroup
{
public const string Name = "demo";
}
2 changes: 1 addition & 1 deletion samples/CliHostSampleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ static async Task Main(string[] args)
private static ICliHost CreateCliHost()
{
return CliHostBuilder.Create()
.EnableHelp(HelpCommandKind.Command)
.EnableHelp(HelpCommandKind.CommandOrArgument)
.Build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,3 @@ public AssemblyScanResult ScanForCommands(Assembly[] assemblies, Func<Type, bool
};
}
}

public class AssemblyScanResult
{
public required IEnumerable<CliCommandInfo> CommandInfos { get; init; }

public required IEnumerable<CliCommandGroupAttribute> GroupAttributes { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using CreativeCoders.Cli.Core;

namespace CreativeCoders.Cli.Hosting.Commands;

public class AssemblyScanResult
{
public required IEnumerable<CliCommandInfo> CommandInfos { get; init; }

public required IEnumerable<CliCommandGroupAttribute> GroupAttributes { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ private void AddCommand(CliCommandInfo command, string[] commands)
{
var groupNode = GetGroupNode(commands.Take(commands.Length - 1).ToArray());

if (groupNode.Parent == null)
if (groupNode.Parent == null && !_treeRootNodes.Contains(groupNode))
{
_treeRootNodes.Add(groupNode);
}
Expand Down
9 changes: 9 additions & 0 deletions source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ public async Task<CliResult> RunAsync(string[] args)

e.ValidationResult.Messages.ForEach(message => _ansiConsole.MarkupLine($"- {message}"));

return new CliResult(e.ExitCode);
}
catch (CliCommandAbortException e)
{
if (e.PrintMessage)
{
_ansiConsole.MarkupLine(e.IsError ? $"[red]{e.Message}[/]" : $"[yellow]{e.Message}[/]");
}

return new CliResult(e.ExitCode);
}
Comment thread
darthsharp marked this conversation as resolved.
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace CreativeCoders.Cli.Hosting.Exceptions;

public class CliCommandAbortException(string message, int exitCode, Exception? exception = null)
Comment thread
darthsharp marked this conversation as resolved.
: CliExitException(message, exitCode, exception)
{
public bool IsError { get; set; } = true;

public bool PrintMessage { get; set; } = true;
}
Comment thread
darthsharp marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,51 @@ public void AddCommands_WithAlternativeCommands_AddsAliases()
.BeSameAs(cmdInfo);
}

[Fact]
public void AddCommands_WithGroupAttribute_NoDuplicatedGroups()
{
// Arrange
var commandInfo = new CliCommandInfo
{
CommandAttribute = new CliCommandAttribute(["tools", "admin"]),
CommandType = typeof(DummyCommand)
};

var firstGroupAttribute = new CliCommandGroupAttribute(["tools"], "Tools root group");

var groupAttributes = new[]
{
firstGroupAttribute
};

var store = new CliCommandStore();

// Act
store.AddCommands([commandInfo], groupAttributes);

// Assert
var toolsGroupNode = store.TreeRootNodes
.Should()
.ContainSingle(node => node.Name == "tools")
.Which
.Should()
.BeOfType<CliCommandGroupNode>()
.Which;

toolsGroupNode.GroupAttribute
.Should()
.BeSameAs(firstGroupAttribute);

toolsGroupNode.ChildNodes
.OfType<CliCommandNode>()
.Should()
.ContainSingle(node => node.Name == "admin");

store.GroupAttributes
.Should()
.BeEquivalentTo(groupAttributes);
}

[Fact]
public void AddCommands_WithGroupAttributes_AssignsAttributesToGroupNodes()
{
Expand Down
112 changes: 112 additions & 0 deletions tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using FakeItEasy;
using JetBrains.Annotations;
using Spectre.Console;
using Spectre.Console.Rendering;
using Xunit;

namespace CreativeCoders.Cli.Tests.Hosting;
Expand Down Expand Up @@ -123,6 +124,75 @@ public async Task RunAsync_WhenCommandWithoutOptions_ExecutesAndReturnsResult()
.Be(5);
}

[Theory]
[InlineData(typeof(DummyCommandWithErrorAbortException), DummyCommandWithErrorAbortException.ExitCode,
true, DummyCommandWithErrorAbortException.Message, 0)]
[InlineData(typeof(DummyCommandWithNoneErrorAbortException),
DummyCommandWithNoneErrorAbortException.ExitCode,
true, DummyCommandWithNoneErrorAbortException.Message, 1)]
[InlineData(typeof(DummyCommandWithoutPrintErrorAbortException),
DummyCommandWithoutPrintErrorAbortException.ExitCode,
false, DummyCommandWithoutPrintErrorAbortException.Message, 1)]
public async Task RunAsync_WhenCommandWithAbortException_ExecutesAndReturnsExceptionResult(
Type commandType, int exitCode, bool shouldPrintMessage, string message, int expectedColor)
{
// Arrange
Color[] colors = [Color.Red, Color.Yellow];
var args = new[] { "run" };

var ansiConsole = A.Fake<IAnsiConsole>();
var commandStore = A.Fake<ICliCommandStore>();
var serviceProvider = A.Fake<IServiceProvider>();
var helpHandler = A.Fake<ICliCommandHelpHandler>();

SetupServiceProvider(serviceProvider, new CliCommandContext());

A.CallTo(() => helpHandler.ShouldPrintHelp(args))
.Returns(false);

var commandInfo = new CliCommandInfo
{
CommandAttribute = new CliCommandAttribute(["run"]),
CommandType = commandType
};

var commandNode = new CliCommandNode(commandInfo, "run", null);

A.CallTo(() => commandStore.FindCommandNode(args))
.Returns(new FindCommandNodeResult<CliCommandNode>(commandNode, []));

var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler);

// Act
var result = await host.RunAsync(args);

// Assert
result.ExitCode
.Should()
.Be(exitCode);

if (shouldPrintMessage)
{
A.CallTo(() => ansiConsole.Write(A<IRenderable>.That.Matches(r =>
CheckConsoleOutput(r, message, colors[expectedColor]))))
.MustHaveHappenedOnceExactly();
}
else
{
A.CallTo(() => ansiConsole.Write(A<IRenderable>.Ignored))
.MustNotHaveHappened();
}
}

private static bool CheckConsoleOutput(IRenderable renderable, string message, Color color)
{
var text = string.Join("", renderable.GetSegments(AnsiConsole.Console).Select(s => s.Text));
var style = renderable.GetSegments(AnsiConsole.Console).First().Style;

return text.Trim() == message &&
style.Foreground == color;
}

[Fact]
public async Task RunMainAsync_WhenCommandWithoutOptions_ExecutesAndReturnsIntResult()
{
Expand Down Expand Up @@ -514,4 +584,46 @@ public Task<CommandResult> ExecuteAsync()
return Task.FromResult(new CommandResult());
}
}

private sealed class DummyCommandWithErrorAbortException : ICliCommand
{
public const int ExitCode = 12349876;

public const string Message = "Command aborted with error";

public Task<CommandResult> ExecuteAsync()
{
throw new CliCommandAbortException(Message, ExitCode);
}
}

private sealed class DummyCommandWithNoneErrorAbortException : ICliCommand
{
public const int ExitCode = 12349876;

public const string Message = "Command aborted with warning";

public Task<CommandResult> ExecuteAsync()
{
throw new CliCommandAbortException(Message, ExitCode)
{
IsError = false
};
}
}

private sealed class DummyCommandWithoutPrintErrorAbortException : ICliCommand
{
public const int ExitCode = 12349876;

public const string Message = "Command aborted with printing";

public Task<CommandResult> ExecuteAsync()
{
throw new CliCommandAbortException(Message, ExitCode)
{
PrintMessage = false
};
}
}
}