Create console reporter API

The reporter API breaks down complex formatter in to composable compontents. Adds 'ReporterBuilder' and 'FormatterBuilder' as syntax sugar for creating reporters with complex formatting rules

Changes in dotnet-watch
 - Remove dependency on Microsoft.Extensions.Logging and instead use IReporter for console output
 - Only use color output when stdout and stderr are not being redirected
 - Make the default output less noisy

Changes in dotnet-user-secrets
 - Remove dependency on Microsoft.Extensions.Logging to use IReporter

Changes in dotnet-sql-cache
 - Remove dependency on Microsoft.Extensions.Logging to use IReporter
 - Add --verbose option
This commit is contained in:
Nate McMaster 2016-11-23 12:15:23 -08:00
parent e0f85971a3
commit 17da5242e0
No known key found for this signature in database
GPG Key ID: BD729980AA6A21BD
53 changed files with 978 additions and 694 deletions

View File

@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
@ -28,6 +28,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{59E02BDF
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "NuGetPackager", "tools\NuGetPackager\NuGetPackager.xproj", "{8B781D87-1FC3-4A34-9089-2BDF6B562B85}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Tools.Tests", "test\Microsoft.Extensions.Tools.Tests\Microsoft.Extensions.Tools.Tests.xproj", "{A24BF1D1-4326-4455-A528-09F1E20EDC83}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -62,6 +64,10 @@ Global
{8B781D87-1FC3-4A34-9089-2BDF6B562B85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B781D87-1FC3-4A34-9089-2BDF6B562B85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B781D87-1FC3-4A34-9089-2BDF6B562B85}.Release|Any CPU.Build.0 = Release|Any CPU
{A24BF1D1-4326-4455-A528-09F1E20EDC83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A24BF1D1-4326-4455-A528-09F1E20EDC83}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A24BF1D1-4326-4455-A528-09F1E20EDC83}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A24BF1D1-4326-4455-A528-09F1E20EDC83}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -74,5 +80,6 @@ Global
{8A2E6961-6B12-4A8E-8215-3E7301D52EAC} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9} = {66517987-2A5A-4330-B130-207039378FD4}
{8B781D87-1FC3-4A34-9089-2BDF6B562B85} = {59E02BDF-98DE-4D64-B576-2D0299D5E052}
{A24BF1D1-4326-4455-A528-09F1E20EDC83} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
EndGlobalSection
EndGlobal

View File

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.DotNet.Watcher.Tools;
@ -17,9 +16,11 @@ namespace Microsoft.DotNet.Watcher
public bool IsQuiet { get; private set; }
public bool IsVerbose { get; private set; }
public IList<string> RemainingArguments { get; private set; }
public static CommandLineOptions Parse(string[] args, IConsole console)
{
Ensure.NotNull(args, nameof(args));
Ensure.NotNull(console, nameof(console));
var app = new CommandLineApplication(throwOnUnexpectedArg: false)
{
@ -60,20 +61,10 @@ Examples:
CommandOptionType.SingleValue); // TODO multiple shouldn't be too hard to support
var optQuiet = app.Option("-q|--quiet", "Suppresses all output except warnings and errors",
CommandOptionType.NoValue);
var optVerbose = app.Option("-v|--verbose", "Show verbose output",
CommandOptionType.NoValue);
var optVerbose = app.VerboseOption();
app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
app.OnExecute(() =>
{
if (app.RemainingArguments.Count == 0)
{
app.ShowHelp();
}
return 0;
});
if (app.Execute(args) != 0)
{
return null;
@ -81,8 +72,12 @@ Examples:
if (optQuiet.HasValue() && optVerbose.HasValue())
{
console.Error.WriteLine(Resources.Error_QuietAndVerboseSpecified.Bold().Red());
return null;
throw new CommandParsingException(app, Resources.Error_QuietAndVerboseSpecified);
}
if (app.RemainingArguments.Count == 0)
{
app.ShowHelp();
}
return new CommandLineOptions
@ -95,4 +90,4 @@ Examples:
};
}
}
}
}

View File

@ -1,64 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watcher
{
/// <summary>
/// Logger to print formatted command output.
/// </summary>
public class CommandOutputLogger : ILogger
{
private readonly CommandOutputProvider _provider;
private readonly AnsiConsole _outConsole;
private readonly string _loggerName;
public CommandOutputLogger(CommandOutputProvider commandOutputProvider, string loggerName, bool useConsoleColor)
{
_provider = commandOutputProvider;
_outConsole = AnsiConsole.GetOutput(useConsoleColor);
_loggerName = loggerName;
}
public IDisposable BeginScope<TState>(TState state)
{
throw new NotImplementedException();
}
public bool IsEnabled(LogLevel logLevel)
{
if (logLevel < _provider.LogLevel)
{
return false;
}
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (IsEnabled(logLevel))
{
_outConsole.WriteLine($"[{_loggerName}] {Caption(logLevel)}: {formatter(state, exception)}");
}
}
private string Caption(LogLevel logLevel)
{
switch (logLevel)
{
case LogLevel.Trace: return "\x1b[35mtrce\x1b[39m";
case LogLevel.Debug: return "\x1b[35mdbug\x1b[39m";
case LogLevel.Information: return "\x1b[32minfo\x1b[39m";
case LogLevel.Warning: return "\x1b[33mwarn\x1b[39m";
case LogLevel.Error: return "\x1b[31mfail\x1b[39m";
case LogLevel.Critical: return "\x1b[31mcrit\x1b[39m";
}
throw new Exception("Unknown LogLevel");
}
}
}

View File

@ -1,29 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watcher
{
public class CommandOutputProvider : ILoggerProvider
{
private readonly bool _isWindows;
public CommandOutputProvider()
{
_isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
}
public ILogger CreateLogger(string name)
{
return new CommandOutputLogger(this, name, useConsoleColor: _isWindows);
}
public void Dispose()
{
}
public LogLevel LogLevel { get; set; } = LogLevel.Information;
}
}

View File

@ -1,33 +1,35 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher
{
public class DotNetWatcher
{
private readonly ILogger _logger;
private readonly IReporter _reporter;
private readonly ProcessRunner _processRunner;
public DotNetWatcher(ILogger logger)
public DotNetWatcher(IReporter reporter)
{
Ensure.NotNull(logger, nameof(logger));
Ensure.NotNull(reporter, nameof(reporter));
_logger = logger;
_processRunner = new ProcessRunner(logger);
_reporter = reporter;
_processRunner = new ProcessRunner(reporter);
}
public async Task WatchAsync(ProcessSpec processSpec, IFileSetFactory fileSetFactory, CancellationToken cancellationToken)
public async Task WatchAsync(ProcessSpec processSpec, IFileSetFactory fileSetFactory,
CancellationToken cancellationToken)
{
Ensure.NotNull(processSpec, nameof(processSpec));
var cancelledTaskSource = new TaskCompletionSource<object>();
cancellationToken.Register(state => ((TaskCompletionSource<object>)state).TrySetResult(null), cancelledTaskSource);
cancellationToken.Register(state => ((TaskCompletionSource<object>) state).TrySetResult(null),
cancelledTaskSource);
while (true)
{
@ -46,9 +48,10 @@ namespace Microsoft.DotNet.Watcher
var fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token);
var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token);
_logger.LogInformation("Running {execName} with the following arguments: {args}",
processSpec.ShortDisplayName(),
ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments));
var args = ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments);
_reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: {args}");
_reporter.Output("Started");
var finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task);
@ -58,6 +61,15 @@ namespace Microsoft.DotNet.Watcher
await Task.WhenAll(processTask, fileSetTask);
if (processTask.Result == 0)
{
_reporter.Output("Exited");
}
else
{
_reporter.Error($"Exited with error code {processTask.Result}");
}
if (finishedTask == cancelledTaskSource.Task || cancellationToken.IsCancellationRequested)
{
return;
@ -65,7 +77,7 @@ namespace Microsoft.DotNet.Watcher
if (finishedTask == processTask)
{
_logger.LogInformation("Waiting for a file to change before restarting dotnet...");
_reporter.Warn("Waiting for a file to change before restarting dotnet...");
// Now wait for a file to change before restarting process
await fileSetWatcher.GetChangedFileAsync(cancellationToken);
@ -73,10 +85,10 @@ namespace Microsoft.DotNet.Watcher
if (!string.IsNullOrEmpty(fileSetTask.Result))
{
_logger.LogInformation($"File changed: {fileSetTask.Result}");
_reporter.Output($"File changed: {fileSetTask.Result}");
}
}
}
}
}
}
}

View File

@ -18,6 +18,8 @@ namespace Microsoft.DotNet.Watcher.Internal
public bool Contains(string filePath) => _files.Contains(filePath);
public int Count => _files.Count;
public IEnumerator<string> GetEnumerator() => _files.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _files.GetEnumerator();
}

View File

