diff --git a/.travis.yml b/.travis.yml index 1d6d4144bc..93fdc0133c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,6 @@ mono: os: - linux - osx -osx_image: xcode7.1 branches: only: - master diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/AppWithDepsTests.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/AppWithDepsTests.cs index 9e17328c69..253f3643d1 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/AppWithDepsTests.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/AppWithDepsTests.cs @@ -3,80 +3,51 @@ using System; using System.IO; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { - public class AppWithDepsTests + public class AppWithDepsTests : IDisposable { - private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); - - private readonly ITestOutputHelper _logger; + private readonly AppWithDeps _app; public AppWithDepsTests(ITestOutputHelper logger) { - _logger = logger; + _app = new AppWithDeps(logger); + _app.Prepare(); } - // Change a file included in compilation [Fact] - public void ChangeFileInDependency() + public async Task ChangeFileInDependency() { - using (var scenario = new AppWithDepsScenario(_logger)) - { - scenario.Start(); - using (var wait = new WaitForFileToChange(scenario.StartedFile)) - { - var fileToChange = Path.Combine(scenario.DependencyFolder, "Foo.cs"); - var programCs = File.ReadAllText(fileToChange); - File.WriteAllText(fileToChange, programCs); + await _app.StartWatcher().OrTimeout(); - wait.Wait(_defaultTimeout, - expectedToChange: true, - errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); - } - } + var fileToChange = Path.Combine(_app.DependencyFolder, "Foo.cs"); + var programCs = File.ReadAllText(fileToChange); + File.WriteAllText(fileToChange, programCs); + + await _app.HasRestarted().OrTimeout(); } - private class AppWithDepsScenario : DotNetWatchScenario + public void Dispose() + { + _app.Dispose(); + } + + private class AppWithDeps : WatchableApp { - private const string AppWithDeps = "AppWithDeps"; private const string Dependency = "Dependency"; - public AppWithDepsScenario(ITestOutputHelper logger) - : base(logger) + public AppWithDeps(ITestOutputHelper logger) + : base("AppWithDeps", logger) { - StatusFile = Path.Combine(Scenario.TempFolder, "status"); - StartedFile = StatusFile + ".started"; - - Scenario.AddTestProjectFolder(AppWithDeps); Scenario.AddTestProjectFolder(Dependency); - Scenario.Restore(AppWithDeps); // restore3 should be transitive - - AppWithDepsFolder = Path.Combine(Scenario.WorkFolder, AppWithDeps); DependencyFolder = Path.Combine(Scenario.WorkFolder, Dependency); } - public void Start() - { - // Wait for the process to start - using (var wait = new WaitForFileToChange(StatusFile)) - { - RunDotNetWatch(new[] { "run", StatusFile }, Path.Combine(Scenario.WorkFolder, AppWithDeps)); - - wait.Wait(_defaultTimeout, - expectedToChange: true, - errorMessage: $"File not created: {StatusFile}"); - } - - Waiters.WaitForFileToBeReadable(StatusFile, _defaultTimeout); - } - - public string StatusFile { get; private set; } - public string StartedFile { get; private set; } - public string AppWithDepsFolder { get; private set; } public string DependencyFolder { get; private set; } } } diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/AwaitableProcess.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/AwaitableProcess.cs new file mode 100644 index 0000000000..fc920e7eb0 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/AwaitableProcess.cs @@ -0,0 +1,102 @@ +// 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.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Internal; +using Xunit.Abstractions; + +namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests +{ + public class AwaitableProcess : IDisposable + { + private Process _process; + private readonly ProcessSpec _spec; + private BufferBlock _source; + private ITestOutputHelper _logger; + private int _reading; + + public AwaitableProcess(ProcessSpec spec, ITestOutputHelper logger) + { + _spec = spec; + _logger = logger; + } + + public void Start() + { + if (_process != null) + { + throw new InvalidOperationException("Already started"); + } + + var psi = new ProcessStartInfo + { + UseShellExecute = false, + FileName = _spec.Executable, + WorkingDirectory = _spec.WorkingDirectory, + Arguments = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(_spec.Arguments), + RedirectStandardOutput = true, + RedirectStandardError = true + }; + _process = Process.Start(psi); + _logger.WriteLine($"{DateTime.Now}: process start: '{psi.FileName} {psi.Arguments}'"); + StartProcessingOutput(_process.StandardOutput); + StartProcessingOutput(_process.StandardError);; + } + + public Task GetOutputLineAsync(string message) + => GetOutputLineAsync(m => message == m); + + public async Task GetOutputLineAsync(Predicate predicate) + { + while (!_source.Completion.IsCompleted) + { + while (await _source.OutputAvailableAsync()) + { + var next = await _source.ReceiveAsync(); + _logger.WriteLine($"{DateTime.Now}: recv: '{next}'"); + if (predicate(next)) + { + return next; + } + } + } + + return null; + } + + private void StartProcessingOutput(StreamReader streamReader) + { + _source = _source ?? new BufferBlock(); + Interlocked.Increment(ref _reading); + Task.Run(() => + { + string line; + while ((line = streamReader.ReadLine()) != null) + { + _logger.WriteLine($"{DateTime.Now} post: {line}"); + _source.Post(line); + } + + if (Interlocked.Decrement(ref _reading) <= 0) + { + _source.Complete(); + } + }).ConfigureAwait(false); + } + + public void Dispose() + { + if (_process != null && !_process.HasExited) + { + _process.KillTree(); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/GlobbingAppTests.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/GlobbingAppTests.cs index 912b243115..a6ccfaa91c 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/GlobbingAppTests.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/GlobbingAppTests.cs @@ -2,227 +2,134 @@ // 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.Threading; +using System.Linq; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { - public class GlobbingAppTests + public class GlobbingAppTests : IDisposable { - private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); - - private static readonly TimeSpan _negativeTestWaitTime = TimeSpan.FromSeconds(10); - - private readonly ITestOutputHelper _logger; - + private GlobbingApp _app; public GlobbingAppTests(ITestOutputHelper logger) { - _logger = logger; + _app = new GlobbingApp(logger); + _app.Prepare(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ChangeCompiledFile(bool usePollingWatcher) + { + await _app.StartWatcher().OrTimeout(); + + var types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); + Assert.Equal(2, types); + + var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs"); + var programCs = File.ReadAllText(fileToChange); + File.WriteAllText(fileToChange, programCs); + + await _app.HasRestarted().OrTimeout(); + types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); + Assert.Equal(2, types); + } + + [Fact(Skip = "Broken. See https://github.com/aspnet/DotNetTools/issues/212")] + public async Task AddCompiledFile() + { + await _app.StartWatcher().OrTimeout(); + + var types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); + Assert.Equal(2, types); + + var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Bar.cs"); + File.WriteAllText(fileToChange, "public class Bar {}"); + + await _app.HasRestarted().OrTimeout(); + types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); + Assert.Equal(3, types); + } + + // TODO re-enable when MSBuild is updated. See https://github.com/aspnet/DotNetTools/issues/224 + [Fact(Skip = "Broken. See https://github.com/Microsoft/msbuild/issues/701")] + public async Task DeleteCompiledFile() + { + await _app.StartWatcher().OrTimeout(); + + var types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); + Assert.Equal(2, types); + + var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs"); + File.Delete(fileToChange); + + await _app.HasRestarted().OrTimeout(); + types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); + Assert.Equal(1, types); + } + + // TODO re-enable when MSBuild is updated. See https://github.com/aspnet/DotNetTools/issues/224 + [Fact(Skip = "Broken. See https://github.com/Microsoft/msbuild/issues/701")] + public async Task DeleteSourceFolder() + { + await _app.StartWatcher().OrTimeout(); + + var types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); + Assert.Equal(2, types); + + var folderToDelete = Path.Combine(_app.SourceDirectory, "include"); + Directory.Delete(folderToDelete, recursive: true); + + await _app.HasRestarted().OrTimeout(); + types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); + Assert.Equal(1, types); } [Fact] - public void ChangeCompiledFile_PollingWatcher() + public async Task RenameCompiledFile() { - ChangeCompiledFile(usePollingWatcher: true); + await _app.StartWatcher().OrTimeout(); + + var oldFile = Path.Combine(_app.SourceDirectory, "include", "Foo.cs"); + var newFile = Path.Combine(_app.SourceDirectory, "include", "Foo_new.cs"); + File.Move(oldFile, newFile); + + await _app.HasRestarted().OrTimeout(); } [Fact] - public void ChangeCompiledFile_DotNetWatcher() + public async Task ChangeExcludedFile() { - ChangeCompiledFile(usePollingWatcher: false); + await _app.StartWatcher().OrTimeout(); + + var changedFile = Path.Combine(_app.SourceDirectory, "exclude", "Baz.cs"); + File.WriteAllText(changedFile, ""); + + var restart = _app.HasRestarted(); + var finished = await Task.WhenAny(Task.Delay(TimeSpan.FromSeconds(10)), restart); + Assert.NotSame(restart, finished); } - // Change a file included in compilation - private void ChangeCompiledFile(bool usePollingWatcher) + public void Dispose() { - using (var scenario = new GlobbingAppScenario(_logger)) - using (var wait = new WaitForFileToChange(scenario.StartedFile)) + _app.Dispose(); + } + + private class GlobbingApp : WatchableApp + { + public GlobbingApp(ITestOutputHelper logger) + : base("GlobbingApp", logger) { - scenario.UsePollingWatcher = usePollingWatcher; - - scenario.Start(); - - var fileToChange = Path.Combine(scenario.TestAppFolder, "include", "Foo.cs"); - var programCs = File.ReadAllText(fileToChange); - File.WriteAllText(fileToChange, programCs); - - wait.Wait(_defaultTimeout, - expectedToChange: true, - errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); } - } - - // Add a file to a folder included in compilation - [Fact] - public void AddCompiledFile() - { - // Add a file in a folder that's included in compilation - using (var scenario = new GlobbingAppScenario(_logger)) - using (var wait = new WaitForFileToChange(scenario.StartedFile)) + public async Task GetCompiledAppDefinedTypes() { - scenario.Start(); - - var fileToChange = Path.Combine(scenario.TestAppFolder, "include", "Bar.cs"); - File.WriteAllText(fileToChange, ""); - - wait.Wait(_defaultTimeout, - expectedToChange: true, - errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); + var definedTypesMessage = await Process.GetOutputLineAsync(m => m.StartsWith("Defined types = ")); + return int.Parse(definedTypesMessage.Split('=').Last()); } } - - // Delete a file included in compilation - [Fact] - public void DeleteCompiledFile() - { - using (var scenario = new GlobbingAppScenario(_logger)) - using (var wait = new WaitForFileToChange(scenario.StartedFile)) - { - scenario.Start(); - - var fileToChange = Path.Combine(scenario.TestAppFolder, "include", "Foo.cs"); - File.Delete(fileToChange); - - wait.Wait(_defaultTimeout, - expectedToChange: true, - errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); - } - } - - // Delete an entire folder - [Fact] - public void DeleteSourceFolder() - { - using (var scenario = new GlobbingAppScenario(_logger)) - using (var wait = new WaitForFileToChange(scenario.StartedFile)) - { - scenario.Start(); - - var folderToDelete = Path.Combine(scenario.TestAppFolder, "include"); - Directory.Delete(folderToDelete, recursive: true); - - wait.Wait(_defaultTimeout, - expectedToChange: true, - errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); - } - } - - // Rename a file included in compilation - [Fact] - public void RenameCompiledFile() - { - using (var scenario = new GlobbingAppScenario(_logger)) - using (var wait = new WaitForFileToChange(scenario.StatusFile)) - { - scenario.Start(); - - var oldFile = Path.Combine(scenario.TestAppFolder, "include", "Foo.cs"); - var newFile = Path.Combine(scenario.TestAppFolder, "include", "Foo_new.cs"); - File.Move(oldFile, newFile); - - wait.Wait(_defaultTimeout, - expectedToChange: true, - errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); - } - } - - [Fact] - public void ChangeNonCompiledFile_PollingWatcher() - { - ChangeNonCompiledFile(usePollingWatcher: true); - } - - [Fact] - public void ChangeNonCompiledFile_DotNetWatcher() - { - ChangeNonCompiledFile(usePollingWatcher: false); - } - - // Add a file that's in a included folder but not matching the globbing pattern - private void ChangeNonCompiledFile(bool usePollingWatcher) - { - using (var scenario = new GlobbingAppScenario(_logger)) - { - scenario.UsePollingWatcher = usePollingWatcher; - - scenario.Start(); - - var ids = File.ReadAllLines(scenario.StatusFile); - var procId = int.Parse(ids[0]); - - var changedFile = Path.Combine(scenario.TestAppFolder, "include", "not_compiled.css"); - File.WriteAllText(changedFile, ""); - - Console.WriteLine($"Waiting {_negativeTestWaitTime.TotalSeconds} seconds to see if the app restarts"); - Waiters.WaitForProcessToStop( - procId, - _negativeTestWaitTime, - expectedToStop: false, - errorMessage: "Test app restarted"); - } - } - - // Change a file that's in an excluded folder - [Fact] - public void ChangeExcludedFile() - { - using (var scenario = new GlobbingAppScenario(_logger)) - { - scenario.Start(); - - var ids = File.ReadAllLines(scenario.StatusFile); - var procId = int.Parse(ids[0]); - - var changedFile = Path.Combine(scenario.TestAppFolder, "exclude", "Baz.cs"); - File.WriteAllText(changedFile, ""); - - Console.WriteLine($"Waiting {_negativeTestWaitTime.TotalSeconds} seconds to see if the app restarts"); - Waiters.WaitForProcessToStop( - procId, - _negativeTestWaitTime, - expectedToStop: false, - errorMessage: "Test app restarted"); - } - } - - private class GlobbingAppScenario : DotNetWatchScenario - { - private const string TestAppName = "GlobbingApp"; - - public GlobbingAppScenario(ITestOutputHelper logger) - : base(logger) - { - StatusFile = Path.Combine(Scenario.TempFolder, "status"); - StartedFile = StatusFile + ".started"; - - Scenario.AddTestProjectFolder(TestAppName); - Scenario.Restore(TestAppName); - - TestAppFolder = Path.Combine(Scenario.WorkFolder, TestAppName); - } - - public void Start() - { - // Wait for the process to start - using (var wait = new WaitForFileToChange(StartedFile)) - { - RunDotNetWatch(new[] { "run", StatusFile }, Path.Combine(Scenario.WorkFolder, TestAppName)); - - wait.Wait(_defaultTimeout, - expectedToChange: true, - errorMessage: $"File not created: {StartedFile}"); - } - - Waiters.WaitForFileToBeReadable(StartedFile, _defaultTimeout); - } - - public string StatusFile { get; private set; } - public string StartedFile { get; private set; } - public string TestAppFolder { get; private set; } - } } } diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/NoDepsAppTests.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/NoDepsAppTests.cs index 81f5f962dc..73d5ecd982 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/NoDepsAppTests.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/NoDepsAppTests.cs @@ -2,132 +2,63 @@ // 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.Diagnostics; using System.IO; -using System.Threading; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { - public class NoDepsAppTests + public class NoDepsAppTests : IDisposable { - private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); - private readonly ITestOutputHelper _logger; + private readonly WatchableApp _app; public NoDepsAppTests(ITestOutputHelper logger) { - _logger = logger; + _app = new WatchableApp("NoDepsApp", logger); + _app.Prepare(); } [Fact] - public void RestartProcessOnFileChange() + public async Task RestartProcessOnFileChange() { - using (var scenario = new NoDepsAppScenario(_logger)) - { - // Wait for the process to start - using (var wait = new WaitForFileToChange(scenario.StartedFile)) - { - scenario.RunDotNetWatch(new[] { "run", scenario.StatusFile, "--no-exit" }); + await _app.StartWatcher(new[] { "--no-exit" }).OrTimeout(); + var pid = await _app.GetProcessId().OrTimeout(); - wait.Wait(_defaultTimeout, - expectedToChange: true, - errorMessage: $"File not created: {scenario.StartedFile}"); - } + // Then wait for it to restart when we change a file + var fileToChange = Path.Combine(_app.SourceDirectory, "Program.cs"); + var programCs = File.ReadAllText(fileToChange); + File.WriteAllText(fileToChange, programCs); - // Then wait for it to restart when we change a file - using (var wait = new WaitForFileToChange(scenario.StartedFile)) - { - var fileToChange = Path.Combine(scenario.TestAppFolder, "Program.cs"); - var programCs = File.ReadAllText(fileToChange); - File.WriteAllText(fileToChange, programCs); + await _app.HasRestarted().OrTimeout(); + var pid2 = await _app.GetProcessId().OrTimeout(); + Assert.NotEqual(pid, pid2); - wait.Wait(_defaultTimeout, - expectedToChange: true, - errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); - } - - // Check that the first child process is no longer running - Waiters.WaitForFileToBeReadable(scenario.StatusFile, _defaultTimeout); - var ids = File.ReadAllLines(scenario.StatusFile); - var firstProcessId = int.Parse(ids[0]); - Waiters.WaitForProcessToStop( - firstProcessId, - TimeSpan.FromSeconds(1), - expectedToStop: true, - errorMessage: $"PID: {firstProcessId} is still alive"); - } + // first app should have shut down + Assert.Throws(() => Process.GetProcessById(pid)); } [Fact] - public void RestartProcessThatTerminatesAfterFileChange() + public async Task RestartProcessThatTerminatesAfterFileChange() { - using (var scenario = new NoDepsAppScenario(_logger)) - { - // Wait for the process to start - using (var wait = new WaitForFileToChange(scenario.StartedFile)) - { - scenario.RunDotNetWatch(new[] { "run", scenario.StatusFile }); + await _app.StartWatcher().OrTimeout(); + var pid = await _app.GetProcessId().OrTimeout(); + await _app.HasExited().OrTimeout(); // process should exit after run - wait.Wait(_defaultTimeout, - expectedToChange: true, - errorMessage: $"File not created: {scenario.StartedFile}"); - } + var fileToChange = Path.Combine(_app.SourceDirectory, "Program.cs"); + var programCs = File.ReadAllText(fileToChange); + File.WriteAllText(fileToChange, programCs); - // Then wait for the app to exit - Waiters.WaitForFileToBeReadable(scenario.StartedFile, _defaultTimeout); - var ids = File.ReadAllLines(scenario.StatusFile); - var procId = int.Parse(ids[0]); - Waiters.WaitForProcessToStop( - procId, - _defaultTimeout, - expectedToStop: true, - errorMessage: "Test app did not exit"); - - // Then wait for it to restart when we change a file - using (var wait = new WaitForFileToChange(scenario.StartedFile)) - { - // On Unix the file write time is in 1s increments; - // if we don't wait, there's a chance that the polling - // watcher will not detect the change - Thread.Sleep(1000); - - var fileToChange = Path.Combine(scenario.TestAppFolder, "Program.cs"); - var programCs = File.ReadAllText(fileToChange); - File.WriteAllText(fileToChange, programCs); - - wait.Wait(_defaultTimeout, - expectedToChange: true, - errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); - } - } + await _app.HasRestarted().OrTimeout(); + var pid2 = await _app.GetProcessId().OrTimeout(); + Assert.NotEqual(pid, pid2); + await _app.HasExited().OrTimeout(); // process should exit after run } - private class NoDepsAppScenario : DotNetWatchScenario + public void Dispose() { - private const string TestAppName = "NoDepsApp"; - - public NoDepsAppScenario(ITestOutputHelper logger) - : base(logger) - { - StatusFile = Path.Combine(Scenario.TempFolder, "status"); - StartedFile = StatusFile + ".started"; - - Scenario.AddTestProjectFolder(TestAppName); - Scenario.Restore(TestAppName); - - TestAppFolder = Path.Combine(Scenario.WorkFolder, TestAppName); - } - - public string StatusFile { get; private set; } - public string StartedFile { get; private set; } - public string TestAppFolder { get; private set; } - - public void RunDotNetWatch(IEnumerable args) - { - RunDotNetWatch(args, Path.Combine(Scenario.WorkFolder, TestAppName)); - } + _app.Dispose(); } } } diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Properties/AssemblyInfo.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Properties/AssemblyInfo.cs deleted file mode 100644 index d776ca34eb..0000000000 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using Xunit; - -[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/DotNetWatchScenario.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/DotNetWatchScenario.cs deleted file mode 100644 index a49a0ed616..0000000000 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/DotNetWatchScenario.cs +++ /dev/null @@ -1,57 +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 System.Diagnostics; -using System.IO; -using Microsoft.Extensions.Internal; -using Xunit.Abstractions; - -namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests -{ - public class DotNetWatchScenario : IDisposable - { - protected ProjectToolScenario Scenario { get; } - public DotNetWatchScenario() - : this(null) - { - } - - public DotNetWatchScenario(ITestOutputHelper logger) - { - Scenario = new ProjectToolScenario(logger); - } - - public Process WatcherProcess { get; private set; } - - public bool UsePollingWatcher { get; set; } - - protected void RunDotNetWatch(IEnumerable arguments, string workingFolder) - { - IDictionary envVariables = null; - if (UsePollingWatcher) - { - envVariables = new Dictionary() - { - ["DOTNET_USE_POLLING_FILE_WATCHER"] = "true" - }; - } - - WatcherProcess = Scenario.ExecuteDotnetWatch(arguments, workingFolder, envVariables); - } - - public virtual void Dispose() - { - if (WatcherProcess != null) - { - if (!WatcherProcess.HasExited) - { - WatcherProcess.KillTree(); - } - WatcherProcess.Dispose(); - } - Scenario.Dispose(); - } - } -} diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/ProjectToolScenario.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/ProjectToolScenario.cs index c7fc330aef..fb7e058d6c 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/ProjectToolScenario.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/ProjectToolScenario.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; using System.Reflection; using System.Threading; using Microsoft.DotNet.Cli.Utils; @@ -58,12 +56,21 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests public void Restore(string project) { - project = Path.Combine(WorkFolder, project); - _logger?.WriteLine($"Restoring msbuild project in {project}"); + ExecuteCommand(project, "restore"); + } - var restore = Command - .Create(new Muxer().MuxerPath, new[] { "restore", "/p:SkipInvalidConfigurations=true" }) + public void Build(string project) + { + _logger?.WriteLine($"Building {project}"); + ExecuteCommand(project, "build"); + } + + private void ExecuteCommand(string project, params string[] arguments) + { + project = Path.Combine(WorkFolder, project); + var command = Command + .Create(new Muxer().MuxerPath, arguments) .WorkingDirectory(project) .CaptureStdErr() .CaptureStdOut() @@ -71,9 +78,9 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests .OnOutputLine(l => _logger?.WriteLine(l)) .Execute(); - if (restore.ExitCode != 0) + if (command.ExitCode != 0) { - throw new Exception($"Exit code {restore.ExitCode}"); + throw new InvalidOperationException($"Exit code {command.ExitCode}"); } } @@ -88,7 +95,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests File.Copy(nugetConfigFilePath, tempNugetConfigFile); } - public Process ExecuteDotnetWatch(IEnumerable arguments, string workDir, IDictionary environmentVariables = null) + public IEnumerable GetDotnetWatchArguments() { // this launches a new .NET Core process using the runtime of the current test app // and the version of dotnet-watch that this test app is compiled against @@ -104,32 +111,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests args.Add(Path.Combine(AppContext.BaseDirectory, "dotnet-watch.dll")); - var argsStr = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args.Concat(arguments)); - - _logger?.WriteLine($"Running dotnet {argsStr} in {workDir}"); - - var psi = new ProcessStartInfo(new Muxer().MuxerPath, argsStr) - { - UseShellExecute = false, - WorkingDirectory = workDir - }; - - if (environmentVariables != null) - { - foreach (var newEnvVar in environmentVariables) - { - var varKey = newEnvVar.Key; - var varValue = newEnvVar.Value; -#if NET451 - psi.EnvironmentVariables[varKey] = varValue; - -#else - psi.Environment[varKey] = varValue; -#endif - } - } - - return Process.Start(psi); + return args; } private static string FindNugetConfig() diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/WatchableApp.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/WatchableApp.cs new file mode 100644 index 0000000000..7e5dc8ee8e --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/WatchableApp.cs @@ -0,0 +1,91 @@ +// 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.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.DotNet.Cli.Utils; +using Xunit.Abstractions; + +namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests +{ + public class WatchableApp : IDisposable + { + private const string StartedMessage = "Started"; + private const string ExitingMessage = "Exiting"; + + protected ProjectToolScenario Scenario { get; } + private readonly ITestOutputHelper _logger; + protected AwaitableProcess Process { get; set; } + private string _appName; + private bool _prepared; + + public WatchableApp(string appName, ITestOutputHelper logger) + { + _logger = logger; + _appName = appName; + Scenario = new ProjectToolScenario(logger); + Scenario.AddTestProjectFolder(appName); + SourceDirectory = Path.Combine(Scenario.WorkFolder, appName); + } + + public string SourceDirectory { get; } + + public Task HasRestarted() + => Process.GetOutputLineAsync(StartedMessage); + + public Task HasExited() + => Process.GetOutputLineAsync(ExitingMessage); + + public bool UsePollingWatcher { get; set; } + + public Task StartWatcher([CallerMemberName] string name = null) + => StartWatcher(Array.Empty(), name); + + public async Task GetProcessId() + { + var line = await Process.GetOutputLineAsync(l => l.StartsWith("PID =")); + var pid = line.Split('=').Last(); + return int.Parse(pid); + } + + public void Prepare() + { + Scenario.Restore(_appName); + Scenario.Build(_appName); + _prepared = true; + } + + public async Task StartWatcher(string[] arguments, [CallerMemberName] string name = null) + { + if (!_prepared) + { + throw new InvalidOperationException("Call .Prepare() first"); + } + + var args = Scenario + .GetDotnetWatchArguments() + .Concat(new[] { "run", "--" }) + .Concat(arguments); + + var spec = new ProcessSpec + { + Executable = new Muxer().MuxerPath, + Arguments = args, + WorkingDirectory = SourceDirectory + }; + + Process = new AwaitableProcess(spec, _logger); + Process.Start(); + await Process.GetOutputLineAsync(StartedMessage); + } + + public virtual void Dispose() + { + Process.Dispose(); + Scenario.Dispose(); + } + } +} diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TaskExtensions.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TaskExtensions.cs new file mode 100644 index 0000000000..b93d843943 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TaskExtensions.cs @@ -0,0 +1,27 @@ +// 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.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests +{ + public static class TaskExtensions + { + public static async Task OrTimeout(this Task task, int timeout = 30, [CallerFilePath] string file = null, [CallerLineNumber] int line = 0) + { + await OrTimeout((Task)task, timeout, file, line); + return task.Result; + } + + public static async Task OrTimeout(this Task task, int timeout = 30, [CallerFilePath] string file = null, [CallerLineNumber] int line = 0) + { + var finished = await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(timeout))); + if (!ReferenceEquals(finished, task)) + { + throw new TimeoutException($"Task exceeded max running time of {timeout}s at {file}:{line}"); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/AppWithDeps.csproj b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/AppWithDeps.csproj index 00ea5a7ab7..e5fc0c1ed5 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/AppWithDeps.csproj +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/AppWithDeps.csproj @@ -9,7 +9,7 @@ - + diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/Program.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/Program.cs index a5aaf90366..c9338cbc98 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/Program.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/Program.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.IO; using System.Threading; namespace ConsoleApplication @@ -14,26 +13,9 @@ namespace ConsoleApplication public static void Main(string[] args) { - ConsoleWrite("AppWithDeps started."); - - File.AppendAllLines(args[0], new string[] { $"{processId}" }); - - File.WriteAllText(args[0] + ".started", ""); - Block(); - } - - private static void ConsoleWrite(string text) - { - Console.WriteLine($"[{processId}] {text}"); - } - - private static void Block() - { - while (true) - { - ConsoleWrite("Blocked..."); - Thread.Sleep(1000); - } + Console.WriteLine("Started"); + Console.WriteLine($"PID = " + Process.GetCurrentProcess().Id); + Thread.Sleep(Timeout.Infinite); } } } diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/Dependency/Dependency.csproj b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/Dependency/Dependency.csproj index d8bbbdd6b7..04aad78be1 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/Dependency/Dependency.csproj +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/Dependency/Dependency.csproj @@ -9,7 +9,7 @@ - + \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/GlobbingApp.csproj b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/GlobbingApp.csproj index 2389ac538f..904fb1c9ed 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/GlobbingApp.csproj +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/GlobbingApp.csproj @@ -8,7 +8,7 @@ - + diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/Program.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/Program.cs index 9e89b57a60..768257e074 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/Program.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/Program.cs @@ -3,37 +3,20 @@ using System; using System.Diagnostics; -using System.IO; +using System.Linq; +using System.Reflection; using System.Threading; namespace ConsoleApplication { public class Program { - private static readonly int processId = Process.GetCurrentProcess().Id; - public static void Main(string[] args) { - ConsoleWrite("GlobbingApp started."); - - File.AppendAllLines(args[0], new string[] { $"{processId}" }); - - File.WriteAllText(args[0] + ".started", ""); - Block(); - } - - private static void ConsoleWrite(string text) - { - Console.WriteLine($"[{processId}] {text}"); - } - - private static void Block() - { - while (true) - { - ConsoleWrite("Blocked..."); - Thread.Sleep(1000); - } + Console.WriteLine("Started"); + Console.WriteLine("PID = " + Process.GetCurrentProcess().Id); + Console.WriteLine("Defined types = " + typeof(Program).GetTypeInfo().Assembly.DefinedTypes.Count()); + Thread.Sleep(Timeout.Infinite); } } } diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/NoDepsApp.csproj b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/NoDepsApp.csproj index 3fe95153a3..e218c105dd 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/NoDepsApp.csproj +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/NoDepsApp.csproj @@ -8,7 +8,7 @@ - + diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/Program.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/Program.cs index cbff2063f8..b503e70ce6 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/Program.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/Program.cs @@ -3,41 +3,21 @@ using System; using System.Diagnostics; -using System.IO; using System.Threading; namespace ConsoleApplication { public class Program { - private static readonly int processId = Process.GetCurrentProcess().Id; - public static void Main(string[] args) { - ConsoleWrite("NoDepsApp started."); - - File.AppendAllLines(args[0], new string[] { $"{processId}" }); - - File.WriteAllText(args[0] + ".started", ""); - - if (args.Length > 1 && args[1] == "--no-exit") + Console.WriteLine("Started"); + Console.WriteLine($"PID = " + Process.GetCurrentProcess().Id); + if (args.Length > 0 && args[0] == "--no-exit") { - Block(); - } - } - - private static void ConsoleWrite(string text) - { - Console.WriteLine($"[{processId}] {text}"); - } - - private static void Block() - { - while (true) - { - ConsoleWrite("Blocked..."); - Thread.Sleep(1000); + Thread.Sleep(Timeout.Infinite); } + Console.WriteLine("Exiting"); } } } diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/WaitForFileToChange.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/WaitForFileToChange.cs deleted file mode 100644 index 995e61a738..0000000000 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/WaitForFileToChange.cs +++ /dev/null @@ -1,61 +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.IO; -using System.Threading; -using Microsoft.DotNet.Watcher.Internal; - -namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests -{ - public class WaitForFileToChange : IDisposable - { - private readonly IFileSystemWatcher _watcher; - private readonly string _expectedFile; - - private ManualResetEvent _changed = new ManualResetEvent(false); - - public WaitForFileToChange(string file) - { - _watcher = FileWatcherFactory.CreateWatcher(Path.GetDirectoryName(file), usePollingWatcher: true); - _expectedFile = file; - - _watcher.OnFileChange += WatcherEvent; - - _watcher.EnableRaisingEvents = true; - } - - private void WatcherEvent(object sender, string file) - { - if (file.Equals(_expectedFile, StringComparison.Ordinal)) - { - Waiters.WaitForFileToBeReadable(_expectedFile, TimeSpan.FromSeconds(10)); - _changed?.Set(); - } - } - - public void Wait(TimeSpan timeout, bool expectedToChange, string errorMessage) - { - if (_changed != null) - { - var changed = _changed.WaitOne(timeout); - if (changed != expectedToChange) - { - throw new Exception(errorMessage); - } - } - } - - public void Dispose() - { - _watcher.EnableRaisingEvents = false; - - _watcher.OnFileChange -= WatcherEvent; - - _watcher.Dispose(); - _changed.Dispose(); - - _changed = null; - } - } -} diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Waiters.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Waiters.cs deleted file mode 100644 index de8c0f7f3f..0000000000 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Waiters.cs +++ /dev/null @@ -1,84 +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.Diagnostics; -using System.IO; -using System.Threading; - -namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests -{ - public static class Waiters - { - public static void WaitForFileToBeReadable(string file, TimeSpan timeout) - { - var watch = new Stopwatch(); - - Exception lastException = null; - - watch.Start(); - while (watch.Elapsed < timeout) - { - try - { - File.ReadAllText(file); - watch.Stop(); - return; - } - catch (Exception e) - { - lastException = e; - } - Thread.Sleep(500); - } - watch.Stop(); - - if (lastException != null) - { - Console.WriteLine("Last exception:"); - Console.WriteLine(lastException); - } - - throw new InvalidOperationException($"{file} is not readable."); - } - - public static void WaitForProcessToStop(int processId, TimeSpan timeout, bool expectedToStop, string errorMessage) - { - Console.WriteLine($"Waiting for process {processId} to stop..."); - - Process process = null; - - try - { - process = Process.GetProcessById(processId); - } - catch (Exception e) - { - // If we expect the process to stop, then it might have stopped already - if (!expectedToStop) - { - Console.WriteLine($"Could not find process {processId}: {e}"); - } - } - - var watch = new Stopwatch(); - watch.Start(); - while (watch.Elapsed < timeout) - { - if (process == null || process.HasExited) - { - Console.WriteLine($"Process {processId} is no longer running"); - break; - } - Thread.Sleep(500); - } - watch.Stop(); - - bool isStopped = process == null || process.HasExited; - if (isStopped != expectedToStop) - { - throw new InvalidOperationException(errorMessage); - } - } - } -} diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/test.cmd b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/test.cmd new file mode 100644 index 0000000000..0aa8bdf56e --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/test.cmd @@ -0,0 +1,6 @@ +@echo off + +rem For local testing + +dotnet build +..\..\.build\dotnet\dotnet.exe exec --depsfile bin\Debug\netcoreapp1.0\Microsoft.DotNet.Watcher.Tools.FunctionalTests.deps.json --runtimeconfig bin\Debug\netcoreapp1.0\Microsoft.DotNet.Watcher.Tools.FunctionalTests.runtimeconfig.json ..\..\.build\dotnet-test-xunit\2.2.0-preview2-build1029\lib\netcoreapp1.0\dotnet-test-xunit.dll bin\Debug\netcoreapp1.0\Microsoft.DotNet.Watcher.Tools.FunctionalTests.dll %* \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/test.sh b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/test.sh new file mode 100755 index 0000000000..e617f19860 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/test.sh @@ -0,0 +1,8 @@ +dotnet build + +../../.build/dotnet/dotnet exec \ +--depsfile bin/Debug/netcoreapp1.0/Microsoft.DotNet.Watcher.Tools.FunctionalTests.deps.json \ +--runtimeconfig bin/Debug/netcoreapp1.0/Microsoft.DotNet.Watcher.Tools.FunctionalTests.runtimeconfig.json \ +../../.build/dotnet-test-xunit/2.2.0-preview2-build1029/lib/netcoreapp1.0/dotnet-test-xunit.dll \ +bin/Debug/netcoreapp1.0/Microsoft.DotNet.Watcher.Tools.FunctionalTests.dll \ +$@ \ No newline at end of file