Run app and verify response

This commit is contained in:
Mike Harder 2018-04-30 12:17:36 -07:00 committed by GitHub
parent ec34d7cbbb
commit 8fe927d40d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 277 additions and 19 deletions

View File

@ -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);
}
}
}
}
}

View File

@ -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<string> ExpectedBinFilesAfterBuild => Enumerable.Concat(base.ExpectedBinFilesAfterBuild, new[]
{

View File

@ -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<string> ExpectedObjFilesAfterBuild => Enumerable.Concat(base.ExpectedObjFilesAfterBuild, new[]
{

View File

@ -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<string> ExpectedObjFilesAfterRestore => new[]
{

View File

@ -38,5 +38,11 @@ namespace AspNetCoreSdkTests.Templates
d);
public static IEnumerable<TestCaseData> Current => IgnoreRazorClassLibEmpty;
public static IEnumerable<TestCaseData> CurrentWebApplications { get; } =
from d in Current
where ((Template)d.Arguments[0]).Type == TemplateType.WebApplication
select d;
}
}

View File

@ -2,7 +2,8 @@
{
public enum TemplateType
{
Application,
ClassLibrary
ClassLibrary,
ConsoleApplication,
WebApplication,
}
}

View File

@ -7,5 +7,7 @@
protected WebApiTemplate() { }
public override string Name => "webapi";
public override string RelativeUrl => "/api/values";
}
}

View File

@ -12,6 +12,8 @@ namespace AspNetCoreSdkTests.Templates
public override string Name => "web";
public override TemplateType Type => TemplateType.WebApplication;
public override IEnumerable<string> ExpectedObjFilesAfterBuild => Enumerable.Concat(base.ExpectedObjFilesAfterBuild, new[]
{
$"{Name}.RazorAssemblyInfo.cache",

View File

@ -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();
}
}
}
}

View File

@ -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<string> 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();
}
}
}

View File

@ -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<KeyValuePair<string, string>> 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<KeyValuePair<string, string>> 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<KeyValuePair<string, string>> 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<KeyValuePair<string, string>> 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();
}
}
}

View File

@ -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<int>();
GetAllChildIdsUnix(process.Id, children, timeout);
foreach (var childId in children)
{
KillProcessUnix(childId, timeout);
}
KillProcessUnix(process.Id, timeout);
}
}
private static void GetAllChildIdsUnix(int parentId, ISet<int> 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;
}
}
}

View File

@ -11,7 +11,7 @@ namespace AspNetCoreSdkTests.Util
Path = IOUtil.GetTempDir();
}
public void Dispose()
public virtual void Dispose()
{
IOUtil.DeleteDir(Path);
}