@ -7,48 +7,47 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher.Internal
{
public class MsBuildFileSetFactory : IFileSetFactory
{
private const string TargetName = "GenerateWatchList";
private const string ProjectExtensionFileExtension = ".dotnetwatch.targets";
private const string WatchTargetsFileName = "DotNetWatchCommon.targets";
private readonly ILogger _logger;
private readonly IReporter _reporter;
private readonly string _projectFile;
private readonly string _watchTargetsDir;
private readonly OutputSink _outputSink;
private readonly ProcessRunner _processRunner;
public MsBuildFileSetFactory(ILogger logger, string projectFile)
: this(logger, projectFile, new OutputSink())
public MsBuildFileSetFactory(IReporter reporter, string projectFile)
: this(reporter, projectFile, new OutputSink())
{
}
// output sink is for testing
internal MsBuildFileSetFactory(ILogger logger, string projectFile, OutputSink outputSink)
internal MsBuildFileSetFactory(IReporter reporter, string projectFile, OutputSink outputSink)
{
Ensure.NotNull(logger, nameof(logger));
Ensure.NotNull(reporter, nameof(reporter));
Ensure.NotNullOrEmpty(projectFile, nameof(projectFile));
Ensure.NotNull(outputSink, nameof(outputSink));
_logger = logger;
_reporter = reporter;
_projectFile = projectFile;
_watchTargetsDir = FindWatchTargetsDir();
_outputSink = outputSink;
_processRunner = new ProcessRunner(logger);
_processRunner = new ProcessRunner(reporter);
}
internal List<string> BuildFlags { get; } = new List<string>
{
"/nologo",
"/v:n",
"/t:GenerateWatchList",
"/t:" + TargetName,
"/p:DotNetWatchBuild=true", // extensibility point for users
"/p:DesignTimeBuild=true", // don't do expensive things
};
@ -66,10 +65,6 @@ namespace Microsoft.DotNet.Watcher.Internal
{
cancellationToken.ThrowIfCancellationRequested();
#if DEBUG
var stopwatch = new Stopwatch();
stopwatch.Start();
#endif
var capture = _outputSink.StartCapture();
// TODO adding files doesn't currently work. Need to provide a way to detect new files
// find files
@ -80,53 +75,56 @@ namespace Microsoft.DotNet.Watcher.Internal
Arguments = new[]
{
"msbuild",
_projectFile,
_projectFile,
$"/p:_DotNetWatchTargetsLocation={_watchTargetsDir}", // add our dotnet-watch targets
$"/p:_DotNetWatchListFile={watchList}"
}.Concat(BuildFlags),
OutputCapture = capture
};
_reporter.Verbose($"Running MSBuild target '{TargetName}' on '{_projectFile}'");
var exitCode = await _processRunner.RunAsync(processSpec, cancellationToken);
if (exitCode == 0)
{
var files = File.ReadAllLines(watchList)
var fileset = new FileSet(
File.ReadAllLines(watchList)
.Select(l => l?.Trim())
.Where(l => !string.IsNullOrEmpty(l));
var fileset = new FileSet(files);
.Where(l => !string.IsNullOrEmpty(l)));
_reporter.Verbose($"Watching {fileset.Count} file(s) for changes");
#if DEBUG
_logger.LogDebug(string.Join(Environment.NewLine, fileset));
Debug.Assert(files.All(Path.IsPathRooted), "All files should be rooted paths");
stopwatch.Stop();
_logger.LogDebug("Gathered project information in {time}ms", stopwatch.ElapsedMilliseconds);
foreach (var file in fileset)
{
_reporter.Verbose($" -> {file}");
}
Debug.Assert(fileset.All(Path.IsPathRooted), "All files should be rooted paths");
#endif
return fileset;
}
var sb = new StringBuilder()
.Append("Error(s) finding watch items project file '")
.Append(Path.GetFileName(_projectFile))
.AppendLine("' :");
_reporter.Error($"Error(s) finding watch items project file '{Path.GetFileName(_projectFile)}'");
_reporter.Output($"MSBuild output from target '{TargetName}'");
foreach (var line in capture.Lines)
{
sb.Append(" [MSBUILD] :").AppendLine(line);
_reporter.Output($" [MSBUILD] : {line}");
}
_logger.LogError(sb.ToString());
_logger.LogInformation("Fix the error to continue or press Ctrl+C to exit.");
_reporter.Warn("Fix the error to continue or press Ctrl+C to exit.");
var fileSet = new FileSet(new[] { _projectFile });
var fileSet = new FileSet(new[] {_projectFile});
using (var watcher = new FileSetWatcher(fileSet))
{
await watcher.GetChangedFileAsync(cancellationToken);
_logger.LogInformation($"File changed: {_projectFile}");
_reporter.Output($"File changed: {_projectFile}");
}
}
}
@ -143,7 +141,8 @@ namespace Microsoft.DotNet.Watcher.Internal
var projectExtensionsPath = Path.Combine(Path.GetDirectoryName(_projectFile), "obj");
// see https://github.com/Microsoft/msbuild/blob/bf9b21cc7869b96ea2289ff31f6aaa5e1d525a26/src/XMakeTasks/Microsoft.Common.targets#L127
var projectExtensionFile = Path.Combine(projectExtensionsPath, Path.GetFileName(_projectFile) + ProjectExtensionFileExtension);
var projectExtensionFile = Path.Combine(projectExtensionsPath,
Path.GetFileName(_projectFile) + ProjectExtensionFileExtension);
if (!File.Exists(projectExtensionFile))
{
@ -173,4 +172,4 @@ namespace Microsoft.DotNet.Watcher.Internal
return Path.GetDirectoryName(targetPath);
}
}
}
}

View File

