From 4b6918ca1260d009ec08dbf1d31e56da43f7005e Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Tue, 3 Mar 2020 21:33:36 -0800 Subject: [PATCH] Add tye run test (#63) --- src/Tye.Hosting/TyeHost.cs | 127 ++++++++++++------ src/tye/ConfigModel/ConfigApplication.cs | 2 +- .../ConfigModel/ConfigConfigurationSource.cs | 2 +- src/tye/ConfigModel/ConfigFactory.cs | 2 +- src/tye/ConfigModel/ConfigService.cs | 2 +- src/tye/ConfigModel/ConfigServiceBinding.cs | 5 +- src/tye/Program.RunCommand.cs | 3 +- test/E2ETest/E2ETest.csproj | 8 +- test/E2ETest/RetryHandler.cs | 53 ++++++++ test/E2ETest/TyeRunTest.cs | 84 ++++++++++++ test/E2ETest/UnitTest1.cs | 14 -- tye.sln | 25 ++-- 12 files changed, 254 insertions(+), 73 deletions(-) create mode 100644 test/E2ETest/RetryHandler.cs create mode 100644 test/E2ETest/TyeRunTest.cs delete mode 100644 test/E2ETest/UnitTest1.cs diff --git a/src/Tye.Hosting/TyeHost.cs b/src/Tye.Hosting/TyeHost.cs index afdbe7b6..7028b352 100644 --- a/src/Tye.Hosting/TyeHost.cs +++ b/src/Tye.Hosting/TyeHost.cs @@ -9,12 +9,89 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Serilog; using Serilog.Filters; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; namespace Tye.Hosting { public class TyeHost { - public static async Task RunAsync(Application application, string[] args) + private Microsoft.Extensions.Logging.ILogger? _logger; + private IHostApplicationLifetime? _lifetime; + private AggregateApplicationProcessor? _processor; + private WebApplication? _app; + private readonly Application _application; + private readonly string[] _args; + + public TyeHost(Application application, string[] args) + { + _application = application; + _args = args; + } + + public async Task RunAsync() + { + await StartAsync(); + + var waitForStop = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _lifetime?.ApplicationStopping.Register(obj => waitForStop.TrySetResult(null), null); + await waitForStop.Task; + + await StopAsync(); + } + + public async Task StartAsync() + { + var app = BuildWebApplication(_application, _args); + _app = app; + + ConfigureApplication(app); + + _logger = app.Logger; + _lifetime = app.ApplicationLifetime; + + _logger.LogInformation("Executing application from {Source}", _application.Source); + + var configuration = app.Configuration; + + _processor = CreateApplicationProcessor(_args, _logger, configuration); + + await app.StartAsync(); + + _logger.LogInformation("Dashboard running on {Address}", app.Addresses.First()); + + try + { + await _processor.StartAsync(_application); + } + catch (Exception ex) + { + _logger.LogError(0, ex, "Failed to launch application"); + } + + return app; + } + + public async Task StopAsync() + { + try + { + if (_processor != null) + { + await _processor.StopAsync(_application); + } + } + finally + { + if (_app != null) + { + // Stop the host after everything else has been shutdown + await _app.StopAsync(); + } + } + } + + private static WebApplication BuildWebApplication(Application application, string[] args) { var builder = WebApplication.CreateBuilder(args); @@ -42,9 +119,12 @@ namespace Tye.Hosting }); builder.Services.AddSingleton(application); + var app = builder.Build(); + return app; + } - using var app = builder.Build(); - + private static void ConfigureApplication(WebApplication app) + { var port = app.Configuration["port"] ?? "0"; app.Listen($"http://127.0.0.1:{port}"); @@ -61,14 +141,10 @@ namespace Tye.Hosting app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); + } - var logger = app.Logger; - - logger.LogInformation("Executing application from {Source}", application.Source); - - var lifetime = app.ApplicationLifetime; - var configuration = app.Configuration; - + private static AggregateApplicationProcessor CreateApplicationProcessor(string[] args, Microsoft.Extensions.Logging.ILogger logger, IConfiguration configuration) + { var diagnosticOptions = DiagnosticOptions.FromConfiguration(configuration); var diagnosticsCollector = new DiagnosticsCollector(logger, diagnosticOptions); @@ -81,36 +157,7 @@ namespace Tye.Hosting new DockerRunner(logger), new ProcessRunner(logger, ProcessRunnerOptions.FromArgs(args)), }); - - await app.StartAsync(); - - logger.LogInformation("Dashboard running on {Address}", app.Addresses.First()); - - try - { - await processor.StartAsync(application); - } - catch (Exception ex) - { - logger.LogError(0, ex, "Failed to launch application"); - } - - var waitForStop = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - lifetime.ApplicationStopping.Register(obj => waitForStop.TrySetResult(null), null); - - await waitForStop.Task; - - logger.LogInformation("Shutting down..."); - - try - { - await processor.StopAsync(application); - } - finally - { - // Stop the host after everything else has been shutdown - await app.StopAsync(); - } + return processor; } } } diff --git a/src/tye/ConfigModel/ConfigApplication.cs b/src/tye/ConfigModel/ConfigApplication.cs index 56715cf4..65419af8 100644 --- a/src/tye/ConfigModel/ConfigApplication.cs +++ b/src/tye/ConfigModel/ConfigApplication.cs @@ -6,7 +6,7 @@ using YamlDotNet.Serialization; namespace Tye.ConfigModel { - internal class ConfigApplication + public class ConfigApplication { // This gets set by all of the code paths that read the application [YamlIgnore] diff --git a/src/tye/ConfigModel/ConfigConfigurationSource.cs b/src/tye/ConfigModel/ConfigConfigurationSource.cs index cafda2e0..16f0619d 100644 --- a/src/tye/ConfigModel/ConfigConfigurationSource.cs +++ b/src/tye/ConfigModel/ConfigConfigurationSource.cs @@ -2,7 +2,7 @@ namespace Tye.ConfigModel { - internal class ConfigConfigurationSource + public class ConfigConfigurationSource { [Required] public string Name { get; set; } = default!; diff --git a/src/tye/ConfigModel/ConfigFactory.cs b/src/tye/ConfigModel/ConfigFactory.cs index e1a98889..31024af4 100644 --- a/src/tye/ConfigModel/ConfigFactory.cs +++ b/src/tye/ConfigModel/ConfigFactory.cs @@ -7,7 +7,7 @@ using YamlDotNet.Serialization.NamingConventions; namespace Tye.ConfigModel { - internal static class ConfigFactory + public static class ConfigFactory { public static ConfigApplication FromFile(FileInfo file) { diff --git a/src/tye/ConfigModel/ConfigService.cs b/src/tye/ConfigModel/ConfigService.cs index d45d4e75..64740083 100644 --- a/src/tye/ConfigModel/ConfigService.cs +++ b/src/tye/ConfigModel/ConfigService.cs @@ -4,7 +4,7 @@ using YamlDotNet.Serialization; namespace Tye.ConfigModel { - internal class ConfigService + public class ConfigService { [Required] public string Name { get; set; } = default!; diff --git a/src/tye/ConfigModel/ConfigServiceBinding.cs b/src/tye/ConfigModel/ConfigServiceBinding.cs index ed631582..1b24763c 100644 --- a/src/tye/ConfigModel/ConfigServiceBinding.cs +++ b/src/tye/ConfigModel/ConfigServiceBinding.cs @@ -1,8 +1,9 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; namespace Tye.ConfigModel { - internal class ConfigServiceBinding + public class ConfigServiceBinding { public string? Name { get; set; } public string? ConnectionString { get; set; } diff --git a/src/tye/Program.RunCommand.cs b/src/tye/Program.RunCommand.cs index 292b8746..5216577f 100644 --- a/src/tye/Program.RunCommand.cs +++ b/src/tye/Program.RunCommand.cs @@ -59,7 +59,8 @@ namespace Tye } var application = ConfigFactory.FromFile(path); - return TyeHost.RunAsync(application.ToHostingApplication(), args); + var host = new TyeHost(application.ToHostingApplication(), args); + return host.RunAsync(); }); return command; diff --git a/test/E2ETest/E2ETest.csproj b/test/E2ETest/E2ETest.csproj index 6fadcd75..b77c798c 100644 --- a/test/E2ETest/E2ETest.csproj +++ b/test/E2ETest/E2ETest.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -13,4 +13,10 @@ + + + + + + diff --git a/test/E2ETest/RetryHandler.cs b/test/E2ETest/RetryHandler.cs new file mode 100644 index 00000000..c011a962 --- /dev/null +++ b/test/E2ETest/RetryHandler.cs @@ -0,0 +1,53 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace E2ETest +{ + public class RetryHandler : DelegatingHandler + { + private static readonly int MaxRetries = 5; + private static readonly TimeSpan InitialRetryDelay = TimeSpan.FromMilliseconds(100); + + public RetryHandler(HttpMessageHandler innerHandler) + : base(innerHandler) + { + } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + HttpResponseMessage? response = null; + var delay = InitialRetryDelay; + for (var i = 0; i < MaxRetries; i++) + { + try + { + response = await base.SendAsync(request, cancellationToken); + } + catch (Exception) + { + if (i == MaxRetries - 1) + { + throw; + } + } + + if (response != null && + (response.IsSuccessStatusCode || response.StatusCode != (HttpStatusCode)503)) + { + return response; + } + + await Task.Delay(delay, cancellationToken); + delay *= 2; + } + + throw new TimeoutException("Could not reach response after "); + } + } +} diff --git a/test/E2ETest/TyeRunTest.cs b/test/E2ETest/TyeRunTest.cs new file mode 100644 index 00000000..0ea23bc3 --- /dev/null +++ b/test/E2ETest/TyeRunTest.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Tye.ConfigModel; +using Tye.Hosting; +using Xunit; + +namespace E2ETest +{ + public class TyeRunTest + { + [Fact] + public async Task SingleProjectTest() + { + var application = ConfigFactory.FromFile(new FileInfo(Path.Combine(GetSolutionRootDirectory("tye"), "samples", "single-project", "test-project", "test-project.csproj"))); + var host = new TyeHost(application.ToHostingApplication(), new string[0]); + var webApplication = await host.StartAsync(); + try + { + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true, + AllowAutoRedirect = false + }; + + var client = new HttpClient(new RetryHandler(handler)); + + // Make sure dashboard and applications are up. + // Dashboard should be hosted in same process. + var dashboardResponse = await client.GetStringAsync(new Uri(webApplication.Addresses.First())); + + // Only one service for single application. + var service = application.Services.First(); + var binding = service.Bindings.First(); + + var protocol = binding.Protocol?.Length != 0 ? binding.Protocol : "http"; + var hostName = binding.Host != null && binding.Host.Length != 0 ? binding.Host : "localhost"; + + var uriString = $"{protocol}://{hostName}:{binding.Port}"; + + // Confirm that the uri is in the dashboard response. + Assert.Contains(uriString, dashboardResponse); + + var uriBackendProcess = new Uri(uriString); + + // This isn't reliable right now because micronetes only guarantees the process starts, not that + // that kestrel started. + var appResponse = await client.GetAsync(uriBackendProcess); + Assert.Equal(HttpStatusCode.OK, appResponse.StatusCode); + } + finally + { + await host.StopAsync(); + } + } + + + // https://github.com/dotnet/aspnetcore/blob/5a0526dfd991419d5bce0d8ea525b50df2e37b04/src/Testing/src/TestPathUtilities.cs + // This can get into a bad pattern for having crazy paths in places. Eventually, especially if we use helix, + // we may want to avoid relying on sln position. + public static string GetSolutionRootDirectory(string solution) + { + var applicationBasePath = AppContext.BaseDirectory; + var directoryInfo = new DirectoryInfo(applicationBasePath); + + do + { + var projectFileInfo = new FileInfo(Path.Combine(directoryInfo.FullName, $"{solution}.sln")); + if (projectFileInfo.Exists) + { + return projectFileInfo.DirectoryName; + } + + directoryInfo = directoryInfo.Parent; + } + while (directoryInfo.Parent != null); + + throw new Exception($"Solution file {solution}.sln could not be found in {applicationBasePath} or its parent directories."); + } + } +} diff --git a/test/E2ETest/UnitTest1.cs b/test/E2ETest/UnitTest1.cs deleted file mode 100644 index 86299013..00000000 --- a/test/E2ETest/UnitTest1.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Xunit; - -namespace E2ETest -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} diff --git a/tye.sln b/tye.sln index 3b0735e9..47e04216 100644 --- a/tye.sln +++ b/tye.sln @@ -1,23 +1,23 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29613.14 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8C662D59-A3CB-466F-8E85-A8E6BA5E7601}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tye", "src\tye\tye.csproj", "{9BFE4070-D6D8-41F8-B92E-F85C3DCD7AB8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tye", "src\tye\tye.csproj", "{9BFE4070-D6D8-41F8-B92E-F85C3DCD7AB8}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F19B02EB-A372-417A-B2C2-EA0D5A3C76D5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "E2ETest", "test\E2ETest\E2ETest.csproj", "{D15E5FF6-C1E7-4110-A2BE-06ADA7ACA82B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "E2ETest", "test\E2ETest\E2ETest.csproj", "{D15E5FF6-C1E7-4110-A2BE-06ADA7ACA82B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tye.Hosting", "src\Tye.Hosting\Tye.Hosting.csproj", "{0F4F5A86-DD27-4AF9-BF97-221EAD5040E7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tye.Hosting", "src\Tye.Hosting\Tye.Hosting.csproj", "{0F4F5A86-DD27-4AF9-BF97-221EAD5040E7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tye.Hosting.Diagnostics", "src\Tye.Hosting.Diagnostics\Tye.Hosting.Diagnostics.csproj", "{CEBFC149-8162-4A0A-9AD4-40498B9172CD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tye.Hosting.Diagnostics", "src\Tye.Hosting.Diagnostics\Tye.Hosting.Diagnostics.csproj", "{CEBFC149-8162-4A0A-9AD4-40498B9172CD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tye.Hosting.Runtime", "src\Tye.Hosting.Runtime\Tye.Hosting.Runtime.csproj", "{34719884-1338-4965-BA2A-F98DB03733C2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tye.Hosting.Runtime", "src\Tye.Hosting.Runtime\Tye.Hosting.Runtime.csproj", "{34719884-1338-4965-BA2A-F98DB03733C2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tye.Core", "src\Tye.Core\Tye.Core.csproj", "{D0359C69-6EA9-4B03-9455-90E8E04F1CB0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tye.Core", "src\Tye.Core\Tye.Core.csproj", "{D0359C69-6EA9-4B03-9455-90E8E04F1CB0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -28,9 +28,6 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9BFE4070-D6D8-41F8-B92E-F85C3DCD7AB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9BFE4070-D6D8-41F8-B92E-F85C3DCD7AB8}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -105,6 +102,9 @@ Global {D0359C69-6EA9-4B03-9455-90E8E04F1CB0}.Release|x86.ActiveCfg = Release|Any CPU {D0359C69-6EA9-4B03-9455-90E8E04F1CB0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection GlobalSection(NestedProjects) = preSolution {9BFE4070-D6D8-41F8-B92E-F85C3DCD7AB8} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601} {D15E5FF6-C1E7-4110-A2BE-06ADA7ACA82B} = {F19B02EB-A372-417A-B2C2-EA0D5A3C76D5} @@ -113,4 +113,7 @@ Global {34719884-1338-4965-BA2A-F98DB03733C2} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601} {D0359C69-6EA9-4B03-9455-90E8E04F1CB0} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D8002603-BB27-4500-BF86-274A8E72D302} + EndGlobalSection EndGlobal