Skip to content

Commit

Permalink
Extend HostBuilderExtensions.RunCommandLineApplicationAsync to suppor…
Browse files Browse the repository at this point in the history
…t injection of IConsole and IConvention. (#178)
  • Loading branch information
TheConstructor authored and natemcmaster committed Nov 26, 2018
1 parent d72af41 commit a0e8d52
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 6 deletions.
7 changes: 7 additions & 0 deletions CommandLineUtils.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "files", "files", "{509D6286
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "McMaster.Extensions.Hosting.CommandLine", "src\Hosting.CommandLine\McMaster.Extensions.Hosting.CommandLine.csproj", "{407245F7-3F2C-4634-8578-7EFCA9BD26BD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hosting.CommandLine.Tests", "test\Hosting.CommandLine.Tests\McMaster.Extensions.Hosting.CommandLine.Tests.csproj", "{04A5D2B8-18E4-4C75-AEF9-79D171FAC210}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -50,6 +52,10 @@ Global
{407245F7-3F2C-4634-8578-7EFCA9BD26BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{407245F7-3F2C-4634-8578-7EFCA9BD26BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{407245F7-3F2C-4634-8578-7EFCA9BD26BD}.Release|Any CPU.Build.0 = Release|Any CPU
{04A5D2B8-18E4-4C75-AEF9-79D171FAC210}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04A5D2B8-18E4-4C75-AEF9-79D171FAC210}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04A5D2B8-18E4-4C75-AEF9-79D171FAC210}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04A5D2B8-18E4-4C75-AEF9-79D171FAC210}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -58,6 +64,7 @@ Global
{CBCFAFF3-A3B1-4C41-B2D1-092BF7307A4E} = {95D4B35E-0A21-4D64-8BAF-27DD6C019FC5}
{1258544C-1FDE-4810-9A1B-189A925E9B45} = {C4842A1B-019E-40FF-A396-CF5AFDE8FA54}
{407245F7-3F2C-4634-8578-7EFCA9BD26BD} = {95D4B35E-0A21-4D64-8BAF-27DD6C019FC5}
{04A5D2B8-18E4-4C75-AEF9-79D171FAC210} = {C4842A1B-019E-40FF-A396-CF5AFDE8FA54}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {55FD25E0-565D-49F9-9370-28DA7196E539}
Expand Down
5 changes: 5 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,9 @@ exec dotnet test --no-restore --no-build --configuration $Configuration '-clp:Su
@testArgs `
@MSBuildArgs

exec dotnet test --no-restore --no-build --configuration $Configuration '-clp:Summary' `
"$PSScriptRoot/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj" `
@testArgs `
@MSBuildArgs