@ -7,20 +7,19 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher.Internal
{
public class ProcessRunner
{
private readonly ILogger _logger;
private readonly IReporter _reporter;
public ProcessRunner(ILogger logger)
public ProcessRunner(IReporter reporter)
{
Ensure.NotNull(logger, nameof(logger));
Ensure.NotNull(reporter, nameof(reporter));
_logger = logger;
_reporter = reporter;
}
// May not be necessary in the future. See https://github.com/dotnet/corefx/issues/12039
@ -30,12 +29,16 @@ namespace Microsoft.DotNet.Watcher.Internal
int exitCode;
var stopwatch = new Stopwatch();
using (var process = CreateProcess(processSpec))
using (var processState = new ProcessState(process))
{
cancellationToken.Register(() => processState.TryKill());
stopwatch.Start();
process.Start();
_reporter.Verbose($"Started '{processSpec.Executable}' with process id {process.Id}");
if (processSpec.IsOutputCaptured)
{
@ -47,16 +50,12 @@ namespace Microsoft.DotNet.Watcher.Internal
}
else
{
_logger.LogInformation("{execName} process id: {pid}", processSpec.ShortDisplayName(), process.Id);
await processState.Task;
}
exitCode = process.ExitCode;
}
if (!processSpec.IsOutputCaptured)
{
LogResult(processSpec, exitCode);
stopwatch.Stop();
_reporter.Verbose($"Process id {process.Id} ran for {stopwatch.ElapsedMilliseconds}ms");
}
return exitCode;
@ -81,19 +80,6 @@ namespace Microsoft.DotNet.Watcher.Internal
return process;
}
private void LogResult(ProcessSpec processSpec, int exitCode)
{
var processName = processSpec.ShortDisplayName();
if (exitCode == 0)
{
_logger.LogInformation("{execName} exit code: {code}", processName, exitCode);
}
else
{
_logger.LogError("{execName} exit code: {code}", processName, exitCode);
}
}
private static async Task ConsumeStreamAsync(StreamReader reader, Action<string> consume)
{
string line;

View File

@ -14,9 +14,7 @@
</packageTypes>
<dependencies>
<group targetFramework=".NETCoreApp1.0">
<dependency id="Microsoft.Extensions.Logging" version="$dep_1$" />
<dependency id="Microsoft.Extensions.Logging.Console" version="$dep_2$" />
<dependency id="Microsoft.NETCore.App" version="$dep_3$" />
<dependency id="Microsoft.NETCore.App" version="$dep_1$" />
</group>
</dependencies>
</metadata>

View File

@ -2,20 +2,18 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher
{
public class Program
{
private const string LoggerName = "DotNetWatcher";
private readonly IConsole _console;
private readonly string _workingDir;
@ -39,44 +37,18 @@ namespace Microsoft.DotNet.Watcher
public async Task<int> RunAsync(string[] args)
{
using (CancellationTokenSource ctrlCTokenSource = new CancellationTokenSource())
CommandLineOptions options;
try
{
_console.CancelKeyPress += (sender, ev) =>
{
if (!ctrlCTokenSource.IsCancellationRequested)
{
_console.Out.WriteLine($"[{LoggerName}] Shutdown requested. Press Ctrl+C again to force exit.");
ev.Cancel = true;
}
else
{
ev.Cancel = false;
}
ctrlCTokenSource.Cancel();
};
try
{
return await MainInternalAsync(args, ctrlCTokenSource.Token);
}
catch (Exception ex)
{
if (ex is TaskCanceledException || ex is OperationCanceledException)
{
// swallow when only exception is the CTRL+C forced an exit
return 0;
}
_console.Error.WriteLine(ex.ToString());
_console.Error.WriteLine($"[{LoggerName}] An unexpected error occurred".Bold().Red());
return 1;
}
options = CommandLineOptions.Parse(args, _console);
}
catch (CommandParsingException ex)
{
CreateReporter(verbose: true, quiet: false, console: _console)
.Error(ex.Message);
return 1;
}
}
private async Task<int> MainInternalAsync(string[] args, CancellationToken cancellationToken)
{
var options = CommandLineOptions.Parse(args, _console);
if (options == null)
{
// invalid args syntax
@ -88,58 +60,122 @@ namespace Microsoft.DotNet.Watcher
return 2;
}
var loggerFactory = new LoggerFactory();
var commandProvider = new CommandOutputProvider
{
LogLevel = ResolveLogLevel(options)
};
loggerFactory.AddProvider(commandProvider);
var logger = loggerFactory.CreateLogger(LoggerName);
var reporter = CreateReporter(options.IsVerbose, options.IsQuiet, _console);
using (CancellationTokenSource ctrlCTokenSource = new CancellationTokenSource())
{
_console.CancelKeyPress += (sender, ev) =>
{
if (!ctrlCTokenSource.IsCancellationRequested)
{
reporter.Output("Shutdown requested. Press Ctrl+C again to force exit.");
ev.Cancel = true;
}
else
{
ev.Cancel = false;
}
ctrlCTokenSource.Cancel();
};
try
{
return await MainInternalAsync(reporter, options.Project, options.RemainingArguments, ctrlCTokenSource.Token);
}
catch (Exception ex)
{
if (ex is TaskCanceledException || ex is OperationCanceledException)
{
// swallow when only exception is the CTRL+C forced an exit
return 0;
}
reporter.Error(ex.ToString());
reporter.Error("An unexpected error occurred");
return 1;
}
}
}
private async Task<int> MainInternalAsync(
IReporter reporter,
string project,
ICollection<string> args,
CancellationToken cancellationToken)
{
// TODO multiple projects should be easy enough to add here
string projectFile;
try
{
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, options.Project);
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, project);
}
catch (FileNotFoundException ex)
{
_console.Error.WriteLine(ex.Message.Bold().Red());
reporter.Error(ex.Message);
return 1;
}
var fileSetFactory = new MsBuildFileSetFactory(logger, projectFile);
var fileSetFactory = new MsBuildFileSetFactory(reporter, projectFile);
var processInfo = new ProcessSpec
{
Executable = DotNetMuxer.MuxerPathOrDefault(),
WorkingDirectory = Path.GetDirectoryName(projectFile),
Arguments = options.RemainingArguments
Arguments = args
};
await new DotNetWatcher(logger)
.WatchAsync(processInfo, fileSetFactory, cancellationToken);
await new DotNetWatcher(reporter)
.WatchAsync(processInfo, fileSetFactory, cancellationToken);
return 0;
}
private LogLevel ResolveLogLevel(CommandLineOptions options)
private static IReporter CreateReporter(bool verbose, bool quiet, IConsole console)
{
if (options.IsQuiet)
{
return LogLevel.Warning;
}
const string prefix = "watch : ";
var colorPrefix = new ColorFormatter(ConsoleColor.DarkGray).Format(prefix);
bool globalVerbose;
bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE"), out globalVerbose);
return new ReporterBuilder()
.WithConsole(console)
.Verbose(f =>
{
if (console.IsOutputRedirected)
{
f.WithPrefix(prefix);
}
else
{
f.WithColor(ConsoleColor.DarkGray).WithPrefix(colorPrefix);
}
if (options.IsVerbose // dotnet watch --verbose
|| globalVerbose) // dotnet --verbose watch
{
return LogLevel.Debug;
}
return LogLevel.Information;
f.When(() => verbose || CliContext.IsGlobalVerbose());
})
.Output(f => f
.WithPrefix(console.IsOutputRedirected ? prefix : colorPrefix)
.When(() => !quiet))
.Warn(f =>
{
if (console.IsOutputRedirected)
{
f.WithPrefix(prefix);
}
else
{
f.WithColor(ConsoleColor.Yellow).WithPrefix(colorPrefix);
}
})
.Error(f =>
{
if (console.IsOutputRedirected)
{
f.WithPrefix(prefix);
}
else
{
f.WithColor(ConsoleColor.Red).WithPrefix(colorPrefix);
}
})
.Build();
}
}
}
}

View File

