From c0d533430bef984f0823f60163c50ffc68a49b89 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Sun, 8 Mar 2020 15:07:22 -0700 Subject: [PATCH] Add tye generate tests (#85) --- samples/single-project/tye.yaml | 6 + src/{shared => Tye.Core}/DockerDetector.cs | 2 +- src/{shared => Tye.Core}/NameSanitizer.cs | 2 +- src/{shared => Tye.Core}/ProcessResult.cs | 2 +- src/{shared => Tye.Core}/ProcessUtil.cs | 2 +- src/Tye.Core/Tye.Core.csproj | 4 - .../AggregateApplicationProcessor.cs | 4 +- src/Tye.Hosting/DockerRunner.cs | 10 +- src/Tye.Hosting/EventPipeDiagnosticsRunner.cs | 4 +- src/Tye.Hosting/IApplicationProcessor.cs | 4 +- src/Tye.Hosting/ProcessRunner.cs | 10 +- src/Tye.Hosting/ProxyService.cs | 4 +- src/Tye.Hosting/Tye.Hosting.csproj | 8 +- src/Tye.Hosting/TyeDashboardApi.cs | 10 +- src/Tye.Hosting/TyeHost.cs | 6 +- src/shared/TempDirectory.cs | 8 +- src/tye/GenerateHost.cs | 59 +++++++ src/tye/Program.DeployCommand.cs | 2 +- src/tye/Program.GenerateCommand.cs | 41 +---- src/tye/tye.csproj | 4 - test/E2ETest/E2ETest.csproj | 3 + .../ConditionalFactAttribute.cs | 12 ++ .../ConditionalFactDiscoverer.cs | 25 +++ test/E2ETest/Infrastructure/ITestCondition.cs | 9 + .../SkipIfDockerNotRunningAttribute.cs | 24 +++ .../E2ETest/Infrastructure/SkippedTestCase.cs | 46 +++++ .../Infrastructure/TestMethodExtensions.cs | 31 ++++ test/E2ETest/TestOutputLogEventSink.cs | 22 ++- test/E2ETest/TyeGenerateTests.cs | 106 +++++++++++ test/E2ETest/TyeInitTests.cs | 6 +- .../testassets/generate/frontend-backend.yaml | 101 +++++++++++ .../testassets/generate/multi-project.yaml | 167 ++++++++++++++++++ .../testassets/generate/single-project.yaml | 51 ++++++ 33 files changed, 700 insertions(+), 95 deletions(-) create mode 100644 samples/single-project/tye.yaml rename src/{shared => Tye.Core}/DockerDetector.cs (97%) rename src/{shared => Tye.Core}/NameSanitizer.cs (95%) rename src/{shared => Tye.Core}/ProcessResult.cs (94%) rename src/{shared => Tye.Core}/ProcessUtil.cs (99%) create mode 100644 src/tye/GenerateHost.cs create mode 100644 test/E2ETest/Infrastructure/ConditionalFactAttribute.cs create mode 100644 test/E2ETest/Infrastructure/ConditionalFactDiscoverer.cs create mode 100644 test/E2ETest/Infrastructure/ITestCondition.cs create mode 100644 test/E2ETest/Infrastructure/SkipIfDockerNotRunningAttribute.cs create mode 100644 test/E2ETest/Infrastructure/SkippedTestCase.cs create mode 100644 test/E2ETest/Infrastructure/TestMethodExtensions.cs create mode 100644 test/E2ETest/TyeGenerateTests.cs create mode 100644 test/E2ETest/testassets/generate/frontend-backend.yaml create mode 100644 test/E2ETest/testassets/generate/multi-project.yaml create mode 100644 test/E2ETest/testassets/generate/single-project.yaml diff --git a/samples/single-project/tye.yaml b/samples/single-project/tye.yaml new file mode 100644 index 00000000..50d77d3a --- /dev/null +++ b/samples/single-project/tye.yaml @@ -0,0 +1,6 @@ +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +name: single-project +services: +- name: test-project + project: test-project/test-project.csproj diff --git a/src/shared/DockerDetector.cs b/src/Tye.Core/DockerDetector.cs similarity index 97% rename from src/shared/DockerDetector.cs rename to src/Tye.Core/DockerDetector.cs index 294d6f44..5c484cab 100644 --- a/src/shared/DockerDetector.cs +++ b/src/Tye.Core/DockerDetector.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Tye { - internal class DockerDetector + public class DockerDetector { public static DockerDetector Instance { get; } = new DockerDetector(); diff --git a/src/shared/NameSanitizer.cs b/src/Tye.Core/NameSanitizer.cs similarity index 95% rename from src/shared/NameSanitizer.cs rename to src/Tye.Core/NameSanitizer.cs index bb8acc43..0702a116 100644 --- a/src/shared/NameSanitizer.cs +++ b/src/Tye.Core/NameSanitizer.cs @@ -6,7 +6,7 @@ using System; namespace Tye { - internal static class NameSanitizer + public static class NameSanitizer { // Converts an arbitrary string into something usable as a C# identifier. // diff --git a/src/shared/ProcessResult.cs b/src/Tye.Core/ProcessResult.cs similarity index 94% rename from src/shared/ProcessResult.cs rename to src/Tye.Core/ProcessResult.cs index 111bb87f..64adb458 100644 --- a/src/shared/ProcessResult.cs +++ b/src/Tye.Core/ProcessResult.cs @@ -4,7 +4,7 @@ namespace Tye { - internal class ProcessResult + public class ProcessResult { public ProcessResult(string standardOutput, string standardError, int exitCode) { diff --git a/src/shared/ProcessUtil.cs b/src/Tye.Core/ProcessUtil.cs similarity index 99% rename from src/shared/ProcessUtil.cs rename to src/Tye.Core/ProcessUtil.cs index 3ac482bd..f7f6cc20 100644 --- a/src/shared/ProcessUtil.cs +++ b/src/Tye.Core/ProcessUtil.cs @@ -13,7 +13,7 @@ using Mono.Unix.Native; namespace Tye { - internal static class ProcessUtil + public static class ProcessUtil { public static async Task RunAsync( string filename, diff --git a/src/Tye.Core/Tye.Core.csproj b/src/Tye.Core/Tye.Core.csproj index e9362bcc..206b2db7 100644 --- a/src/Tye.Core/Tye.Core.csproj +++ b/src/Tye.Core/Tye.Core.csproj @@ -25,11 +25,7 @@ - - - - diff --git a/src/Tye.Hosting/AggregateApplicationProcessor.cs b/src/Tye.Hosting/AggregateApplicationProcessor.cs index acecf3a1..ca2416d2 100644 --- a/src/Tye.Hosting/AggregateApplicationProcessor.cs +++ b/src/Tye.Hosting/AggregateApplicationProcessor.cs @@ -18,7 +18,7 @@ namespace Tye.Hosting _applicationProcessors = applicationProcessors; } - public async Task StartAsync(Application application) + public async Task StartAsync(Tye.Hosting.Model.Application application) { foreach (var processor in _applicationProcessors) { @@ -26,7 +26,7 @@ namespace Tye.Hosting } } - public async Task StopAsync(Application application) + public async Task StopAsync(Tye.Hosting.Model.Application application) { // Shutdown in the opposite order foreach (var processor in _applicationProcessors.Reverse()) diff --git a/src/Tye.Hosting/DockerRunner.cs b/src/Tye.Hosting/DockerRunner.cs index 69b82f9f..b55a4deb 100644 --- a/src/Tye.Hosting/DockerRunner.cs +++ b/src/Tye.Hosting/DockerRunner.cs @@ -21,7 +21,7 @@ namespace Tye.Hosting _logger = logger; } - public Task StartAsync(Application application) + public Task StartAsync(Tye.Hosting.Model.Application application) { var tasks = new Task[application.Services.Count]; var index = 0; @@ -33,7 +33,7 @@ namespace Tye.Hosting return Task.WhenAll(tasks); } - public Task StopAsync(Application application) + public Task StopAsync(Tye.Hosting.Model.Application application) { var services = application.Services; @@ -48,7 +48,7 @@ namespace Tye.Hosting return Task.WhenAll(tasks); } - private async Task StartContainerAsync(Application application, Service service, DockerRunInfo docker) + private async Task StartContainerAsync(Tye.Hosting.Model.Application application, Tye.Hosting.Model.Service service, DockerRunInfo docker) { if (!await DockerDetector.Instance.IsDockerInstalled.Value) { @@ -224,7 +224,7 @@ namespace Tye.Hosting service.Items[typeof(DockerInformation)] = dockerInfo; } - private static void PrintStdOutAndErr(Service service, string replica, ProcessResult result) + private static void PrintStdOutAndErr(Tye.Hosting.Model.Service service, string replica, ProcessResult result) { if (result.ExitCode != 0) { @@ -240,7 +240,7 @@ namespace Tye.Hosting } } - private async Task StopContainerAsync(Service service) + private async Task StopContainerAsync(Tye.Hosting.Model.Service service) { if (service.Items.TryGetValue(typeof(DockerInformation), out var value) && value is DockerInformation di) { diff --git a/src/Tye.Hosting/EventPipeDiagnosticsRunner.cs b/src/Tye.Hosting/EventPipeDiagnosticsRunner.cs index 14e575c1..90d1e3be 100644 --- a/src/Tye.Hosting/EventPipeDiagnosticsRunner.cs +++ b/src/Tye.Hosting/EventPipeDiagnosticsRunner.cs @@ -23,7 +23,7 @@ namespace Tye.Hosting _diagnosticsCollector = diagnosticsCollector; } - public Task StartAsync(Application application) + public Task StartAsync(Tye.Hosting.Model.Application application) { foreach (var service in application.Services.Values) { @@ -38,7 +38,7 @@ namespace Tye.Hosting return Task.CompletedTask; } - public Task StopAsync(Application application) + public Task StopAsync(Tye.Hosting.Model.Application application) { foreach (var service in application.Services.Values) { diff --git a/src/Tye.Hosting/IApplicationProcessor.cs b/src/Tye.Hosting/IApplicationProcessor.cs index 0a005c0a..aa857c2c 100644 --- a/src/Tye.Hosting/IApplicationProcessor.cs +++ b/src/Tye.Hosting/IApplicationProcessor.cs @@ -9,8 +9,8 @@ namespace Tye.Hosting { public interface IApplicationProcessor { - Task StartAsync(Application application); + Task StartAsync(Tye.Hosting.Model.Application application); - Task StopAsync(Application application); + Task StopAsync(Tye.Hosting.Model.Application application); } } diff --git a/src/Tye.Hosting/ProcessRunner.cs b/src/Tye.Hosting/ProcessRunner.cs index 3ca089a5..a85864c9 100644 --- a/src/Tye.Hosting/ProcessRunner.cs +++ b/src/Tye.Hosting/ProcessRunner.cs @@ -28,7 +28,7 @@ namespace Tye.Hosting _buildProjects = options.BuildProjects; } - public Task StartAsync(Application application) + public Task StartAsync(Tye.Hosting.Model.Application application) { var tasks = new Task[application.Services.Count]; var index = 0; @@ -49,12 +49,12 @@ namespace Tye.Hosting return Task.WhenAll(tasks); } - public Task StopAsync(Application application) + public Task StopAsync(Tye.Hosting.Model.Application application) { return KillRunningProcesses(application.Services); } - private async Task LaunchService(Application application, Service service) + private async Task LaunchService(Tye.Hosting.Model.Application application, Tye.Hosting.Model.Service service) { var serviceDescription = service.Description; var serviceName = serviceDescription.Name; @@ -263,9 +263,9 @@ namespace Tye.Hosting service.Items[typeof(ProcessInfo)] = processInfo; } - private Task KillRunningProcesses(IDictionary services) + private Task KillRunningProcesses(IDictionary services) { - static async Task KillProcessAsync(Service service) + static async Task KillProcessAsync(Tye.Hosting.Model.Service service) { if (service.Items.TryGetValue(typeof(ProcessInfo), out var stateObj) && stateObj is ProcessInfo state) { diff --git a/src/Tye.Hosting/ProxyService.cs b/src/Tye.Hosting/ProxyService.cs index cf3976e1..212d4076 100644 --- a/src/Tye.Hosting/ProxyService.cs +++ b/src/Tye.Hosting/ProxyService.cs @@ -30,7 +30,7 @@ namespace Tye.Hosting _logger = logger; } - public async Task StartAsync(Application application) + public async Task StartAsync(Tye.Hosting.Model.Application application) { _host = new HostBuilder() .ConfigureServer(server => @@ -172,7 +172,7 @@ namespace Tye.Hosting await _host.StartAsync(); } - public async Task StopAsync(Application application) + public async Task StopAsync(Tye.Hosting.Model.Application application) { if (_host != null) { diff --git a/src/Tye.Hosting/Tye.Hosting.csproj b/src/Tye.Hosting/Tye.Hosting.csproj index 84851d4c..6cc4f48f 100644 --- a/src/Tye.Hosting/Tye.Hosting.csproj +++ b/src/Tye.Hosting/Tye.Hosting.csproj @@ -13,13 +13,6 @@ - - - - - - - @@ -36,6 +29,7 @@ + diff --git a/src/Tye.Hosting/TyeDashboardApi.cs b/src/Tye.Hosting/TyeDashboardApi.cs index e9ca460d..e339de79 100644 --- a/src/Tye.Hosting/TyeDashboardApi.cs +++ b/src/Tye.Hosting/TyeDashboardApi.cs @@ -55,7 +55,7 @@ namespace Tye.Hosting private async Task Services(HttpContext context) { - var app = context.RequestServices.GetRequiredService(); + var app = context.RequestServices.GetRequiredService(); context.Response.ContentType = "application/json"; @@ -66,7 +66,7 @@ namespace Tye.Hosting private async Task Service(HttpContext context) { - var app = context.RequestServices.GetRequiredService(); + var app = context.RequestServices.GetRequiredService(); var name = (string)context.Request.RouteValues["name"]; context.Response.ContentType = "application/json"; @@ -88,7 +88,7 @@ namespace Tye.Hosting private async Task Logs(HttpContext context) { - var app = context.RequestServices.GetRequiredService(); + var app = context.RequestServices.GetRequiredService(); var name = (string)context.Request.RouteValues["name"]; context.Response.ContentType = "application/json"; @@ -110,7 +110,7 @@ namespace Tye.Hosting private async Task AllMetrics(HttpContext context) { - var app = context.RequestServices.GetRequiredService(); + var app = context.RequestServices.GetRequiredService(); var sb = new StringBuilder(); foreach (var s in app.Services.OrderBy(s => s.Key)) @@ -138,7 +138,7 @@ namespace Tye.Hosting private async Task Metics(HttpContext context) { - var app = context.RequestServices.GetRequiredService(); + var app = context.RequestServices.GetRequiredService(); var sb = new StringBuilder(); diff --git a/src/Tye.Hosting/TyeHost.cs b/src/Tye.Hosting/TyeHost.cs index 9d60e810..7127e8e7 100644 --- a/src/Tye.Hosting/TyeHost.cs +++ b/src/Tye.Hosting/TyeHost.cs @@ -26,10 +26,10 @@ namespace Tye.Hosting private IHostApplicationLifetime? _lifetime; private AggregateApplicationProcessor? _processor; - private readonly Application _application; + private readonly Tye.Hosting.Model.Application _application; private readonly string[] _args; - public TyeHost(Application application, string[] args) + public TyeHost(Tye.Hosting.Model.Application application, string[] args) { _application = application; _args = args; @@ -103,7 +103,7 @@ namespace Tye.Hosting } private static WebApplication BuildWebApplication( - Application application, + Tye.Hosting.Model.Application application, string[] args, ILogEventSink? sink) { diff --git a/src/shared/TempDirectory.cs b/src/shared/TempDirectory.cs index 114d89f2..d8f0d9fb 100644 --- a/src/shared/TempDirectory.cs +++ b/src/shared/TempDirectory.cs @@ -12,16 +12,18 @@ namespace Tye public static TempDirectory Create() { var directoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(directoryPath); - return new TempDirectory(directoryPath); + var directoryInfo = Directory.CreateDirectory(directoryPath); + return new TempDirectory(directoryPath, directoryInfo); } - private TempDirectory(string directoryPath) + private TempDirectory(string directoryPath, DirectoryInfo directoryInfo) { DirectoryPath = directoryPath; + DirectoryInfo = directoryInfo; } public string DirectoryPath { get; } + public DirectoryInfo DirectoryInfo { get; } public void Dispose() { diff --git a/src/tye/GenerateHost.cs b/src/tye/GenerateHost.cs new file mode 100644 index 00000000..03232669 --- /dev/null +++ b/src/tye/GenerateHost.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.CommandLine; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Tye.ConfigModel; + +namespace Tye +{ + public class GenerateHost + { + public static Task GenerateAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive) + { + var application = ConfigFactory.FromFile(path); + return ExecuteGenerateAsync(new OutputContext(console, verbosity), application, environment: "production", interactive); + } + + public static async Task ExecuteGenerateAsync(OutputContext output, ConfigApplication application, string environment, bool interactive) + { + var temporaryApplication = await Program.CreateApplicationAdapterAsync(output, application, interactive); + var steps = new List() + { + new CombineStep() { Environment = environment, }, + new PublishProjectStep(), + new BuildDockerImageStep() { Environment = environment, }, // Make an image but don't push it + }; + + steps.Add(new GenerateKubernetesManifestStep() { Environment = environment, }); + + var executor = new ServiceExecutor(output, temporaryApplication, steps); + foreach (var service in temporaryApplication.Services) + { + await executor.ExecuteAsync(service); + } + + await GenerateApplicationManifestAsync(output, temporaryApplication, application.Name ?? application.Source.Directory.Name, environment); + } + + private static async Task GenerateApplicationManifestAsync(OutputContext output, Tye.Application application, string applicationName, string environment) + { + using var step = output.BeginStep("Generating Application Manifests..."); + + var outputFilePath = Path.GetFullPath(Path.Combine(application.RootDirectory, $"{applicationName}-generate-{environment}.yaml")); + output.WriteInfoLine($"Writing output to '{outputFilePath}'."); + { + using var stream = File.OpenWrite(outputFilePath); + using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true); + + await ApplicationYamlWriter.WriteAsync(output, writer, application); + } + + step.MarkComplete(); + } + } +} diff --git a/src/tye/Program.DeployCommand.cs b/src/tye/Program.DeployCommand.cs index c86cf3ae..aeb41fb4 100644 --- a/src/tye/Program.DeployCommand.cs +++ b/src/tye/Program.DeployCommand.cs @@ -78,7 +78,7 @@ namespace Tye await DeployApplicationManifestAsync(output, temporaryApplication, application.Source.Directory.Name, environment); } - private static async Task CreateApplicationAdapterAsync(OutputContext output, ConfigApplication application, bool interactive) + internal static async Task CreateApplicationAdapterAsync(OutputContext output, ConfigApplication application, bool interactive) { var globals = new ApplicationGlobals() { diff --git a/src/tye/Program.GenerateCommand.cs b/src/tye/Program.GenerateCommand.cs index 97f7135f..ac883e98 100644 --- a/src/tye/Program.GenerateCommand.cs +++ b/src/tye/Program.GenerateCommand.cs @@ -35,49 +35,10 @@ namespace Tye throw new CommandException("No project or solution file was found."); } - var application = ConfigFactory.FromFile(path); - return ExecuteGenerateAsync(new OutputContext(console, verbosity), application, environment: "production", interactive); + return GenerateHost.GenerateAsync(console, path, verbosity, interactive); }); return command; } - - private static async Task ExecuteGenerateAsync(OutputContext output, ConfigApplication application, string environment, bool interactive) - { - var temporaryApplication = await CreateApplicationAdapterAsync(output, application, interactive); - var steps = new List() - { - new CombineStep() { Environment = environment, }, - new PublishProjectStep(), - new BuildDockerImageStep() { Environment = environment, }, // Make an image but don't push it - }; - - steps.Add(new GenerateKubernetesManifestStep() { Environment = environment, }); - - var executor = new ServiceExecutor(output, temporaryApplication, steps); - foreach (var service in temporaryApplication.Services) - { - await executor.ExecuteAsync(service); - } - - await GenerateApplicationManifestAsync(output, temporaryApplication, application.Source.Directory.Name, environment); - } - - private static async Task GenerateApplicationManifestAsync(OutputContext output, Tye.Application application, string applicationName, string environment) - { - using var step = output.BeginStep("Generating Application Manifests..."); - - var outputFilePath = Path.GetFullPath(Path.Combine(".", $"{applicationName}-{environment}.yaml")); - output.WriteInfoLine($"Writing output to '{outputFilePath}'."); - - { - using var stream = File.OpenWrite(outputFilePath); - using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true); - - await ApplicationYamlWriter.WriteAsync(output, writer, application); - } - - step.MarkComplete(); - } } } diff --git a/src/tye/tye.csproj b/src/tye/tye.csproj index 6c3225fb..8dd3e3ed 100644 --- a/src/tye/tye.csproj +++ b/src/tye/tye.csproj @@ -11,11 +11,7 @@ - - - - diff --git a/test/E2ETest/E2ETest.csproj b/test/E2ETest/E2ETest.csproj index 2a9ad66f..e0eea27a 100644 --- a/test/E2ETest/E2ETest.csproj +++ b/test/E2ETest/E2ETest.csproj @@ -23,6 +23,9 @@ + + + diff --git a/test/E2ETest/Infrastructure/ConditionalFactAttribute.cs b/test/E2ETest/Infrastructure/ConditionalFactAttribute.cs new file mode 100644 index 00000000..4559f87d --- /dev/null +++ b/test/E2ETest/Infrastructure/ConditionalFactAttribute.cs @@ -0,0 +1,12 @@ +using System; +using Xunit; +using Xunit.Sdk; + +namespace E2ETest +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + [XunitTestCaseDiscoverer("E2ETest." + nameof(ConditionalFactDiscoverer), "E2ETest")] + public class ConditionalFactAttribute : FactAttribute + { + } +} diff --git a/test/E2ETest/Infrastructure/ConditionalFactDiscoverer.cs b/test/E2ETest/Infrastructure/ConditionalFactDiscoverer.cs new file mode 100644 index 00000000..023cf5f2 --- /dev/null +++ b/test/E2ETest/Infrastructure/ConditionalFactDiscoverer.cs @@ -0,0 +1,25 @@ +using Xunit.Abstractions; +using Xunit.Sdk; + +// Do not change this namespace without changing the usage in ConditionalFactAttribute +namespace E2ETest +{ + internal class ConditionalFactDiscoverer : FactDiscoverer + { + private readonly IMessageSink _diagnosticMessageSink; + + public ConditionalFactDiscoverer(IMessageSink diagnosticMessageSink) + : base(diagnosticMessageSink) + { + _diagnosticMessageSink = diagnosticMessageSink; + } + + protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + var skipReason = testMethod.EvaluateSkipConditions(); + return skipReason != null + ? new SkippedTestCase(skipReason, _diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod) + : base.CreateTestCase(discoveryOptions, testMethod, factAttribute); + } + } +} diff --git a/test/E2ETest/Infrastructure/ITestCondition.cs b/test/E2ETest/Infrastructure/ITestCondition.cs new file mode 100644 index 00000000..17c13e67 --- /dev/null +++ b/test/E2ETest/Infrastructure/ITestCondition.cs @@ -0,0 +1,9 @@ +namespace E2ETest +{ + public interface ITestCondition + { + bool IsMet { get; } + + string SkipReason { get; } + } +} diff --git a/test/E2ETest/Infrastructure/SkipIfDockerNotRunningAttribute.cs b/test/E2ETest/Infrastructure/SkipIfDockerNotRunningAttribute.cs new file mode 100644 index 00000000..8a443ad3 --- /dev/null +++ b/test/E2ETest/Infrastructure/SkipIfDockerNotRunningAttribute.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Tye; + +namespace E2ETest +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)] + internal class SkipIfDockerNotRunningAttribute : Attribute, ITestCondition + { + public SkipIfDockerNotRunningAttribute() + { + // TODO Check performance of this. + IsMet = DockerDetector.Instance.IsDockerConnectedToDaemon.Value.GetAwaiter().GetResult(); + SkipReason = "Docker is not installed or running."; + } + + public bool IsMet { get; } + + public string SkipReason { get; set; } + } +} diff --git a/test/E2ETest/Infrastructure/SkippedTestCase.cs b/test/E2ETest/Infrastructure/SkippedTestCase.cs new file mode 100644 index 00000000..e9d4315d --- /dev/null +++ b/test/E2ETest/Infrastructure/SkippedTestCase.cs @@ -0,0 +1,46 @@ +using System; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace E2ETest +{ + public class SkippedTestCase : XunitTestCase + { + private string _skipReason; + + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public SkippedTestCase() : base() + { + _skipReason = ""; + } + + public SkippedTestCase( + string skipReason, + IMessageSink diagnosticMessageSink, + TestMethodDisplay defaultMethodDisplay, + TestMethodDisplayOptions defaultMethodDisplayOptions, + ITestMethod testMethod) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) + { + _skipReason = skipReason; + } + + protected override string GetSkipReason(IAttributeInfo factAttribute) + => _skipReason ?? base.GetSkipReason(factAttribute); + + public override void Deserialize(IXunitSerializationInfo data) + { + _skipReason = data.GetValue(nameof(_skipReason)); + + // We need to call base after reading our value, because Deserialize will call + // into GetSkipReason. + base.Deserialize(data); + } + + public override void Serialize(IXunitSerializationInfo data) + { + base.Serialize(data); + data.AddValue(nameof(_skipReason), _skipReason); + } + } +} diff --git a/test/E2ETest/Infrastructure/TestMethodExtensions.cs b/test/E2ETest/Infrastructure/TestMethodExtensions.cs new file mode 100644 index 00000000..e36bbdce --- /dev/null +++ b/test/E2ETest/Infrastructure/TestMethodExtensions.cs @@ -0,0 +1,31 @@ +using System.Linq; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace E2ETest +{ + public static class TestMethodExtensions + { + public static string EvaluateSkipConditions(this ITestMethod testMethod) + { + var testClass = testMethod.TestClass.Class; + var assembly = testMethod.TestClass.TestCollection.TestAssembly.Assembly; + var conditionAttributes = testMethod.Method + .GetCustomAttributes(typeof(ITestCondition)) + .Concat(testClass.GetCustomAttributes(typeof(ITestCondition))) + .Concat(assembly.GetCustomAttributes(typeof(ITestCondition))) + .OfType() + .Select(attributeInfo => attributeInfo.Attribute); + + foreach (ITestCondition condition in conditionAttributes) + { + if (!condition.IsMet) + { + return condition.SkipReason; + } + } + + return null!; + } + } +} diff --git a/test/E2ETest/TestOutputLogEventSink.cs b/test/E2ETest/TestOutputLogEventSink.cs index 05be4d61..b7b2e70a 100644 --- a/test/E2ETest/TestOutputLogEventSink.cs +++ b/test/E2ETest/TestOutputLogEventSink.cs @@ -1,20 +1,38 @@ -using Serilog.Core; +using System.CommandLine; +using System.CommandLine.IO; +using Serilog.Core; using Serilog.Events; using Xunit.Abstractions; namespace E2ETest { - public class TestOutputLogEventSink : ILogEventSink + public class TestOutputLogEventSink : ILogEventSink, IConsole, IStandardStreamWriter { private readonly ITestOutputHelper output; + public TestOutputLogEventSink(ITestOutputHelper output) { this.output = output; } + public IStandardStreamWriter Out => this; + + public bool IsOutputRedirected => false; + + public IStandardStreamWriter Error => this; + + public bool IsErrorRedirected => false; + + public bool IsInputRedirected => false; + public void Emit(LogEvent logEvent) { output.WriteLine(logEvent.RenderMessage()); } + + public void Write(string value) + { + output.WriteLine(value); + } } } diff --git a/test/E2ETest/TyeGenerateTests.cs b/test/E2ETest/TyeGenerateTests.cs new file mode 100644 index 00000000..f4d604a4 --- /dev/null +++ b/test/E2ETest/TyeGenerateTests.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using System.Threading.Tasks; +using Tye; +using Tye.ConfigModel; +using Xunit; +using Xunit.Abstractions; + +namespace E2ETest +{ + public class TyeGenerateTests + { + private readonly ITestOutputHelper output; + private readonly TestOutputLogEventSink sink; + + public TyeGenerateTests(ITestOutputHelper output) + { + this.output = output; + sink = new TestOutputLogEventSink(output); + } + + [ConditionalFact] + [SkipIfDockerNotRunning] + public async Task SingleProjectGenerateTest() + { + var projectName = "single-project"; + var environment = "production"; + + var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", projectName)); + using var tempDirectory = TempDirectory.Create(); + DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); + + var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); + + var application = ConfigFactory.FromFile(projectFile); + + // Need to add docker registry for generate + application.Registry = "test"; + + await GenerateHost.ExecuteGenerateAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); + + // name of application is the folder + var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); + var expectedContent = File.ReadAllText($"testassets/generate/{projectName}.yaml"); + + Assert.Equal(expectedContent, content); + } + + [ConditionalFact] + [SkipIfDockerNotRunning] + public async Task FrontendBackendGenerateTest() + { + var projectName = "frontend-backend"; + var environment = "production"; + + var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", projectName)); + using var tempDirectory = TempDirectory.Create(); + DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); + + var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); + + var application = ConfigFactory.FromFile(projectFile); + + // Need to add docker registry for generate + application.Registry = "test"; + + await GenerateHost.ExecuteGenerateAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); + + // name of application is the folder + var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); + var expectedContent = File.ReadAllText($"testassets/generate/{projectName}.yaml"); + + Assert.Equal(expectedContent, content); + } + + [ConditionalFact] + [SkipIfDockerNotRunning] + public async Task MultipleProjectGenerateTest() + { + var projectName = "multi-project"; + var environment = "production"; + + var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", projectName)); + using var tempDirectory = TempDirectory.Create(); + DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); + + var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); + + var application = ConfigFactory.FromFile(projectFile); + + // Need to add docker registry for generate + application.Registry = "test"; + + await GenerateHost.ExecuteGenerateAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); + + // name of application is the folder + var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); + var expectedContent = File.ReadAllText($"testassets/generate/{projectName}.yaml"); + + Assert.Equal(expectedContent, content); + } + } +} diff --git a/test/E2ETest/TyeInitTests.cs b/test/E2ETest/TyeInitTests.cs index cc837594..bf05da36 100644 --- a/test/E2ETest/TyeInitTests.cs +++ b/test/E2ETest/TyeInitTests.cs @@ -2,12 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.IO; -using System.Text.Json; -using Microsoft.Build.Construction; using Tye; -using Tye.ConfigModel; using Xunit; using Xunit.Abstractions; @@ -31,6 +27,8 @@ namespace E2ETest using var tempDirectory = TempDirectory.Create(); DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); + File.Delete(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); + var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "test-project.csproj")); var (content, _) = InitHost.CreateTyeFileContent(projectFile, force: false); diff --git a/test/E2ETest/testassets/generate/frontend-backend.yaml b/test/E2ETest/testassets/generate/frontend-backend.yaml new file mode 100644 index 00000000..5cef23ad --- /dev/null +++ b/test/E2ETest/testassets/generate/frontend-backend.yaml @@ -0,0 +1,101 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: backend + labels: + app.kubernetes.io/name: backend + app.kubernetes.io/part-of: frontend-backend +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: backend + template: + metadata: + labels: + app.kubernetes.io/name: backend + app.kubernetes.io/part-of: frontend-backend + spec: + containers: + - name: backend + image: test/backend:1.0.0 + imagePullPolicy: Always + env: + - name: ASPNETCORE_URLS + value: http://*:5050 + - name: SERVICE__FRONTEND__PORT + value: '5051' + - name: SERVICE__FRONTEND__HOST + value: 'frontend' + ports: + - containerPort: 5050 +... +--- +kind: Service +apiVersion: v1 +metadata: + name: backend + labels: + app.kubernetes.io/name: backend + app.kubernetes.io/part-of: frontend-backend +spec: + selector: + app.kubernetes.io/name: backend + type: ClusterIP + ports: + - name: backend + protocol: TCP + port: 5050 + targetPort: 5050 +... +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: frontend + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/part-of: frontend-backend +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: frontend + template: + metadata: + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/part-of: frontend-backend + spec: + containers: + - name: frontend + image: test/frontend:1.0.0 + imagePullPolicy: Always + env: + - name: ASPNETCORE_URLS + value: http://*:5051 + - name: SERVICE__BACKEND__PORT + value: '5050' + - name: SERVICE__BACKEND__HOST + value: 'backend' + ports: + - containerPort: 5051 +... +--- +kind: Service +apiVersion: v1 +metadata: + name: frontend + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/part-of: frontend-backend +spec: + selector: + app.kubernetes.io/name: frontend + type: ClusterIP + ports: + - name: frontend + protocol: TCP + port: 5051 + targetPort: 5051 +... diff --git a/test/E2ETest/testassets/generate/multi-project.yaml b/test/E2ETest/testassets/generate/multi-project.yaml new file mode 100644 index 00000000..224dac61 --- /dev/null +++ b/test/E2ETest/testassets/generate/multi-project.yaml @@ -0,0 +1,167 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: backend + labels: + app.kubernetes.io/name: backend + app.kubernetes.io/part-of: multi-project +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: backend + template: + metadata: + labels: + app.kubernetes.io/name: backend + app.kubernetes.io/part-of: multi-project + spec: + containers: + - name: backend + image: test/backend:1.0.0 + imagePullPolicy: Always + env: + - name: ASPNETCORE_URLS + value: http://*:7000 + - name: SERVICE__FRONTEND__PORT + value: '8000' + - name: SERVICE__FRONTEND__HOST + value: 'frontend' + volumeMounts: + - name: rabbit-rabbit + mountPath: /var/tye/bindings/rabbit-rabbit + readOnly: true + ports: + - containerPort: 7000 + volumes: + - name: rabbit-rabbit + secret: + secretName: binding-production-rabbit-rabbit-secret + items: + - key: connectionstring + path: CONNECTIONSTRING__RABBIT +... +--- +kind: Service +apiVersion: v1 +metadata: + name: backend + labels: + app.kubernetes.io/name: backend + app.kubernetes.io/part-of: multi-project +spec: + selector: + app.kubernetes.io/name: backend + type: ClusterIP + ports: + - name: backend + protocol: TCP + port: 7000 + targetPort: 7000 +... +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: frontend + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/part-of: multi-project +spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: frontend + template: + metadata: + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/part-of: multi-project + spec: + containers: + - name: frontend + image: test/frontend:1.0.0 + imagePullPolicy: Always + env: + - name: ASPNETCORE_URLS + value: http://*:8000 + - name: SERVICE__BACKEND__PORT + value: '7000' + - name: SERVICE__BACKEND__HOST + value: 'backend' + volumeMounts: + - name: rabbit-rabbit + mountPath: /var/tye/bindings/rabbit-rabbit + readOnly: true + ports: + - containerPort: 8000 + volumes: + - name: rabbit-rabbit + secret: + secretName: binding-production-rabbit-rabbit-secret + items: + - key: connectionstring + path: CONNECTIONSTRING__RABBIT +... +--- +kind: Service +apiVersion: v1 +metadata: + name: frontend + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/part-of: multi-project +spec: + selector: + app.kubernetes.io/name: frontend + type: ClusterIP + ports: + - name: frontend + protocol: TCP + port: 8000 + targetPort: 8000 +... +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: worker + labels: + app.kubernetes.io/name: worker + app.kubernetes.io/part-of: multi-project +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: worker + template: + metadata: + labels: + app.kubernetes.io/name: worker + app.kubernetes.io/part-of: multi-project + spec: + containers: + - name: worker + image: test/worker:1.0.0 + imagePullPolicy: Always + env: + - name: SERVICE__BACKEND__PORT + value: '7000' + - name: SERVICE__BACKEND__HOST + value: 'backend' + - name: SERVICE__FRONTEND__PORT + value: '8000' + - name: SERVICE__FRONTEND__HOST + value: 'frontend' + volumeMounts: + - name: rabbit-rabbit + mountPath: /var/tye/bindings/rabbit-rabbit + readOnly: true + volumes: + - name: rabbit-rabbit + secret: + secretName: binding-production-rabbit-rabbit-secret + items: + - key: connectionstring + path: CONNECTIONSTRING__RABBIT +... diff --git a/test/E2ETest/testassets/generate/single-project.yaml b/test/E2ETest/testassets/generate/single-project.yaml new file mode 100644 index 00000000..bab5a1d2 --- /dev/null +++ b/test/E2ETest/testassets/generate/single-project.yaml @@ -0,0 +1,51 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: test-project + labels: + app.kubernetes.io/name: test-project + app.kubernetes.io/part-of: single-project +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: test-project + template: + metadata: + labels: + app.kubernetes.io/name: test-project + app.kubernetes.io/part-of: single-project + spec: + containers: + - name: test-project + image: test/test-project:1.0.0 + imagePullPolicy: Always + env: + - name: ASPNETCORE_URLS + value: http://*:5000 + ports: + - containerPort: 5001 + - containerPort: 5000 +... +--- +kind: Service +apiVersion: v1 +metadata: + name: test-project + labels: + app.kubernetes.io/name: test-project + app.kubernetes.io/part-of: single-project +spec: + selector: + app.kubernetes.io/name: test-project + type: ClusterIP + ports: + - name: test-project + protocol: TCP + port: 5001 + targetPort: 5001 + - name: test-project + protocol: TCP + port: 5000 + targetPort: 5000 +...