write-host -f magenta 'Done'
17 changes: 13 additions & 4 deletions src/Hosting.CommandLine/HostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using McMaster.Extensions.CommandLineUtils.Abstractions;
using McMaster.Extensions.Hosting.CommandLine.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.Hosting
{
Expand All @@ -33,12 +34,20 @@ public static async Task<int> RunCommandLineApplicationAsync<TApp>(
var state = new CommandLineState(args);
hostBuilder.ConfigureServices(
(context, services)
=> services
=>
{
services
.AddSingleton<IHostLifetime, CommandLineLifetime>()
.AddSingleton(state)
.TryAddSingleton(PhysicalConsole.Singleton);
services
.AddSingleton(provider =>
{
state.SetConsole(provider.GetService<IConsole>());
return state;
})
.AddSingleton<CommandLineContext>(state)
.AddSingleton(PhysicalConsole.Singleton)
.AddSingleton<ICommandLineService, CommandLineService<TApp>>());
.AddSingleton<ICommandLineService, CommandLineService<TApp>>();
});

using (var host = hostBuilder.Build())
{
Expand Down
9 changes: 8 additions & 1 deletion src/Hosting.CommandLine/Internal/CommandLineService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Threading;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using McMaster.Extensions.CommandLineUtils.Conventions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace McMaster.Extensions.Hosting.CommandLine.Internal
Expand All @@ -30,10 +32,15 @@ public CommandLineService(ILogger<CommandLineService<T>> logger, CommandLineStat

logger.LogDebug("Constructing CommandLineApplication<{type}> with args [{args}]",
typeof(T).FullName, string.Join(",", state.Arguments));
_application = new CommandLineApplication<T>();
_application = new CommandLineApplication<T>(state.Console, state.WorkingDirectory, true);
_application.Conventions
.UseDefaultConventions()
.UseConstructorInjection(serviceProvider);

foreach (var convention in serviceProvider.GetServices<IConvention>())
{
_application.Conventions.AddConvention(convention);
}
}

/// <inheritdoc />
Expand Down
6 changes: 5 additions & 1 deletion src/Hosting.CommandLine/Internal/CommandLineState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ internal class CommandLineState : CommandLineContext
public CommandLineState(string[] args)
{
Arguments = args;
Console = PhysicalConsole.Singleton;
}

internal void SetConsole(IConsole console)
{
Console = console;
}

public int ExitCode { get; set; }
Expand Down
106 changes: 106 additions & 0 deletions test/Hosting.CommandLine.Tests/HostBuilderExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System.IO;
using McMaster.Extensions.CommandLineUtils;
using McMaster.Extensions.CommandLineUtils.Conventions;
using McMaster.Extensions.Hosting.CommandLine.Tests.Utilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Moq;
using Xunit;
using Xunit.Abstractions;

namespace McMaster.Extensions.Hosting.CommandLine.Tests
{
public class HostBuilderExtensionsTests
{
private readonly ITestOutputHelper _output;

public HostBuilderExtensionsTests(ITestOutputHelper output)
{
_output = output;
}

[Fact]
public void TestReturnCode()
{
Assert.Equal(42,
new HostBuilder()
.ConfigureServices(collection => collection.AddSingleton<IConsole>(new TestConsole(_output)))
.RunCommandLineApplicationAsync<Return42Command>(new string[0])
.GetAwaiter()
.GetResult());
}

[Fact]
public async void TestConsoleInjection()
{
var console = new Mock<IConsole>();
var textWriter = new Mock<TextWriter>();
textWriter.Setup(writer => writer.WriteLine("42")).Verifiable();
console.SetupGet(c => c.Out).Returns(textWriter.Object);
await new HostBuilder()
.ConfigureServices(collection => collection.AddSingleton<IConsole>(console.Object))
.RunCommandLineApplicationAsync<Write42Command>(new string[0]);
Mock.Verify(console, textWriter);
}

[Fact]
public async void TestConventionInjection()
{
var valueHolder = new ValueHolder<string[]>();
var convention = new Mock<IConvention>();
convention.Setup(c => c.Apply(It.IsAny<ConventionContext>()))
.Callback((ConventionContext c) => c.Application.ThrowOnUnexpectedArgument = false).Verifiable();
var args = new[] {"Capture", "some", "test", "arguments"};
await new HostBuilder()
.ConfigureServices(collection => collection
.AddSingleton<IConsole>(new TestConsole(_output))
.AddSingleton(valueHolder)
.AddSingleton(convention.Object))
.RunCommandLineApplicationAsync<CaptureRemainingArgsCommand>(args);
Assert.Equal(args, valueHolder.Value);
Mock.Verify(convention);
}

public class Return42Command
{
private int OnExecute()
{
return 42;
}
}

public class Write42Command
{
private void OnExecute(CommandLineApplication<Write42Command> app)
{
app.Out.WriteLine("42");
}
}

public class ValueHolder<T>
{
public T Value { get; set; }
}

[Command("Capture")]
public class CaptureRemainingArgsCommand
{
public ValueHolder<string[]> ValueHolder { get; set; }

public string[] RemainingArguments
{
get => ValueHolder.Value;
set => ValueHolder.Value = value;
}

public CaptureRemainingArgsCommand(ValueHolder<string[]> valueHolder)
{
ValueHolder = valueHolder;
}

private void OnExecute(CommandLineApplication<CaptureRemainingArgsCommand> app)
{
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Hosting.CommandLine\McMaster.Extensions.Hosting.CommandLine.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net461'">
<Reference Include="System.ComponentModel.DataAnnotations" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="Moq" Version="4.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>
<ItemGroup>
<None Update="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
44 changes: 44 additions & 0 deletions test/Hosting.CommandLine.Tests/Utilities/TestConsole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.IO;
using McMaster.Extensions.CommandLineUtils;
using Xunit.Abstractions;

namespace McMaster.Extensions.Hosting.CommandLine.Tests.Utilities
{
public class TestConsole : IConsole
{
public TestConsole(ITestOutputHelper output)
{
Out = new XunitTextWriter(output);
Error = new XunitTextWriter(output);
}

public TextWriter Out { get; set; }

public TextWriter Error { get; set; }

public TextReader In => throw new NotImplementedException();

public bool IsInputRedirected => throw new NotImplementedException();

public bool IsOutputRedirected => true;

public bool IsErrorRedirected => true;

public ConsoleColor ForegroundColor { get; set; }
public ConsoleColor BackgroundColor { get; set; }

public event ConsoleCancelEventHandler CancelKeyPress
{
add { }
remove { }
}

public void ResetColor()
{
}
}
}
49 changes: 49 additions & 0 deletions test/Hosting.CommandLine.Tests/Utilities/XunitTextWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.IO;
using System.Text;
using Xunit.Abstractions;

namespace McMaster.Extensions.Hosting.CommandLine.Tests.Utilities
{
public class XunitTextWriter : TextWriter
{
private readonly ITestOutputHelper _output;
private readonly StringBuilder _sb = new StringBuilder();

public XunitTextWriter(ITestOutputHelper output)
{
_output = output;
}

public override Encoding Encoding => Encoding.Unicode;

public override void Write(char ch)
{
if (ch == '\n')
{
_output.WriteLine(_sb.ToString());
_sb.Clear();
}
else
{
_sb.Append(ch);
}
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_sb.Length > 0)
{
_output.WriteLine(_sb.ToString());
_sb.Clear();
}
}

base.Dispose(disposing);
}
}
}
3 changes: 3 additions & 0 deletions test/Hosting.CommandLine.Tests/xunit.runner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"methodDisplay": "method"
}

0 comments on commit a0e8d52

Please sign in to comment.