@ -32,8 +32,6 @@
]
},
"dependencies": {
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.Extensions.Process.Sources": {
"type": "build",
"version": "1.0.0"

View File

@ -14,11 +14,8 @@
</packageTypes>
<dependencies>
<group targetFramework=".NETCoreApp1.0">
<dependency id="Microsoft.Extensions.CommandLineUtils" version="$dep_1$" />
<dependency id="Microsoft.Extensions.Logging" version="$dep_2$" />
<dependency id="Microsoft.Extensions.Logging.Console" version="$dep_3$" />
<dependency id="Microsoft.NETCore.App" version="$dep_4$" />
<dependency id="System.Data.SqlClient" version="$dep_5$" />
<dependency id="Microsoft.NETCore.App" version="$dep_1$" />
<dependency id="System.Data.SqlClient" version="$dep_2$" />
</group>
</dependencies>
</metadata>

View File

@ -6,7 +6,7 @@ using System.Data;
using System.Data.SqlClient;
using System.Reflection;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.Caching.SqlConfig.Tools
{
@ -15,54 +15,61 @@ namespace Microsoft.Extensions.Caching.SqlConfig.Tools
private string _connectionString = null;
private string _schemaName = null;
private string _tableName = null;
private readonly IConsole _console;
private readonly ILogger _logger;
public Program()
public Program(IConsole console)
{
var loggerFactory = new LoggerFactory();
loggerFactory.AddConsole();
_logger = loggerFactory.CreateLogger<Program>();
Ensure.NotNull(console, nameof(console));
_console = console;
}
public static int Main(string[] args)
{
return new Program().Run(args);
return new Program(PhysicalConsole.Singleton).Run(args);
}
public int Run(string[] args)
{
DebugHelper.HandleDebugSwitch(ref args);
try
{
var description = "Creates table and indexes in Microsoft SQL Server database " +
"to be used for distributed caching";
var app = new CommandLineApplication
{
Name = "dotnet sql-cache",
FullName = "SQL Server Cache Command Line Tool",
Description = description,
Description =
"Creates table and indexes in Microsoft SQL Server database to be used for distributed caching",
};
app.HelpOption("-?|-h|--help");
app.HelpOption();
app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
var verbose = app.VerboseOption();
app.Command("create", command =>
{
command.Description = description;
command.Description = app.Description;
var connectionStringArg = command.Argument(
"[connectionString]",
"The connection string to connect to the database.");
var schemaNameArg = command.Argument("[schemaName]", "Name of the table schema.");
var tableNameArg = command.Argument("[tableName]", "Name of the table to be created.");
command.HelpOption("-?|-h|--help");
"[connectionString]", "The connection string to connect to the database.");
var schemaNameArg = command.Argument(
"[schemaName]", "Name of the table schema.");
var tableNameArg = command.Argument(
"[tableName]", "Name of the table to be created.");
command.HelpOption();
command.OnExecute(() =>
{
var reporter = CreateReporter(verbose.HasValue());
if (string.IsNullOrEmpty(connectionStringArg.Value)
|| string.IsNullOrEmpty(schemaNameArg.Value)
|| string.IsNullOrEmpty(tableNameArg.Value))
|| string.IsNullOrEmpty(schemaNameArg.Value)
|| string.IsNullOrEmpty(tableNameArg.Value))
{
_logger.LogWarning("Invalid input");
reporter.Error("Invalid input");
app.ShowHelp();
return 2;
}
@ -71,7 +78,7 @@ namespace Microsoft.Extensions.Caching.SqlConfig.Tools
_schemaName = schemaNameArg.Value;
_tableName = tableNameArg.Value;
return CreateTableAndIndexes();
return CreateTableAndIndexes(reporter);
});
});
@ -86,12 +93,41 @@ namespace Microsoft.Extensions.Caching.SqlConfig.Tools
}
catch (Exception exception)
{
_logger.LogCritical("An error occurred. {ErrorMessage}", exception.Message);
CreateReporter(verbose: false).Error($"An error occurred. {exception.Message}");
return 1;
}
}
private int CreateTableAndIndexes()
private IReporter CreateReporter(bool verbose)
{
return new ReporterBuilder()
.WithConsole(_console)
.Verbose(f =>
{
f.When(() => verbose);
if (!_console.IsOutputRedirected)
{
f.WithColor(ConsoleColor.DarkGray);
}
})
.Warn(f =>
{
if (!_console.IsOutputRedirected)
{
f.WithColor(ConsoleColor.Yellow);
}
})
.Error(f =>
{
if (!_console.IsErrorRedirected)
{
f.WithColor(ConsoleColor.Red);
}
})
.Build();
}
private int CreateTableAndIndexes(IReporter reporter)
{
ValidateConnectionString();
@ -106,7 +142,7 @@ namespace Microsoft.Extensions.Caching.SqlConfig.Tools
{
if (reader.Read())
{
_logger.LogWarning(
reporter.Warn(
$"Table with schema '{_schemaName}' and name '{_tableName}' already exists. " +
"Provide a different table name and try again.");
return 1;
@ -118,23 +154,26 @@ namespace Microsoft.Extensions.Caching.SqlConfig.Tools
try
{
command = new SqlCommand(sqlQueries.CreateTable, connection, transaction);
reporter.Verbose($"Executing {command.CommandText}");
command.ExecuteNonQuery();
command = new SqlCommand(
sqlQueries.CreateNonClusteredIndexOnExpirationTime,
connection,
transaction);
reporter.Verbose($"Executing {command.CommandText}");
command.ExecuteNonQuery();
transaction.Commit();
_logger.LogInformation("Table and index were created successfully.");
reporter.Output("Table and index were created successfully.");
}
catch (Exception ex)
{
_logger.LogError(
"An error occurred while trying to create the table and index. {ErrorMessage}",
ex.Message);
reporter.Error(
$"An error occurred while trying to create the table and index. {ex.Message}");
transaction.Rollback();
return 1;
@ -154,7 +193,7 @@ namespace Microsoft.Extensions.Caching.SqlConfig.Tools
catch (Exception ex)
{
throw new ArgumentException(
$"Invalid Sql server connection string '{_connectionString}'. {ex.Message}", ex);
$"Invalid SQL Server connection string '{_connectionString}'. {ex.Message}", ex);
}
}
}

View File

@ -5,7 +5,8 @@
"emitEntryPoint": true,
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk",
"compile": "../Shared/CommandLineApplicationExtensions.cs"
"compile": "../Shared/**/*.cs"
},
"description": "Command line tool to create tables and indexes in a Microsoft SQL Server database for distributed caching.",
"packOptions": {
@ -20,9 +21,6 @@
]
},
"dependencies": {
"Microsoft.Extensions.CommandLineUtils": "1.0.0",
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.NETCore.App": {
"version": "1.0.1",
"type": "platform"

View File

@ -31,8 +31,7 @@ namespace Microsoft.Extensions.SecretManager.Tools
app.HelpOption();
app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
var optionVerbose = app.Option("-v|--verbose", "Verbose output",
CommandOptionType.NoValue, inherited: true);
var optionVerbose = app.VerboseOption();
var optionProject = app.Option("-p|--project <PROJECT>", "Path to project, default is current directory",
CommandOptionType.SingleValue, inherited: true);
@ -48,7 +47,7 @@ namespace Microsoft.Extensions.SecretManager.Tools
var options = new CommandLineOptions();
app.Command("set", c => SetCommand.Configure(c, options, console));
app.Command("remove", c => RemoveCommand.Configure(c, options, console));
app.Command("remove", c => RemoveCommand.Configure(c, options));
app.Command("list", c => ListCommand.Configure(c, options));
app.Command("clear", c => ClearCommand.Configure(c, options));

View File

@ -1,62 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
namespace Microsoft.Extensions.SecretManager.Tools
{
/// <summary>
/// Logger to print formatted command output.
/// </summary>
public class CommandOutputLogger : ILogger
{
private readonly CommandOutputProvider _provider;
private readonly AnsiConsole _outConsole;
public CommandOutputLogger(CommandOutputProvider commandOutputProvider, bool useConsoleColor)
{
_provider = commandOutputProvider;
_outConsole = AnsiConsole.GetOutput(useConsoleColor);
}
public IDisposable BeginScope<TState>(TState state)
{
throw new NotImplementedException();
}
public bool IsEnabled(LogLevel logLevel)
{
if (logLevel < _provider.LogLevel)
{
return false;
}
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (IsEnabled(logLevel))
{
_outConsole.WriteLine(string.Format("{0}: {1}", Caption(logLevel), formatter(state, exception)));
}
}
private string Caption(LogLevel logLevel)
{
switch (logLevel)
{
case LogLevel.Trace: return "\x1b[35mtrace\x1b[39m";
case LogLevel.Debug: return "\x1b[35mdebug\x1b[39m";
case LogLevel.Information: return "\x1b[32minfo\x1b[39m";
case LogLevel.Warning: return "\x1b[33mwarn\x1b[39m";
case LogLevel.Error: return "\x1b[31mfail\x1b[39m";
case LogLevel.Critical: return "\x1b[31mcritical\x1b[39m";
}
throw new Exception("Unknown LogLevel");
}
}
}

View File

@ -1,23 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
namespace Microsoft.Extensions.SecretManager.Tools
{
public class CommandOutputProvider : ILoggerProvider
{
public ILogger CreateLogger(string name)
{
var useConsoleColor = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
return new CommandOutputLogger(this, useConsoleColor);
}
public void Dispose()
{
}
public LogLevel LogLevel { get; set; } = LogLevel.Information;
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.SecretManager.Tools.Internal
@ -10,16 +9,16 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
{
public CommandContext(
SecretsStore store,
ILogger logger,
IReporter reporter,
IConsole console)
{
SecretStore = store;
Logger = logger;
Reporter = reporter;
Console = console;
}
public IConsole Console { get; }
public ILogger Logger { get; }
public IReporter Reporter { get; }
public SecretsStore SecretStore { get; }
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -41,13 +40,13 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
if (context.SecretStore.Count == 0)
{
context.Logger.LogInformation(Resources.Error_No_Secrets_Found);
context.Reporter.Output(Resources.Error_No_Secrets_Found);
}
else
{
foreach (var secret in context.SecretStore.AsEnumerable())
{
context.Logger.LogInformation(Resources.FormatMessage_Secret_Value_Format(secret.Key, secret.Value));
context.Reporter.Output(Resources.FormatMessage_Secret_Value_Format(secret.Key, secret.Value));
}
}
}
@ -60,10 +59,9 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
jObject[item.Key] = item.Value;
}
// TODO logger would prefix each line.
context.Console.Out.WriteLine("//BEGIN");
context.Console.Out.WriteLine(jObject.ToString(Formatting.Indented));
context.Console.Out.WriteLine("//END");
context.Reporter.Output("//BEGIN");
context.Reporter.Output(jObject.ToString(Formatting.Indented));
context.Reporter.Output("//END");
}
}
}

View File

@ -7,7 +7,6 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.SecretManager.Tools.Internal
@ -15,27 +14,32 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
public class ProjectIdResolver : IDisposable
{
private const string TargetsFileName = "FindUserSecretsProperty.targets";
private readonly ILogger _logger;
private const string DefaultConfig = "Debug";
private readonly IReporter _reporter;
private readonly string _workingDirectory;
private readonly List<string> _tempFiles = new List<string>();
public ProjectIdResolver(ILogger logger, string workingDirectory)
public ProjectIdResolver(IReporter reporter, string workingDirectory)
{
_workingDirectory = workingDirectory;
_logger = logger;
_reporter = reporter;
}
public string Resolve(string project, string configuration = "Debug")
public string Resolve(string project, string configuration)
{
var finder = new MsBuildProjectFinder(_workingDirectory);
var projectFile = finder.FindMsBuildProject(project);
_logger.LogDebug(Resources.Message_Project_File_Path, projectFile);
_reporter.Verbose(Resources.FormatMessage_Project_File_Path(projectFile));
var targetFile = GetTargetFile();
var outputFile = Path.GetTempFileName();
_tempFiles.Add(outputFile);
configuration = !string.IsNullOrEmpty(configuration)
? configuration
: DefaultConfig;
var args = new[]
{
"msbuild",
@ -53,13 +57,18 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
RedirectStandardOutput = true,
RedirectStandardError = true
};
#if DEBUG
_reporter.Verbose($"Invoking '{psi.FileName} {psi.Arguments}'");
#endif
var process = Process.Start(psi);
process.WaitForExit();
if (process.ExitCode != 0)
{
_logger.LogDebug(process.StandardOutput.ReadToEnd());
_logger.LogDebug(process.StandardError.ReadToEnd());
_reporter.Verbose(process.StandardOutput.ReadToEnd());
_reporter.Verbose(process.StandardError.ReadToEnd());
throw new InvalidOperationException(Resources.FormatError_ProjectFailedToLoad(projectFile));
}

View File

@ -1,9 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.SecretManager.Tools.Internal
@ -12,7 +10,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
{
private readonly string _keyName;
public static void Configure(CommandLineApplication command, CommandLineOptions options, IConsole console)
public static void Configure(CommandLineApplication command, CommandLineOptions options)
{
command.Description = "Removes the specified user secret";
command.HelpOption();
@ -22,12 +20,10 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
{
if (keyArg.Value == null)
{
console.Error.WriteLine(Resources.FormatError_MissingArgument("name").Red());
return 1;
throw new CommandParsingException(command, Resources.FormatError_MissingArgument("name"));
}
options.Command = new RemoveCommand(keyArg.Value);
return 0;
});
}
@ -41,7 +37,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
{
if (!context.SecretStore.ContainsKey(_keyName))
{
context.Logger.LogWarning(Resources.Error_Missing_Secret, _keyName);
context.Reporter.Warn(Resources.FormatError_Missing_Secret(_keyName));
}
else
{

View File

@ -8,7 +8,6 @@ using System.Linq;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
using Newtonsoft.Json.Linq;
@ -19,7 +18,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
private readonly string _secretsFilePath;
private IDictionary<string, string> _secrets;
public SecretsStore(string userSecretsId, ILogger logger)
public SecretsStore(string userSecretsId, IReporter reporter)
{
Ensure.NotNull(userSecretsId, nameof(userSecretsId));
@ -29,7 +28,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
var secretDir = Path.GetDirectoryName(_secretsFilePath);
Directory.CreateDirectory(secretDir);
logger.LogDebug(Resources.Message_Secret_File_Path, _secretsFilePath);
reporter.Verbose(Resources.FormatMessage_Secret_File_Path(_secretsFilePath));
_secrets = Load(userSecretsId);
}

View File

@ -1,11 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.SecretManager.Tools.Internal
@ -34,23 +32,21 @@ Examples:
if (console.IsInputRedirected && nameArg.Value == null)
{
options.Command = new FromStdInStrategy();
return 0;
}
if (string.IsNullOrEmpty(nameArg.Value))
else
{
console.Error.WriteLine(Resources.FormatError_MissingArgument("name").Red());
return 1;
}
if (string.IsNullOrEmpty(nameArg.Value))
{
throw new CommandParsingException(command, Resources.FormatError_MissingArgument("name"));
}
if (valueArg.Value == null)
{
console.Error.WriteLine(Resources.FormatError_MissingArgument("value").Red());
return 1;
}
if (valueArg.Value == null)
{
throw new CommandParsingException(command, Resources.FormatError_MissingArgument("value"));
}
options.Command = new ForOneValueStrategy(nameArg.Value, valueArg.Value);
return 0;
options.Command = new ForOneValueStrategy(nameArg.Value, valueArg.Value);
}
});
}
@ -76,7 +72,7 @@ Examples:
context.SecretStore.Set(k.Key, k.Value);
}
context.Logger.LogInformation(Resources.Message_Saved_Secrets, provider.CurrentData.Count);
context.Reporter.Output(Resources.FormatMessage_Saved_Secrets(provider.CurrentData.Count));
context.SecretStore.Save();
}
@ -97,7 +93,7 @@ Examples:
{
context.SecretStore.Set(_keyName, _keyValue);
context.SecretStore.Save();
context.Logger.LogInformation(Resources.Message_Saved_Secret, _keyName, _keyValue);
context.Reporter.Output(Resources.FormatMessage_Saved_Secret(_keyName, _keyValue));
}
}
}

