diff --git a/.gitignore b/.gitignore index 29b5f79c9b..61e62b3028 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,5 @@ project.lock.json .vscode/ testWorkDir/ *.nuget.props -*.nuget.targets \ No newline at end of file +*.nuget.targets +.idea/ diff --git a/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs b/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs index 5c4ac1058a..b148ad6f08 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs @@ -1,9 +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.Collections.Generic; using System.IO; -using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Watcher.Tools; using Microsoft.DotNet.Watcher.Internal; using Microsoft.Extensions.CommandLineUtils; diff --git a/src/Microsoft.DotNet.Watcher.Tools/DotNetWatcher.cs b/src/Microsoft.DotNet.Watcher.Tools/DotNetWatcher.cs index 58756c33cd..5ee3d8d945 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/DotNetWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/DotNetWatcher.cs @@ -5,6 +5,7 @@ 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 { @@ -45,6 +46,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 finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task); // Regardless of the which task finished first, make sure everything is cancelled diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs index f40a43732a..e332ad88f5 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs @@ -7,11 +7,11 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.Watcher.Tools; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Internal { @@ -23,6 +23,7 @@ namespace Microsoft.DotNet.Watcher.Internal 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()) @@ -40,6 +41,7 @@ namespace Microsoft.DotNet.Watcher.Internal _projectFile = projectFile; _watchTargetsDir = FindWatchTargetsDir(); _outputSink = outputSink; + _processRunner = new ProcessRunner(logger); } internal List BuildFlags { get; } = new List @@ -71,20 +73,21 @@ namespace Microsoft.DotNet.Watcher.Internal var capture = _outputSink.StartCapture(); // TODO adding files doesn't currently work. Need to provide a way to detect new files // find files - var exitCode = Command.CreateDotNet("msbuild", - new[] + var processSpec = new ProcessSpec + { + Executable = DotNetMuxer.MuxerPathOrDefault(), + WorkingDirectory = projectDir, + Arguments = new[] { - _projectFile, + "msbuild", + _projectFile, $"/p:_DotNetWatchTargetsLocation={_watchTargetsDir}", // add our dotnet-watch targets - $"/p:_DotNetWatchListFile={watchList}", - }.Concat(BuildFlags)) - .CaptureStdErr() - .CaptureStdOut() - .OnErrorLine(l => capture.WriteErrorLine(l)) - .OnOutputLine(l => capture.WriteOutputLine(l)) - .WorkingDirectory(projectDir) - .Execute() - .ExitCode; + $"/p:_DotNetWatchListFile={watchList}" + }.Concat(BuildFlags), + OutputCapture = capture + }; + + var exitCode = await _processRunner.RunAsync(processSpec, cancellationToken); if (exitCode == 0) { @@ -104,9 +107,18 @@ namespace Microsoft.DotNet.Watcher.Internal return fileset; } - _logger.LogError($"Error(s) finding watch items project file '{Path.GetFileName(_projectFile)}': "); - _logger.LogError(capture.GetAllLines("[MSBUILD] : ")); - _logger.LogInformation("Fix the error to continue."); + var sb = new StringBuilder() + .Append("Error(s) finding watch items project file '") + .Append(Path.GetFileName(_projectFile)) + .AppendLine("' :"); + + foreach (var line in capture.Lines) + { + sb.Append(" [MSBUILD] :").AppendLine(line); + } + + _logger.LogError(sb.ToString()); + _logger.LogInformation("Fix the error to continue or press Ctrl+C to exit."); var fileSet = new FileSet(new[] { _projectFile }); diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildProjectFinder.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildProjectFinder.cs index ff3f45b66b..2eefb9a891 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildProjectFinder.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildProjectFinder.cs @@ -4,7 +4,6 @@ using System; using System.IO; using System.Linq; -using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Watcher.Tools; namespace Microsoft.DotNet.Watcher.Internal @@ -35,12 +34,12 @@ namespace Microsoft.DotNet.Watcher.Internal if (projects.Count > 1) { - throw new GracefulException(Resources.FormatError_MultipleProjectsFound(projectPath)); + throw new FileNotFoundException(Resources.FormatError_MultipleProjectsFound(projectPath)); } if (projects.Count == 0) { - throw new GracefulException(Resources.FormatError_NoProjectsFound(projectPath)); + throw new FileNotFoundException(Resources.FormatError_NoProjectsFound(projectPath)); } return projects[0]; @@ -48,7 +47,7 @@ namespace Microsoft.DotNet.Watcher.Internal if (!File.Exists(projectPath)) { - throw new GracefulException(Resources.FormatError_ProjectPath_NotFound(projectPath)); + throw new FileNotFoundException(Resources.FormatError_ProjectPath_NotFound(projectPath)); } return projectPath; diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/OutputCapture.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/OutputCapture.cs new file mode 100644 index 0000000000..08e051f732 --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/OutputCapture.cs @@ -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. + +using System.Collections.Generic; + +namespace Microsoft.DotNet.Watcher.Internal +{ + public class OutputCapture + { + private readonly List _lines = new List(); + public IEnumerable Lines => _lines; + public void AddLine(string line) => _lines.Add(line); + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/OutputSink.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/OutputSink.cs index aa764437ad..eb176564ef 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/OutputSink.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/OutputSink.cs @@ -1,27 +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. -using System; -using System.Collections.Generic; -using System.Linq; - namespace Microsoft.DotNet.Watcher.Internal { - internal class OutputSink + public class OutputSink { public OutputCapture Current { get; private set; } public OutputCapture StartCapture() { return (Current = new OutputCapture()); } - - public class OutputCapture - { - private readonly List _lines = new List(); - public IEnumerable Lines => _lines; - public void WriteOutputLine(string line) => _lines.Add(line); - public void WriteErrorLine(string line) => _lines.Add(line); - public string GetAllLines(string prefix) => string.Join(Environment.NewLine, _lines.Select(l => prefix + l)); - } } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/ProcessRunner.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/ProcessRunner.cs index f8d9260039..39f302c71e 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/ProcessRunner.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/ProcessRunner.cs @@ -3,11 +3,12 @@ using System; using System.Diagnostics; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Internal; -using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Internal { @@ -35,30 +36,42 @@ namespace Microsoft.DotNet.Watcher.Internal cancellationToken.Register(() => processState.TryKill()); process.Start(); - _logger.LogInformation("{execName} process id: {pid}", processSpec.ShortDisplayName(), process.Id); - await processState.Task; + if (processSpec.IsOutputCaptured) + { + await Task.WhenAll( + processState.Task, + ConsumeStreamAsync(process.StandardOutput, processSpec.OutputCapture.AddLine), + ConsumeStreamAsync(process.StandardError, processSpec.OutputCapture.AddLine) + ); + } + else + { + _logger.LogInformation("{execName} process id: {pid}", processSpec.ShortDisplayName(), process.Id); + await processState.Task; + } exitCode = process.ExitCode; } - LogResult(processSpec, exitCode); + if (!processSpec.IsOutputCaptured) + { + LogResult(processSpec, exitCode); + } return exitCode; } private Process CreateProcess(ProcessSpec processSpec) { - var arguments = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(processSpec.Arguments); - - _logger.LogInformation("Running {execName} with the following arguments: {args}", processSpec.ShortDisplayName(), arguments); - var startInfo = new ProcessStartInfo { FileName = processSpec.Executable, - Arguments = arguments, + Arguments = ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments), UseShellExecute = false, - WorkingDirectory = processSpec.WorkingDirectory + WorkingDirectory = processSpec.WorkingDirectory, + RedirectStandardOutput = processSpec.IsOutputCaptured, + RedirectStandardError = processSpec.IsOutputCaptured, }; var process = new Process { @@ -81,6 +94,15 @@ namespace Microsoft.DotNet.Watcher.Internal } } + private static async Task ConsumeStreamAsync(StreamReader reader, Action consume) + { + string line; + while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) + { + consume?.Invoke(line); + } + } + private class ProcessState : IDisposable { private readonly Process _process; diff --git a/src/Microsoft.DotNet.Watcher.Tools/ProcessSpec.cs b/src/Microsoft.DotNet.Watcher.Tools/ProcessSpec.cs index 2d18b9a0be..4b1508e5e2 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/ProcessSpec.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/ProcessSpec.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using Microsoft.DotNet.Watcher.Internal; namespace Microsoft.DotNet.Watcher { @@ -11,8 +12,11 @@ namespace Microsoft.DotNet.Watcher public string Executable { get; set; } public string WorkingDirectory { get; set; } public IEnumerable Arguments { get; set; } + public OutputCapture OutputCapture { get; set; } - public string ShortDisplayName() + public string ShortDisplayName() => Path.GetFileNameWithoutExtension(Executable); + + public bool IsOutputCaptured => OutputCapture != null; } } diff --git a/src/Microsoft.DotNet.Watcher.Tools/Program.cs b/src/Microsoft.DotNet.Watcher.Tools/Program.cs index 06cedfa8cf..bb1bbfd828 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Program.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Program.cs @@ -2,12 +2,14 @@ // 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.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Watcher.Internal; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher { @@ -33,7 +35,7 @@ namespace Microsoft.DotNet.Watcher public static int Main(string[] args) { - DebugHelper.HandleDebugSwitch(ref args); + HandleDebugSwitch(ref args); using (CancellationTokenSource ctrlCTokenSource = new CancellationTokenSource()) { @@ -41,7 +43,7 @@ namespace Microsoft.DotNet.Watcher { if (!ctrlCTokenSource.IsCancellationRequested) { - Console.WriteLine($"[{LoggerName}] Shutdown requested. Press CTRL+C again to force exit."); + Console.WriteLine($"[{LoggerName}] Shutdown requested. Press Ctrl+C again to force exit."); ev.Cancel = true; } else @@ -96,12 +98,22 @@ namespace Microsoft.DotNet.Watcher var logger = loggerFactory.CreateLogger(LoggerName); // TODO multiple projects should be easy enough to add here - var projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, options.Project); + string projectFile; + try + { + projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, options.Project); + } + catch (FileNotFoundException ex) + { + _stderr.WriteLine(ex.Message.Bold().Red()); + return 1; + } + var fileSetFactory = new MsBuildFileSetFactory(logger, projectFile); var processInfo = new ProcessSpec { - Executable = new Muxer().MuxerPath, + Executable = DotNetMuxer.MuxerPathOrDefault(), WorkingDirectory = Path.GetDirectoryName(projectFile), Arguments = options.RemainingArguments }; @@ -120,7 +132,7 @@ namespace Microsoft.DotNet.Watcher } bool globalVerbose; - bool.TryParse(Environment.GetEnvironmentVariable(CommandContext.Variables.Verbose), out globalVerbose); + bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE"), out globalVerbose); if (options.IsVerbose // dotnet watch --verbose || globalVerbose) // dotnet --verbose watch @@ -130,5 +142,17 @@ namespace Microsoft.DotNet.Watcher return LogLevel.Information; } + + [Conditional("DEBUG")] + private static void HandleDebugSwitch(ref string[] args) + { + if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase)) + { + args = args.Skip(1).ToArray(); + Console.WriteLine("Waiting for debugger to attach. Press ENTER to continue"); + Console.WriteLine($"Process ID: {Process.GetCurrentProcess().Id}"); + Console.ReadLine(); + } + } } } diff --git a/src/Microsoft.DotNet.Watcher.Tools/project.json b/src/Microsoft.DotNet.Watcher.Tools/project.json index 57860301d6..d53d354b50 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/project.json +++ b/src/Microsoft.DotNet.Watcher.Tools/project.json @@ -32,7 +32,6 @@ ] }, "dependencies": { - "Microsoft.DotNet.Cli.Utils": "1.0.0-preview3-004056", "Microsoft.Extensions.Logging": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.Extensions.Process.Sources": { diff --git a/src/Shared/ArgumentEscaper.cs b/src/Shared/ArgumentEscaper.cs new file mode 100644 index 0000000000..91d8ef3086 --- /dev/null +++ b/src/Shared/ArgumentEscaper.cs @@ -0,0 +1,107 @@ +// 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.Linq; +using System.Text; + +namespace Microsoft.Extensions.Tools.Internal +{ + public static class ArgumentEscaper + { + /// + /// Undo the processing which took place to create string[] args in Main, so that the next process will + /// receive the same string[] args. + /// + /// + /// See https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ + /// + /// + /// + public static string EscapeAndConcatenate(IEnumerable args) + => string.Join(" ", args.Select(EscapeSingleArg)); + + private static string EscapeSingleArg(string arg) + { + var sb = new StringBuilder(); + + var needsQuotes = ShouldSurroundWithQuotes(arg); + var isQuoted = needsQuotes || IsSurroundedWithQuotes(arg); + + if (needsQuotes) + { + sb.Append('"'); + } + + for (int i = 0; i < arg.Length; ++i) + { + var backslashes = 0; + + // Consume all backslashes + while (i < arg.Length && arg[i] == '\\') + { + backslashes++; + i++; + } + + if (i == arg.Length && isQuoted) + { + // Escape any backslashes at the end of the arg when the argument is also quoted. + // This ensures the outside quote is interpreted as an argument delimiter + sb.Append('\\', 2 * backslashes); + } + else if (i == arg.Length) + { + // At then end of the arg, which isn't quoted, + // just add the backslashes, no need to escape + sb.Append('\\', backslashes); + } + else if (arg[i] == '"') + { + // Escape any preceding backslashes and the quote + sb.Append('\\', (2 * backslashes) + 1); + sb.Append('"'); + } + else + { + // Output any consumed backslashes and the character + sb.Append('\\', backslashes); + sb.Append(arg[i]); + } + } + + if (needsQuotes) + { + sb.Append('"'); + } + + return sb.ToString(); + } + + private static bool ShouldSurroundWithQuotes(string argument) + { + // Don't quote already quoted strings + if (IsSurroundedWithQuotes(argument)) + { + return false; + } + + // Only quote if whitespace exists in the string + return ContainsWhitespace(argument); + } + + private static bool IsSurroundedWithQuotes(string argument) + { + if (argument.Length <= 1) + { + return false; + } + + return argument[0] == '"' && argument[argument.Length - 1] == '"'; + } + + private static bool ContainsWhitespace(string argument) + => argument.IndexOfAny(new [] { ' ', '\t', '\n' }) >= 0; + } +} \ No newline at end of file diff --git a/src/Shared/DotNetMuxer.cs b/src/Shared/DotNetMuxer.cs new file mode 100644 index 0000000000..56e627f192 --- /dev/null +++ b/src/Shared/DotNetMuxer.cs @@ -0,0 +1,56 @@ +// 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.Runtime.InteropServices; + +namespace Microsoft.Extensions.Tools.Internal +{ + public static class DotNetMuxer + { + private const string MuxerName = "dotnet"; + + static DotNetMuxer() + { + MuxerPath = TryFindMuxerPath(); + } + + public static string MuxerPath { get; } + + public static string MuxerPathOrDefault() + => MuxerPath ?? MuxerName; + + private static string TryFindMuxerPath() + { + var fileName = MuxerName; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fileName += ".exe"; + } + + var fxDepsFile = AppContext.GetData("FX_DEPS_FILE") as string; + + if (string.IsNullOrEmpty(fxDepsFile)) + { + return null; + } + + var muxerDir = new FileInfo(fxDepsFile) // Microsoft.NETCore.App.deps.json + .Directory? // (version) + .Parent? // Microsoft.NETCore.App + .Parent? // shared + .Parent; // DOTNET_HOME + + if (muxerDir == null) + { + return null; + } + + var muxer = Path.Combine(muxerDir.FullName, fileName); + return File.Exists(muxer) + ? muxer + : null; + } + } +} \ No newline at end of file diff --git a/src/Shared/StringExtensions.cs b/src/Shared/StringExtensions.cs new file mode 100644 index 0000000000..fdb087cfcd --- /dev/null +++ b/src/Shared/StringExtensions.cs @@ -0,0 +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. + +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"; + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/project.json b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/project.json index 645d6f7de4..4e28d24c2f 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/project.json +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/project.json @@ -11,6 +11,7 @@ }, "dependencies": { "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Microsoft.DotNet.Cli.Utils": "1.0.0-preview3-004056", "Microsoft.DotNet.InternalAbstractions": "1.0.0", "Microsoft.AspNetCore.Testing": "1.0.0", "Microsoft.DotNet.Watcher.Tools": "1.0.0-*", diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/ArgumentEscaperTests.cs b/test/Microsoft.DotNet.Watcher.Tools.Tests/ArgumentEscaperTests.cs new file mode 100644 index 0000000000..70c18b584c --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/ArgumentEscaperTests.cs @@ -0,0 +1,23 @@ +// 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.Tools.Internal; +using Xunit; + +namespace Microsoft.DotNet.Watcher.Tools.Tests +{ + public class ArgumentEscaperTests + { + [Theory] + [InlineData(new[] { "one", "two", "three" }, "one two three")] + [InlineData(new[] { "line1\nline2", "word1\tword2" }, "\"line1\nline2\" \"word1\tword2\"")] + [InlineData(new[] { "with spaces" }, "\"with spaces\"")] + [InlineData(new[] { @"with\backslash" }, @"with\backslash")] + [InlineData(new[] { @"""quotedwith\backslash""" }, @"\""quotedwith\backslash\""")] + [InlineData(new[] { @"C:\Users\" }, @"C:\Users\")] + [InlineData(new[] { @"C:\Program Files\dotnet\" }, @"""C:\Program Files\dotnet\\""")] + [InlineData(new[] { @"backslash\""preceedingquote" }, @"backslash\\\""preceedingquote")] + public void EscapesArguments(string[] args, string expected) + => Assert.Equal(expected, ArgumentEscaper.EscapeAndConcatenate(args)); + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/CommandLineOptionsTests.cs b/test/Microsoft.DotNet.Watcher.Tools.Tests/CommandLineOptionsTests.cs index be63a361fc..2957dfe17a 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.Tests/CommandLineOptionsTests.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/CommandLineOptionsTests.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Text; using Xunit; -namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests +namespace Microsoft.DotNet.Watcher.Tools.Tests { public class CommandLineOptionsTests { diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/MsBuildFileSetFactoryTest.cs b/test/Microsoft.DotNet.Watcher.Tools.Tests/MsBuildFileSetFactoryTest.cs index 555be13c70..d236d0078c 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.Tests/MsBuildFileSetFactoryTest.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/MsBuildFileSetFactoryTest.cs @@ -264,7 +264,9 @@ namespace Microsoft.DotNetWatcher.Tools.Tests var fileset = await GetFileSet(filesetFactory); - _logger.LogInformation(output.Current.GetAllLines("Sink output: ")); + _logger.LogInformation(string.Join( + Environment.NewLine, + output.Current.Lines.Select(l => "Sink output: " + l))); var includedProjects = new[] { "A", "B", "C", "D", "E", "F", "G" }; AssertEx.EqualFileList(