From 8fe927d40d1d11074f6f08ea3eb3b8fc1dbd292c Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Mon, 30 Apr 2018 12:17:36 -0700 Subject: [PATCH] Run app and verify response --- AspNetCoreSdkTests/TemplateTests.cs | 42 +++++++ .../Templates/ConsoleApplicationTemplate.cs | 2 +- .../Templates/RazorApplicationBaseTemplate.cs | 2 +- AspNetCoreSdkTests/Templates/Template.cs | 1 + AspNetCoreSdkTests/Templates/TemplateData.cs | 6 + AspNetCoreSdkTests/Templates/TemplateType.cs | 5 +- .../Templates/WebApiTemplate.cs | 2 + AspNetCoreSdkTests/Templates/WebTemplate.cs | 2 + .../Util/ConcurrentStringBuilder.cs | 35 ++++++ AspNetCoreSdkTests/Util/DotNetContext.cs | 47 +++++++- AspNetCoreSdkTests/Util/DotNetUtil.cs | 37 ++++-- AspNetCoreSdkTests/Util/ProcessHelper.cs | 113 ++++++++++++++++++ AspNetCoreSdkTests/Util/TempDir.cs | 2 +- 13 files changed, 277 insertions(+), 19 deletions(-) create mode 100644 AspNetCoreSdkTests/Util/ConcurrentStringBuilder.cs create mode 100644 AspNetCoreSdkTests/Util/ProcessHelper.cs diff --git a/AspNetCoreSdkTests/TemplateTests.cs b/AspNetCoreSdkTests/TemplateTests.cs index f67f58001d..b8e7690676 100644 --- a/AspNetCoreSdkTests/TemplateTests.cs +++ b/AspNetCoreSdkTests/TemplateTests.cs @@ -1,12 +1,24 @@ using AspNetCoreSdkTests.Templates; using AspNetCoreSdkTests.Util; using NUnit.Framework; +using System; +using System.Net; +using System.Net.Http; +using System.Threading; namespace AspNetCoreSdkTests { [TestFixture] public class TemplateTests { + private static readonly TimeSpan _sleepBetweenHttpRequests = TimeSpan.FromMilliseconds(100); + + private static readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler() + { + // Allow self-signed certs + ServerCertificateCustomValidationCallback = (m, c, ch, p) => true + }); + [Test] [TestCaseSource(typeof(TemplateData), nameof(TemplateData.Current))] public void Restore(Template template, NuGetConfig nuGetConfig) @@ -34,5 +46,35 @@ namespace AspNetCoreSdkTests CollectionAssert.AreEquivalent(template.ExpectedBinFilesAfterBuild, context.GetBinFiles()); } } + + [Test] + [TestCaseSource(typeof(TemplateData), nameof(TemplateData.CurrentWebApplications))] + public void Run(Template template, NuGetConfig nuGetConfig) + { + using (var context = new DotNetContext()) + { + context.New(template); + context.Restore(nuGetConfig); + var (httpUrl, httpsUrl) = context.Run(); + + Assert.AreEqual(HttpStatusCode.OK, GetAsync(new Uri(new Uri(httpUrl), template.RelativeUrl)).StatusCode); + Assert.AreEqual(HttpStatusCode.OK, GetAsync(new Uri(new Uri(httpsUrl), template.RelativeUrl)).StatusCode); + } + } + + private HttpResponseMessage GetAsync(Uri requestUri) + { + while (true) + { + try + { + return _httpClient.GetAsync(requestUri).Result; + } + catch + { + Thread.Sleep(_sleepBetweenHttpRequests); + } + } + } } } diff --git a/AspNetCoreSdkTests/Templates/ConsoleApplicationTemplate.cs b/AspNetCoreSdkTests/Templates/ConsoleApplicationTemplate.cs index 46c6110a92..e96fbe5776 100644 --- a/AspNetCoreSdkTests/Templates/ConsoleApplicationTemplate.cs +++ b/AspNetCoreSdkTests/Templates/ConsoleApplicationTemplate.cs @@ -15,7 +15,7 @@ namespace AspNetCoreSdkTests.Templates public override string OutputPath { get; } = Path.Combine("Debug", "netcoreapp2.1"); - public override TemplateType Type => TemplateType.Application; + public override TemplateType Type => TemplateType.ConsoleApplication; public override IEnumerable ExpectedBinFilesAfterBuild => Enumerable.Concat(base.ExpectedBinFilesAfterBuild, new[] { diff --git a/AspNetCoreSdkTests/Templates/RazorApplicationBaseTemplate.cs b/AspNetCoreSdkTests/Templates/RazorApplicationBaseTemplate.cs index 520df95795..2511cd9d99 100644 --- a/AspNetCoreSdkTests/Templates/RazorApplicationBaseTemplate.cs +++ b/AspNetCoreSdkTests/Templates/RazorApplicationBaseTemplate.cs @@ -10,7 +10,7 @@ namespace AspNetCoreSdkTests.Templates public override string OutputPath { get; } = Path.Combine("Debug", "netcoreapp2.1"); - public override TemplateType Type => TemplateType.Application; + public override TemplateType Type => TemplateType.WebApplication; public override IEnumerable ExpectedObjFilesAfterBuild => Enumerable.Concat(base.ExpectedObjFilesAfterBuild, new[] { diff --git a/AspNetCoreSdkTests/Templates/Template.cs b/AspNetCoreSdkTests/Templates/Template.cs index f3ef37bc6d..07a6adc954 100644 --- a/AspNetCoreSdkTests/Templates/Template.cs +++ b/AspNetCoreSdkTests/Templates/Template.cs @@ -6,6 +6,7 @@ namespace AspNetCoreSdkTests.Templates { public abstract string Name { get; } public abstract TemplateType Type { get; } + public virtual string RelativeUrl => string.Empty; public virtual IEnumerable ExpectedObjFilesAfterRestore => new[] { diff --git a/AspNetCoreSdkTests/Templates/TemplateData.cs b/AspNetCoreSdkTests/Templates/TemplateData.cs index d4ba3e1376..9372e278b1 100644 --- a/AspNetCoreSdkTests/Templates/TemplateData.cs +++ b/AspNetCoreSdkTests/Templates/TemplateData.cs @@ -38,5 +38,11 @@ namespace AspNetCoreSdkTests.Templates d); public static IEnumerable Current => IgnoreRazorClassLibEmpty; + + public static IEnumerable CurrentWebApplications { get; } = + from d in Current + where ((Template)d.Arguments[0]).Type == TemplateType.WebApplication + select d; + } } diff --git a/AspNetCoreSdkTests/Templates/TemplateType.cs b/AspNetCoreSdkTests/Templates/TemplateType.cs index d7fa539e94..3717def35e 100644 --- a/AspNetCoreSdkTests/Templates/TemplateType.cs +++ b/AspNetCoreSdkTests/Templates/TemplateType.cs @@ -2,7 +2,8 @@ { public enum TemplateType { - Application, - ClassLibrary + ClassLibrary, + ConsoleApplication, + WebApplication, } } diff --git a/AspNetCoreSdkTests/Templates/WebApiTemplate.cs b/AspNetCoreSdkTests/Templates/WebApiTemplate.cs index ce41e2872e..21d98d9f71 100644 --- a/AspNetCoreSdkTests/Templates/WebApiTemplate.cs +++ b/AspNetCoreSdkTests/Templates/WebApiTemplate.cs @@ -7,5 +7,7 @@ protected WebApiTemplate() { } public override string Name => "webapi"; + + public override string RelativeUrl => "/api/values"; } } diff --git a/AspNetCoreSdkTests/Templates/WebTemplate.cs b/AspNetCoreSdkTests/Templates/WebTemplate.cs index 3274194911..fb91b68953 100644 --- a/AspNetCoreSdkTests/Templates/WebTemplate.cs +++ b/AspNetCoreSdkTests/Templates/WebTemplate.cs @@ -12,6 +12,8 @@ namespace AspNetCoreSdkTests.Templates public override string Name => "web"; + public override TemplateType Type => TemplateType.WebApplication; + public override IEnumerable ExpectedObjFilesAfterBuild => Enumerable.Concat(base.ExpectedObjFilesAfterBuild, new[] { $"{Name}.RazorAssemblyInfo.cache", diff --git a/AspNetCoreSdkTests/Util/ConcurrentStringBuilder.cs b/AspNetCoreSdkTests/Util/ConcurrentStringBuilder.cs new file mode 100644 index 0000000000..796168f993 --- /dev/null +++ b/AspNetCoreSdkTests/Util/ConcurrentStringBuilder.cs @@ -0,0 +1,35 @@ +using System; +using System.Text; + +namespace AspNetCoreSdkTests.Util +{ + public class ConcurrentStringBuilder + { + private StringBuilder _stringBuilder = new StringBuilder(); + private object _lock = new object(); + + public void AppendLine() + { + lock (_lock) + { + _stringBuilder.AppendLine(); + } + } + + public void AppendLine(string data) + { + lock (_lock) + { + _stringBuilder.AppendLine(data); + } + } + + public override string ToString() + { + lock (_lock) + { + return _stringBuilder.ToString(); + } + } + } +} diff --git a/AspNetCoreSdkTests/Util/DotNetContext.cs b/AspNetCoreSdkTests/Util/DotNetContext.cs index 58b37bf5c4..f1eb24b044 100644 --- a/AspNetCoreSdkTests/Util/DotNetContext.cs +++ b/AspNetCoreSdkTests/Util/DotNetContext.cs @@ -1,23 +1,52 @@ using AspNetCoreSdkTests.Templates; +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Threading; namespace AspNetCoreSdkTests.Util { public class DotNetContext : TempDir { + private static readonly TimeSpan _sleepBetweenOutputContains = TimeSpan.FromMilliseconds(100); + + private (Process Process, ConcurrentStringBuilder OutputBuilder, ConcurrentStringBuilder ErrorBuilder) _process; + public string New(Template template) { - return DotNet.New(template.Name, Path); + return DotNetUtil.New(template.Name, Path); } public string Restore(NuGetConfig config) { - return DotNet.Restore(Path, config); + return DotNetUtil.Restore(Path, config); } public string Build() { - return DotNet.Build(Path); + return DotNetUtil.Build(Path); + } + + public (string httpUrl, string httpsUrl) Run() + { + _process = DotNetUtil.Run(Path); + + // Extract URLs from output + while (true) + { + var output = _process.OutputBuilder.ToString(); + if (output.Contains("Application started")) + { + var httpUrl = Regex.Match(output, @"Now listening on: (http:\S*)").Groups[1].Value; + var httpsUrl = Regex.Match(output, @"Now listening on: (https:\S*)").Groups[1].Value; + return (httpUrl, httpsUrl); + } + else + { + Thread.Sleep(_sleepBetweenOutputContains); + } + } } public IEnumerable GetObjFiles() @@ -29,5 +58,17 @@ namespace AspNetCoreSdkTests.Util { return IOUtil.GetFiles(System.IO.Path.Combine(Path, "bin")); } + + public override void Dispose() + { + // Must stop process to release filehandles before calling base.Dispose() which deletes app dir + if (_process.Process != null) + { + DotNetUtil.StopProcess(_process.Process, _process.OutputBuilder, _process.ErrorBuilder, throwOnError: false); + _process.Process = null; + } + + base.Dispose(); + } } } diff --git a/AspNetCoreSdkTests/Util/DotNetUtil.cs b/AspNetCoreSdkTests/Util/DotNetUtil.cs index e6affaa772..eba8e00efc 100644 --- a/AspNetCoreSdkTests/Util/DotNetUtil.cs +++ b/AspNetCoreSdkTests/Util/DotNetUtil.cs @@ -1,13 +1,13 @@ -using System; +using Microsoft.Extensions.Internal; +using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Text; namespace AspNetCoreSdkTests.Util { - internal static class DotNet + internal static class DotNetUtil { private static IEnumerable> GetEnvironment(string workingDirectory) { @@ -32,6 +32,12 @@ namespace AspNetCoreSdkTests.Util return RunDotNet("build --no-restore", workingDirectory, GetEnvironment(workingDirectory)); } + public static (Process Process, ConcurrentStringBuilder OutputBuilder, ConcurrentStringBuilder ErrorBuilder) Run(string workingDirectory) + { + // Bind to dynamic port 0 to avoid port conflicts during parallel tests + return StartDotNet("run --no-restore --urls http://127.0.0.1:0;https://127.0.0.1:0", workingDirectory, GetEnvironment(workingDirectory)); + } + private static string RunDotNet(string arguments, string workingDirectory, IEnumerable> environment = null, bool throwOnError = true) { @@ -39,13 +45,13 @@ namespace AspNetCoreSdkTests.Util return WaitForExit(p.Process, p.OutputBuilder, p.ErrorBuilder, throwOnError: throwOnError); } - private static (Process Process, StringBuilder OutputBuilder, StringBuilder ErrorBuilder) StartDotNet( + private static (Process Process, ConcurrentStringBuilder OutputBuilder, ConcurrentStringBuilder ErrorBuilder) StartDotNet( string arguments, string workingDirectory, IEnumerable> environment = null) { return StartProcess("dotnet", arguments, workingDirectory, environment); } - private static (Process Process, StringBuilder OutputBuilder, StringBuilder ErrorBuilder) StartProcess( + private static (Process Process, ConcurrentStringBuilder OutputBuilder, ConcurrentStringBuilder ErrorBuilder) StartProcess( string filename, string arguments, string workingDirectory, IEnumerable> environment = null) { var process = new Process() @@ -70,13 +76,13 @@ namespace AspNetCoreSdkTests.Util } } - var outputBuilder = new StringBuilder(); + var outputBuilder = new ConcurrentStringBuilder(); process.OutputDataReceived += (_, e) => { outputBuilder.AppendLine(e.Data); }; - var errorBuilder = new StringBuilder(); + var errorBuilder = new ConcurrentStringBuilder(); process.ErrorDataReceived += (_, e) => { errorBuilder.AppendLine(e.Data); @@ -89,7 +95,18 @@ namespace AspNetCoreSdkTests.Util return (process, outputBuilder, errorBuilder); } - public static string WaitForExit(Process process, StringBuilder outputBuilder, StringBuilder errorBuilder, + public static string StopProcess(Process process, ConcurrentStringBuilder outputBuilder, ConcurrentStringBuilder errorBuilder, + bool throwOnError = true) + { + if (!process.HasExited) + { + process.KillTree(); + } + + return WaitForExit(process, outputBuilder, errorBuilder, throwOnError: throwOnError); + } + + public static string WaitForExit(Process process, ConcurrentStringBuilder outputBuilder, ConcurrentStringBuilder errorBuilder, bool throwOnError = true) { // Workaround issue where WaitForExit() blocks until child processes are killed, which is problematic @@ -100,7 +117,7 @@ namespace AspNetCoreSdkTests.Util if (throwOnError && process.ExitCode != 0) { - var sb = new StringBuilder(); + var sb = new ConcurrentStringBuilder(); sb.AppendLine($"Command {process.StartInfo.FileName} {process.StartInfo.Arguments} returned exit code {process.ExitCode}"); sb.AppendLine(); @@ -111,7 +128,5 @@ namespace AspNetCoreSdkTests.Util return outputBuilder.ToString(); } - - } } diff --git a/AspNetCoreSdkTests/Util/ProcessHelper.cs b/AspNetCoreSdkTests/Util/ProcessHelper.cs new file mode 100644 index 0000000000..cf42a7e3a7 --- /dev/null +++ b/AspNetCoreSdkTests/Util/ProcessHelper.cs @@ -0,0 +1,113 @@ +// 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 System.Runtime.InteropServices; + +namespace Microsoft.Extensions.Internal +{ + internal static class ProcessExtensions + { + private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); + + public static void KillTree(this Process process) + { + process.KillTree(_defaultTimeout); + } + + public static void KillTree(this Process process, TimeSpan timeout) + { + string stdout; + if (_isWindows) + { + RunProcessAndWaitForExit( + "taskkill", + $"/T /F /PID {process.Id}", + timeout, + out stdout); + } + else + { + var children = new HashSet(); + GetAllChildIdsUnix(process.Id, children, timeout); + foreach (var childId in children) + { + KillProcessUnix(childId, timeout); + } + KillProcessUnix(process.Id, timeout); + } + } + + private static void GetAllChildIdsUnix(int parentId, ISet children, TimeSpan timeout) + { + string stdout; + var exitCode = RunProcessAndWaitForExit( + "pgrep", + $"-P {parentId}", + timeout, + out stdout); + + if (exitCode == 0 && !string.IsNullOrEmpty(stdout)) + { + using (var reader = new StringReader(stdout)) + { + while (true) + { + var text = reader.ReadLine(); + if (text == null) + { + return; + } + + int id; + if (int.TryParse(text, out id)) + { + children.Add(id); + // Recursively get the children + GetAllChildIdsUnix(id, children, timeout); + } + } + } + } + } + + private static void KillProcessUnix(int processId, TimeSpan timeout) + { + string stdout; + RunProcessAndWaitForExit( + "kill", + $"-TERM {processId}", + timeout, + out stdout); + } + + private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout) + { + var startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var process = Process.Start(startInfo); + + stdout = null; + if (process.WaitForExit((int)timeout.TotalMilliseconds)) + { + stdout = process.StandardOutput.ReadToEnd(); + } + else + { + process.Kill(); + } + + return process.ExitCode; + } + } +} diff --git a/AspNetCoreSdkTests/Util/TempDir.cs b/AspNetCoreSdkTests/Util/TempDir.cs index 273589e0dd..2fb1ccc302 100644 --- a/AspNetCoreSdkTests/Util/TempDir.cs +++ b/AspNetCoreSdkTests/Util/TempDir.cs @@ -11,7 +11,7 @@ namespace AspNetCoreSdkTests.Util Path = IOUtil.GetTempDir(); } - public void Dispose() + public virtual void Dispose() { IOUtil.DeleteDir(Path); }