View File

@ -17,8 +17,7 @@
<group targetFramework=".NETCoreApp1.0">
<!-- MUST BE alphabetical -->
<dependency id="Microsoft.Extensions.Configuration.UserSecrets" version="$dep_1$" />
<dependency id="Microsoft.Extensions.Logging" version="$dep_2$" />
<dependency id="Microsoft.NETCore.App" version="$dep_3$" />
<dependency id="Microsoft.NETCore.App" version="$dep_2$" />
</group>
</dependencies>
</metadata>

View File

@ -3,7 +3,7 @@
using System;
using System.IO;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.SecretManager.Tools.Internal;
using Microsoft.Extensions.Tools.Internal;
@ -11,8 +11,6 @@ namespace Microsoft.Extensions.SecretManager.Tools
{
public class Program
{
private ILogger _logger;
private CommandOutputProvider _loggerProvider;
private readonly IConsole _console;
private readonly string _workingDirectory;
@ -29,23 +27,6 @@ namespace Microsoft.Extensions.SecretManager.Tools
{
_console = console;
_workingDirectory = workingDirectory;
var loggerFactory = new LoggerFactory();
CommandOutputProvider = new CommandOutputProvider();
loggerFactory.AddProvider(CommandOutputProvider);
Logger = loggerFactory.CreateLogger<Program>();
}
public ILogger Logger
{
get { return _logger; }
set { _logger = Ensure.NotNull(value, nameof(value)); }
}
public CommandOutputProvider CommandOutputProvider
{
get { return _loggerProvider; }
set { _loggerProvider = Ensure.NotNull(value, nameof(value)); }
}
public bool TryRun(string[] args, out int returnCode)
@ -57,8 +38,9 @@ namespace Microsoft.Extensions.SecretManager.Tools
}
catch (Exception exception)
{
Logger.LogDebug(exception.ToString());
Logger.LogCritical(Resources.Error_Command_Failed, exception.Message);
var reporter = CreateReporter(verbose: true);
reporter.Verbose(exception.ToString());
reporter.Error(Resources.FormatError_Command_Failed(exception.Message));
returnCode = 1;
return false;
}
@ -66,7 +48,16 @@ namespace Microsoft.Extensions.SecretManager.Tools
internal int RunInternal(params string[] args)
{
var options = CommandLineOptions.Parse(args, _console);
CommandLineOptions options;
try
{
options = CommandLineOptions.Parse(args, _console);
}
catch (CommandParsingException ex)
{
CreateReporter(verbose: false).Error(ex.Message);
return 1;
}
if (options == null)
{
@ -78,36 +69,62 @@ namespace Microsoft.Extensions.SecretManager.Tools
return 2;
}
if (options.IsVerbose)
{
CommandOutputProvider.LogLevel = LogLevel.Debug;
}
var reporter = CreateReporter(options.IsVerbose);
string userSecretsId;
try
{
userSecretsId = ResolveId(options);
userSecretsId = ResolveId(options, reporter);
}
catch (Exception ex) when (ex is InvalidOperationException || ex is FileNotFoundException)
{
_logger.LogError(ex.Message);
reporter.Error(ex.Message);
return 1;
}
var store = new SecretsStore(userSecretsId, Logger);
var context = new Internal.CommandContext(store, Logger, _console);
var store = new SecretsStore(userSecretsId, reporter);
var context = new Internal.CommandContext(store, reporter, _console);
options.Command.Execute(context);
return 0;
}
internal string ResolveId(CommandLineOptions options)
private IReporter CreateReporter(bool verbose)
{
return new ReporterBuilder()
.WithConsole(_console)
.Verbose(f =>
{
f.When(() => verbose);
if (!_console.IsOutputRedirected)
{
f.WithColor(ConsoleColor.DarkGray);
}
})
.Warn(f =>
{
if (!_console.IsOutputRedirected)
{
f.WithColor(ConsoleColor.Yellow);
}
})
.Error(f =>
{
if (!_console.IsErrorRedirected)
{
f.WithColor(ConsoleColor.Red);
}
})
.Build();
}
internal string ResolveId(CommandLineOptions options, IReporter reporter)
{
if (!string.IsNullOrEmpty(options.Id))
{
return options.Id;
}
using (var resolver = new ProjectIdResolver(Logger, _workingDirectory))
using (var resolver = new ProjectIdResolver(reporter, _workingDirectory))
{
return resolver.Resolve(options.Project, options.Configuration);
}

View File

@ -39,7 +39,6 @@
},
"dependencies": {
"Microsoft.Extensions.Configuration.UserSecrets": "1.0.0",
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.NETCore.App": {
"version": "1.0.1",
"type": "platform"

21
src/Shared/CliContext.cs Normal file
View File

@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.Extensions.Tools.Internal
{
public static class CliContext
{
/// <summary>
/// dotnet --verbose subcommand
/// </summary>
/// <returns></returns>
public static bool IsGlobalVerbose()
{
bool globalVerbose;
bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE"), out globalVerbose);
return globalVerbose;
}
}
}

View File

@ -11,6 +11,9 @@ namespace Microsoft.Extensions.CommandLineUtils
public static CommandOption HelpOption(this CommandLineApplication app)
=> app.HelpOption("-?|-h|--help");
public static CommandOption VerboseOption(this CommandLineApplication app)
=> app.Option("-v|--verbose", "Show verbose output", CommandOptionType.NoValue, inherited: true);
public static void OnExecute(this CommandLineApplication app, Action action)
=> app.OnExecute(() =>
{

View File

@ -13,5 +13,7 @@ namespace Microsoft.Extensions.Tools.Internal
TextWriter Error { get; }
TextReader In { get; }
bool IsInputRedirected { get; }
bool IsOutputRedirected { get; }
bool IsErrorRedirected { get; }
}
}

View File

@ -23,5 +23,7 @@ namespace Microsoft.Extensions.Tools.Internal
public TextReader In => Console.In;
public TextWriter Out => Console.Out;
public bool IsInputRedirected => Console.IsInputRedirected;
public bool IsOutputRedirected => Console.IsOutputRedirected;
public bool IsErrorRedirected => Console.IsErrorRedirected;
}
}

View File

@ -0,0 +1,57 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.Extensions.Tools.Internal
{
public class ColorFormatter : IFormatter
{
// resets foreground color only
private const string ResetColor = "\x1B[39m";
private static readonly IDictionary<ConsoleColor, int> AnsiColorCodes
= new Dictionary<ConsoleColor, int>
{
{ConsoleColor.Black, 30},
{ConsoleColor.DarkRed, 31},
{ConsoleColor.DarkGreen, 32},
{ConsoleColor.DarkYellow, 33},
{ConsoleColor.DarkBlue, 34},
{ConsoleColor.DarkMagenta, 35},
{ConsoleColor.DarkCyan, 36},
{ConsoleColor.Gray, 37},
{ConsoleColor.DarkGray, 90},
{ConsoleColor.Red, 91},
{ConsoleColor.Green, 92},
{ConsoleColor.Yellow, 93},
{ConsoleColor.Blue, 94},
{ConsoleColor.Magenta, 95},
{ConsoleColor.Cyan, 96},
{ConsoleColor.White, 97},
};
private readonly string _prefix;
public ColorFormatter(ConsoleColor color)
{
_prefix = GetAnsiCode(color);
}
public string Format(string text)
=> text?.Length > 0
? $"{_prefix}{text}{ResetColor}"
: text;
private static string GetAnsiCode(ConsoleColor color)
{
int code;
if (!AnsiColorCodes.TryGetValue(color, out code))
{
throw new ArgumentOutOfRangeException(nameof(color), color, null);
}
return $"\x1B[{code}m";
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.Extensions.Tools.Internal
{
public class CompositeFormatter : IFormatter
{
private readonly IFormatter[] _formatters;
public CompositeFormatter(IFormatter[] formatters)
{
Ensure.NotNull(formatters, nameof(formatters));
_formatters = formatters;
}
public string Format(string text)
{
foreach (var formatter in _formatters)
{
text = formatter.Format(text);
}
return text;
}
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.Extensions.Tools.Internal
{
public class ConditionalFormatter : IFormatter
{
private readonly Func<bool> _predicate;
public ConditionalFormatter(Func<bool> predicate)
{
Ensure.NotNull(predicate, nameof(predicate));
_predicate = predicate;
}
public string Format(string text)
=> _predicate()
? text
: null;
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.Extensions.Tools.Internal
{
public class DefaultFormatter : IFormatter
{
public static readonly IFormatter Instance = new DefaultFormatter();
private DefaultFormatter() {}
public string Format(string text)
=> text;
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.Extensions.Tools.Internal
{
public class FormatterBuilder
{
private readonly List<IFormatter> _formatters = new List<IFormatter>();
public FormatterBuilder WithColor(ConsoleColor color)
{
_formatters.Add(new ColorFormatter(color));
return this;
}
public FormatterBuilder WithPrefix(string prefix)
{
_formatters.Add(new PrefixFormatter(prefix));
return this;
}
public FormatterBuilder When(Func<bool> predicate)
{
_formatters.Add(new ConditionalFormatter(predicate));
return this;
}
public IFormatter Build()
{
if (_formatters.Count == 0)
{
return DefaultFormatter.Instance;
}
if (_formatters.Count == 1)
{
return _formatters[0];
}
return new CompositeFormatter(_formatters.ToArray());
}
}
}

View File

@ -0,0 +1,62 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
namespace Microsoft.Extensions.Tools.Internal
{
public class FormattingReporter : IReporter
{
private readonly object _writelock = new object();
private readonly IConsole _console;
private readonly IFormatter _verbose;
private readonly IFormatter _warn;
private readonly IFormatter _output;
private readonly IFormatter _error;
public FormattingReporter(IConsole console,
IFormatter verbose,
IFormatter output,
IFormatter warn,
IFormatter error)
{
Ensure.NotNull(console, nameof(console));
Ensure.NotNull(verbose, nameof(verbose));
Ensure.NotNull(output, nameof(output));
Ensure.NotNull(warn, nameof(warn));
Ensure.NotNull(error, nameof(error));
_console = console;
_verbose = verbose;
_output = output;
_warn = warn;
_error = error;
}
public void Verbose(string message)
=> Write(_console.Out, _verbose.Format(message));
public void Output(string message)
=> Write(_console.Out, _output.Format(message));
public void Warn(string message)
=> Write(_console.Out, _warn.Format(message));
public void Error(string message)
=> Write(_console.Error, _error.Format(message));
private void Write(TextWriter writer, string message)
{
if (message == null)
{
return;
}
lock (_writelock)
{
writer.WriteLine(message);
}
}
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.Extensions.Tools.Internal
{
public interface IFormatter
{
string Format(string text);
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.Extensions.Tools.Internal
{
public interface IReporter
{
void Verbose(string message);
void Output(string message);
void Warn(string message);
void Error(string message);
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.Extensions.Tools.Internal
{
public class PrefixFormatter : IFormatter
{
private readonly string _prefix;
public PrefixFormatter(string prefix)
{
Ensure.NotNullOrEmpty(prefix, nameof(prefix));
_prefix = prefix;
}
public string Format(string text)
=> _prefix + text;
}
}

View File

@ -0,0 +1,65 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.Extensions.Tools.Internal
{
public class ReporterBuilder
{
private readonly FormatterBuilder _verbose = new FormatterBuilder();
private readonly FormatterBuilder _output = new FormatterBuilder();
private readonly FormatterBuilder _warn = new FormatterBuilder();
private readonly FormatterBuilder _error = new FormatterBuilder();
private IConsole _console;
public ReporterBuilder WithConsole(IConsole console)
{
_console = console;
return this;
}
public FormatterBuilder Verbose() => _verbose;
public FormatterBuilder Output() => _output;
public FormatterBuilder Warn() => _warn;
public FormatterBuilder Error() => _error;
public ReporterBuilder Verbose(Action<FormatterBuilder> configure)
{
configure(_verbose);
return this;
}
public ReporterBuilder Output(Action<FormatterBuilder> configure)
{
configure(_output);
return this;
}
public ReporterBuilder Warn(Action<FormatterBuilder> configure)
{
configure(_warn);
return this;
}
public ReporterBuilder Error(Action<FormatterBuilder> configure)
{
configure(_error);
return this;
}
public IReporter Build()
{
if (_console == null)
{
throw new InvalidOperationException($"Cannot build without first calling {nameof(WithConsole)}");
}
return new FormattingReporter(_console,
_verbose.Build(),
_output.Build(),
_warn.Build(),
_error.Build());
}
}
}

View File

@ -1,35 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace System
{
public static class StringExtensions
{
public static string Black(this string text)
=> "\x1B[30m" + text + "\x1B[39m";
public static string Red(this string text)
=> "\x1B[31m" + text + "\x1B[39m";
public static string Green(this string text)
=> "\x1B[32m" + text + "\x1B[39m";
public static string Yellow(this string text)
=> "\x1B[33m" + text + "\x1B[39m";
public static string Blue(this string text)
=> "\x1B[34m" + text + "\x1B[39m";
public static string Magenta(this string text)
=> "\x1B[35m" + text + "\x1B[39m";
public static string Cyan(this string text)
=> "\x1B[36m" + text + "\x1B[39m";
public static string White(this string text)
=> "\x1B[37m" + text + "\x1B[39m";
public static string Bold(this string text)
=> "\x1B[1m" + text + "\x1B[22m";
}
}

View File

@ -4,6 +4,7 @@
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
using Xunit;
using Xunit.Abstractions;
@ -14,14 +15,12 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
{
private readonly IConsole _console;
private readonly StringBuilder _stdout = new StringBuilder();
private readonly StringBuilder _stderr = new StringBuilder();
public CommandLineOptionsTests(ITestOutputHelper output)
{
_console = new TestConsole(output)
{
Out = new StringWriter(_stdout),
Error = new StringWriter(_stderr),
};
}
@ -57,8 +56,8 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
[Fact]
public void CannotHaveQuietAndVerbose()
{
Assert.Null(CommandLineOptions.Parse(new[] { "--quiet", "--verbose" }, _console));
Assert.Contains(Resources.Error_QuietAndVerboseSpecified, _stderr.ToString());
var ex = Assert.Throws<CommandParsingException>(() => CommandLineOptions.Parse(new[] { "--quiet", "--verbose" }, _console));
Assert.Equal(Resources.Error_QuietAndVerboseSpecified, ex.Message);
}
}
}

View File

@ -7,7 +7,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
using Xunit;
using Xunit.Abstractions;
@ -17,11 +17,11 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
public class MsBuildFileSetFactoryTest : IDisposable
{
private ILogger _logger;
private readonly IReporter _reporter;
private readonly TemporaryDirectory _tempDir;
public MsBuildFileSetFactoryTest(ITestOutputHelper output)
{
_logger = new XunitLogger(output);
_reporter = new TestReporter(output);
_tempDir = new TemporaryDirectory();
}
@ -255,7 +255,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
graph.Find("A").WithProjectReference(graph.Find("W"), watch: false);
var output = new OutputSink();
var filesetFactory = new MsBuildFileSetFactory(_logger, graph.GetOrCreate("A").Path, output)
var filesetFactory = new MsBuildFileSetFactory(_reporter, graph.GetOrCreate("A").Path, output)
{
// enables capturing markers to know which projects have been visited
BuildFlags = { "/p:_DotNetWatchTraceOutput=true" }
@ -263,7 +263,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
var fileset = await GetFileSet(filesetFactory);
_logger.LogInformation(string.Join(
_reporter.Output(string.Join(
Environment.NewLine,
output.Current.Lines.Select(l => "Sink output: " + l)));
@ -297,7 +297,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
}
private Task<IFileSet> GetFileSet(TemporaryCSharpProject target)
=> GetFileSet(new MsBuildFileSetFactory(_logger, target.Path));
=> GetFileSet(new MsBuildFileSetFactory(_reporter, target.Path));
private async Task<IFileSet> GetFileSet(MsBuildFileSetFactory filesetFactory)
{
_tempDir.Create();

View File

@ -1,35 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Microsoft.DotNet.Watcher.Tools.Tests
{
internal class XunitLogger : ILogger
{
private readonly ITestOutputHelper _output;
public XunitLogger(ITestOutputHelper output)
{
_output = output;
}
public IDisposable BeginScope<TState>(TState state)
=> NullScope.Instance;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
_output.WriteLine($"{logLevel}: {formatter(state, exception)}");
}
private class NullScope : IDisposable
{
private NullScope() { }
public static NullScope Instance = new NullScope();
public void Dispose() { }
}
}
}

View File

@ -7,7 +7,6 @@ using System.IO;
using System.Text;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.Configuration.UserSecrets.Tests;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
using Xunit;
using Xunit.Abstractions;
@ -16,23 +15,24 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
{
public class SecretManagerTests : IClassFixture<UserSecretsTestFixture>
{
private readonly TestLogger _logger;
private readonly TestConsole _console;
private readonly UserSecretsTestFixture _fixture;
private readonly StringBuilder _output = new StringBuilder();
public SecretManagerTests(UserSecretsTestFixture fixture, ITestOutputHelper output)
{
_fixture = fixture;
_logger = new TestLogger(output);
_console = new TestConsole(output);
_console = new TestConsole(output)
{
Error = new StringWriter(_output),
Out = new StringWriter(_output),
};
}
private Program CreateProgram()
{
return new Program(_console, Directory.GetCurrentDirectory())
{
Logger = _logger
};
return new Program(_console, Directory.GetCurrentDirectory());
}
[Theory]
@ -44,7 +44,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", project);
Assert.Contains(Resources.FormatError_ProjectMissingId(project), _logger.Messages);
Assert.Contains(Resources.FormatError_ProjectMissingId(project), _output.ToString());
}
[Fact]
@ -54,7 +54,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", project);
Assert.Contains(Resources.FormatError_ProjectFailedToLoad(project), _logger.Messages);
Assert.Contains(Resources.FormatError_ProjectFailedToLoad(project), _output.ToString());
}
[Fact]
@ -64,7 +64,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var secretManager = CreateProgram();
secretManager.RunInternal("list", "--project", projectPath);
Assert.Contains(Resources.FormatError_ProjectPath_NotFound(projectPath), _logger.Messages);
Assert.Contains(Resources.FormatError_ProjectPath_NotFound(projectPath), _output.ToString());
}
[Fact]
@ -73,12 +73,11 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var projectPath = _fixture.GetTempSecretProject();
var cwd = Path.Combine(projectPath, "nested1");
Directory.CreateDirectory(cwd);
var secretManager = new Program(_console, cwd) { Logger = _logger, CommandOutputProvider = _logger.CommandOutputProvider };
secretManager.CommandOutputProvider.LogLevel = LogLevel.Debug;
var secretManager = new Program(_console, cwd);
secretManager.RunInternal("list", "-p", ".." + Path.DirectorySeparatorChar, "--verbose");
Assert.Contains(Resources.FormatMessage_Project_File_Path(Path.Combine(cwd, "..", "TestProject.csproj")), _logger.Messages);
Assert.Contains(Resources.FormatMessage_Project_File_Path(Path.Combine(cwd, "..", "TestProject.csproj")), _output.ToString());
}
[Theory]
@ -98,7 +97,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var dir = fromCurrentDirectory
? projectPath
: Path.GetTempPath();
var secretManager = new Program(_console, dir) { Logger = _logger };
var secretManager = new Program(_console, dir);
foreach (var secret in secrets)
{
@ -108,30 +107,27 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
secretManager.RunInternal(parameters);
}
Assert.Equal(4, _logger.Messages.Count);
foreach (var keyValue in secrets)
{
Assert.Contains(
string.Format("Successfully saved {0} = {1} to the secret store.", keyValue.Key, keyValue.Value),
_logger.Messages);
_output.ToString());
}
_logger.Messages.Clear();
_output.Clear();
var args = fromCurrentDirectory
? new string[] { "list" }
: new string[] { "list", "-p", projectPath };
secretManager.RunInternal(args);
Assert.Equal(4, _logger.Messages.Count);
foreach (var keyValue in secrets)
{
Assert.Contains(
string.Format("{0} = {1}", keyValue.Key, keyValue.Value),
_logger.Messages);
_output.ToString());
}
// Remove secrets.
_logger.Messages.Clear();
_output.Clear();
foreach (var secret in secrets)
{
var parameters = fromCurrentDirectory ?
@ -141,13 +137,12 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
}
// Verify secrets are removed.
_logger.Messages.Clear();
_output.Clear();
args = fromCurrentDirectory
? new string[] { "list" }
: new string[] { "list", "-p", projectPath };
secretManager.RunInternal(args);
Assert.Equal(1, _logger.Messages.Count);
Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
}
[Fact]
@ -157,17 +152,14 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var secretManager = CreateProgram();
secretManager.RunInternal("set", "secret1", "value1", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count);
Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _logger.Messages);
Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _output.ToString());
secretManager.RunInternal("set", "secret1", "value2", "-p", projectPath);
Assert.Equal(2, _logger.Messages.Count);
Assert.Contains("Successfully saved secret1 = value2 to the secret store.", _logger.Messages);
Assert.Contains("Successfully saved secret1 = value2 to the secret store.", _output.ToString());
_logger.Messages.Clear();
_output.Clear();
secretManager.RunInternal("list", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count);
Assert.Contains("secret1 = value2", _logger.Messages);
Assert.Contains("secret1 = value2", _output.ToString());
}
[Fact]
@ -175,22 +167,19 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
{
string secretId;
var projectPath = _fixture.GetTempSecretProject(out secretId);
_logger.SetLevel(LogLevel.Debug);
var secretManager = CreateProgram();
secretManager.RunInternal("-v", "set", "secret1", "value1", "-p", projectPath);
Assert.Equal(3, _logger.Messages.Count);
Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _logger.Messages);
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _logger.Messages);
Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _logger.Messages);
_logger.Messages.Clear();
Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _output.ToString());
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _output.ToString());
Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _output.ToString());
_output.Clear();
secretManager.RunInternal("-v", "list", "-p", projectPath);
Assert.Equal(3, _logger.Messages.Count);
Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _logger.Messages);
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _logger.Messages);
Assert.Contains("secret1 = value1", _logger.Messages);
Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _output.ToString());
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _output.ToString());
Assert.Contains("secret1 = value1", _output.ToString());
}
[Fact]
@ -199,8 +188,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var projectPath = _fixture.GetTempSecretProject();
var secretManager = CreateProgram();
secretManager.RunInternal("remove", "secret1", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count);
Assert.Contains("Cannot find 'secret1' in the secret store.", _logger.Messages);
Assert.Contains("Cannot find 'secret1' in the secret store.", _output.ToString());
}
[Fact]
@ -210,14 +198,13 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var secretManager = CreateProgram();
secretManager.RunInternal("set", "SeCreT1", "value", "-p", projectPath);
secretManager.RunInternal("list", "-p", projectPath);
Assert.Contains("SeCreT1 = value", _logger.Messages);
Assert.Contains("SeCreT1 = value", _output.ToString());
secretManager.RunInternal("remove", "secret1", "-p", projectPath);
Assert.Equal(2, _logger.Messages.Count);
_logger.Messages.Clear();
_output.Clear();
secretManager.RunInternal("list", "-p", projectPath);
Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
}
[Fact]
@ -230,23 +217,20 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count);
Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _logger.Messages);
Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _output.ToString());
}
[Fact]
public void List_Json()
{
var output = new StringBuilder();
_console.Out = new StringWriter(output);
string id;
var projectPath = _fixture.GetTempSecretProject(out id);
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id);
Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
var secretManager = new Program(_console, Path.GetDirectoryName(projectPath)) { Logger = _logger };
var secretManager = new Program(_console, Path.GetDirectoryName(projectPath));
secretManager.RunInternal("list", "--id", id, "--json");
var stdout = output.ToString();
var stdout = _output.ToString();
Assert.Contains("//BEGIN", stdout);
Assert.Contains(@"""AzureAd:ClientSecret"": ""abcd郩˙î""", stdout);
Assert.Contains("//END", stdout);
@ -262,11 +246,9 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
var secretManager = CreateProgram();
secretManager.RunInternal("set", "AzureAd:ClientSecret", "¡™£¢∞", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count);
secretManager.RunInternal("list", "-p", projectPath);
Assert.Equal(2, _logger.Messages.Count);
Assert.Contains("AzureAd:ClientSecret = ¡™£¢∞", _logger.Messages);
Assert.Contains("AzureAd:ClientSecret = ¡™£¢∞", _output.ToString());
var fileContents = File.ReadAllText(secretsFile, Encoding.UTF8);
Assert.Equal(@"{
""AzureAd:ClientSecret"": ""¡£¢""
@ -280,8 +262,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var projectPath = _fixture.GetTempSecretProject();
var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count);
Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
}
[Theory]
@ -295,7 +276,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
? projectPath
: Path.GetTempPath();
var secretManager = new Program(_console, dir) { Logger = _logger };
var secretManager = new Program(_console, dir);
var secrets = new KeyValuePair<string, string>[]
{
@ -313,39 +294,34 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
secretManager.RunInternal(parameters);
}
Assert.Equal(4, _logger.Messages.Count);
foreach (var keyValue in secrets)
{
Assert.Contains(
string.Format("Successfully saved {0} = {1} to the secret store.", keyValue.Key, keyValue.Value),
_logger.Messages);
_output.ToString());
}
// Verify secrets are persisted.
_logger.Messages.Clear();
_output.Clear();
var args = fromCurrentDirectory ?
new string[] { "list" } :
new string[] { "list", "-p", projectPath };
secretManager.RunInternal(args);
Assert.Equal(4, _logger.Messages.Count);
foreach (var keyValue in secrets)
{
Assert.Contains(
string.Format("{0} = {1}", keyValue.Key, keyValue.Value),
_logger.Messages);
_output.ToString());
}
// Clear secrets.
_logger.Messages.Clear();
_output.Clear();
args = fromCurrentDirectory ? new string[] { "clear" } : new string[] { "clear", "-p", projectPath };
secretManager.RunInternal(args);
Assert.Equal(0, _logger.Messages.Count);
args = fromCurrentDirectory ? new string[] { "list" } : new string[] { "list", "-p", projectPath };
secretManager.RunInternal(args);
Assert.Equal(1, _logger.Messages.Count);
Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
}
}
}

View File

@ -1,11 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Collections.Generic;
using Microsoft.Extensions.SecretManager.Tools.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
using Xunit;
using Xunit.Abstractions;
@ -36,10 +34,10 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
IsInputRedirected = true,
In = new StringReader(input)
};
var secretStore = new TestSecretsStore();
var secretStore = new TestSecretsStore(_output);
var command = new SetCommand.FromStdInStrategy();
command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole));
command.Execute(new CommandContext(secretStore, new TestReporter(_output), testConsole));
Assert.Equal(3, secretStore.Count);
Assert.Equal("str value", secretStore["Key1"]);
@ -63,10 +61,10 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
IsInputRedirected = true,
In = new StringReader(input)
};
var secretStore = new TestSecretsStore();
var secretStore = new TestSecretsStore(_output);
var command = new SetCommand.FromStdInStrategy();
command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole));
command.Execute(new CommandContext(secretStore, new TestReporter(_output), testConsole));
Assert.Equal(3, secretStore.Count);
Assert.True(secretStore.ContainsKey("Key1:nested"));
@ -89,8 +87,8 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
private class TestSecretsStore : SecretsStore
{
public TestSecretsStore()
: base("xyz", NullLogger.Instance)
public TestSecretsStore(ITestOutputHelper output)
: base("xyz", new TestReporter(output))
{
}
@ -105,25 +103,4 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
}
}
}
public class NullLogger : ILogger
{
public static NullLogger Instance = new NullLogger();
private class NullScope : IDisposable
{
public void Dispose()
{
}
}
public IDisposable BeginScope<TState>(TState state)
=> new NullScope();
public bool IsEnabled(LogLevel logLevel)
=> true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
}
}
}

View File

@ -1,50 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Microsoft.Extensions.SecretManager.Tools.Tests
{
public class TestLogger : ILogger
{
private readonly ILogger _wrapped;
private readonly ITestOutputHelper _output;
public TestLogger(ITestOutputHelper output = null)
{
CommandOutputProvider = new CommandOutputProvider();
_wrapped = CommandOutputProvider.CreateLogger("");
_output = output;
}
public CommandOutputProvider CommandOutputProvider { get;}
public void SetLevel(LogLevel level)
{
CommandOutputProvider.LogLevel = level;
}
public List<string> Messages { get; set; } = new List<string>();
public IDisposable BeginScope<TState>(TState state)
{
throw new NotImplementedException();
}
public bool IsEnabled(LogLevel logLevel)
{
return _wrapped.IsEnabled(logLevel);
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (IsEnabled(logLevel))
{
Messages.Add(formatter(state, exception));
_output?.WriteLine(formatter(state, exception));
}
}
}
}

View File

@ -4,7 +4,7 @@
using Microsoft.Extensions.Tools.Internal;
using Xunit;
namespace Microsoft.DotNet.Watcher.Tools.Tests
namespace Microsoft.Extensions.Tools.Tests
{
public class ArgumentEscaperTests
{

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>{A24BF1D1-4326-4455-A528-09F1E20EDC83}</ProjectGuid>
<ProjectTypeGuids>{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}</ProjectTypeGuids>
<RootNamespace>Microsoft.Extensions.Tools.Tests</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,103 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using Microsoft.Extensions.Tools.Internal;
using Xunit;
namespace Microsoft.Extensions.Tools.Tests
{
public class ReporterTests
{
private static readonly string EOL = Environment.NewLine;
[Theory]
[InlineData(ConsoleColor.DarkGray, "\x1B[90m")]
[InlineData(ConsoleColor.Red, "\x1B[91m")]
[InlineData(ConsoleColor.Yellow, "\x1B[93m")]
public void WrapsWithAnsiColorCode(ConsoleColor color, string code)
{
Assert.Equal($"{code}sample\x1B[39m", new ColorFormatter(color).Format("sample"));
}
[Fact]
public void SkipsColorCodesForEmptyOrNullInput()
{
var formatter = new ColorFormatter(ConsoleColor.Blue);
Assert.Empty(formatter.Format(string.Empty));
Assert.Null(formatter.Format(null));
}
[Fact]
public void WritesToStandardStreams()
{
var testConsole = new TestConsole();
var reporter = new FormattingReporter(testConsole,
DefaultFormatter.Instance, DefaultFormatter.Instance,
DefaultFormatter.Instance, DefaultFormatter.Instance);
// stdout
reporter.Verbose("verbose");
Assert.Equal("verbose" + EOL, testConsole.GetOutput());
testConsole.Clear();
reporter.Output("out");
Assert.Equal("out" + EOL, testConsole.GetOutput());
testConsole.Clear();
reporter.Warn("warn");
Assert.Equal("warn" + EOL, testConsole.GetOutput());
testConsole.Clear();
// stderr
reporter.Error("error");
Assert.Equal("error" + EOL, testConsole.GetError());
testConsole.Clear();
}
[Fact]
public void FailsToBuildWithoutConsole()
{
Assert.Throws<InvalidOperationException>(
() => new ReporterBuilder().Build());
}
private class TestConsole : IConsole
{
private readonly StringBuilder _out;
private readonly StringBuilder _error;
public TestConsole()
{
_out = new StringBuilder();
_error = new StringBuilder();
Out = new StringWriter(_out);
Error = new StringWriter(_error);
}
event ConsoleCancelEventHandler IConsole.CancelKeyPress
{
add { }
remove { }
}
public string GetOutput() => _out.ToString();
public string GetError() => _error.ToString();
public void Clear()
{
_out.Clear();
_error.Clear();
}
public TextWriter Out { get; }
public TextWriter Error { get; }
public TextReader In { get; }
public bool IsInputRedirected { get; }
public bool IsOutputRedirected { get; }
public bool IsErrorRedirected { get; }
}
}
}

View File

@ -0,0 +1,23 @@
{
"buildOptions": {
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk",
"compile": "../../src/Shared/**/*.cs"
},
"dependencies": {
"dotnet-test-xunit": "2.2.0-preview2-build1029",
"Microsoft.DotNet.InternalAbstractions": "1.0.500-preview2-1-003177",
"xunit": "2.2.0-beta3-build3402"
},
"testRunner": "xunit",
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.1",
"type": "platform"
}
}
}
}
}

View File

@ -25,6 +25,8 @@ namespace Microsoft.Extensions.Tools.Internal
public TextWriter Out { get; set; }
public TextReader In { get; set; } = new StringReader(string.Empty);
public bool IsInputRedirected { get; set; } = false;
public bool IsOutputRedirected { get; } = false;
public bool IsErrorRedirected { get; } = false;
public ConsoleCancelEventArgs ConsoleCancelKey()
{

View File

@ -0,0 +1,37 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit.Abstractions;
namespace Microsoft.Extensions.Tools.Internal
{
public class TestReporter : IReporter
{
private readonly ITestOutputHelper _output;
public TestReporter(ITestOutputHelper output)
{
_output = output;
}
public void Verbose(string message)
{
_output.WriteLine("verbose: " + message);
}
public void Output(string message)
{
_output.WriteLine("output: " + message);
}
public void Warn(string message)
{
_output.WriteLine("warn: " + message);
}
public void Error(string message)
{
_output.WriteLine("error: " + message);
}
}
}