From adf05b230e75213fa9453de83f009e2b838502ab Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sun, 22 Mar 2020 23:42:42 -0700 Subject: [PATCH] Fix tests --- .../Microsoft.Tye.Core.csproj | 2 +- src/Microsoft.Tye.Core/ProjectReader.cs | 136 ++++++++++++------ src/Microsoft.Tye.Hosting/DockerRunner.cs | 4 + .../Microsoft.Tye.Hosting.csproj | 1 - src/shared/TempDirectory.cs | 22 ++- src/tye/ConfigModel/ConfigFactory.cs | 22 +-- test/E2ETest/E2ETest.csproj | 10 ++ .../ConditionalFactAttribute.cs | 2 +- .../Infrastructure/SkipOnLinuxAttribute.cs | 23 +++ test/E2ETest/TestOutputLogEventSink.cs | 3 +- test/E2ETest/TyeRunTests.cs | 28 +++- .../testassets/generate/frontend-backend.yaml | 4 + .../testassets/generate/multi-project.yaml | 4 + .../generate/single-project-noregistry.yaml | 2 + .../testassets/generate/single-project.yaml | 2 + 15 files changed, 191 insertions(+), 74 deletions(-) create mode 100644 test/E2ETest/Infrastructure/SkipOnLinuxAttribute.cs diff --git a/src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj b/src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj index 16e4ad6e..53ba752d 100644 --- a/src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj +++ b/src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj @@ -16,7 +16,7 @@ The Microsoft.Build.Locator package takes care of dynamically loading these assemblies at runtime. We don't need/want to ship them, just to have them as references. --> - + diff --git a/src/Microsoft.Tye.Core/ProjectReader.cs b/src/Microsoft.Tye.Core/ProjectReader.cs index 427b8d0b..326af38b 100644 --- a/src/Microsoft.Tye.Core/ProjectReader.cs +++ b/src/Microsoft.Tye.Core/ProjectReader.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -10,7 +11,9 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Loader; using System.Threading.Tasks; +using Microsoft.Build.Construction; using Microsoft.Build.Definition; +using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Locator; @@ -21,17 +24,46 @@ namespace Microsoft.Tye { public static class ProjectReader { + private static object @lock = new object(); private static bool registered; - public static Task ReadProjectDetailsAsync(OutputContext output, FileInfo projectFile, Project project) + + public static IEnumerable EnumerateProjects(FileInfo solutionFile) { - if (output is null) + EnsureMSBuildRegistered(null, solutionFile); + return EnumerateProjectsCore(solutionFile); + } + + // Do not load MSBuild types before using EnsureMSBuildRegistered. + [MethodImpl(MethodImplOptions.NoInlining)] + private static IEnumerable EnumerateProjectsCore(FileInfo solutionFile) + { + var solution = SolutionFile.Parse(solutionFile.FullName); + foreach (var project in solution.ProjectsInOrder) { - throw new ArgumentNullException(nameof(output)); + if (project.ProjectType != SolutionProjectType.KnownToBeMSBuildFormat) + { + continue; + } + + var extension = Path.GetExtension(project.AbsolutePath).ToLower(); + switch (extension) + { + case ".csproj": + case ".fsproj": + break; + default: + continue; + } + + yield return new FileInfo(project.AbsolutePath.Replace('\\', '/')); } + } - if (projectFile is null) + public static Task ReadProjectDetailsAsync(OutputContext output, FileInfo projectFile, Project project) + { + if (output is null) { - throw new ArgumentNullException(nameof(projectFile)); + throw new ArgumentNullException(nameof(output)); } if (project is null) @@ -39,54 +71,63 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(project)); } - EnsureMSBuildRegistered(projectFile); + EnsureMSBuildRegistered(output, projectFile); - using (var step = output.BeginStep("Reading Project Details...")) - { - EvaluateProject(output, projectFile, project); + EvaluateProject(output, projectFile, project); - if (!SemVersion.TryParse(project.Version, out var version)) - { - output.WriteInfoLine($"No version or invalid version '{project.Version}' found, using default."); - version = new SemVersion(0, 1, 0); - project.Version = version.ToString(); - } - - step.MarkComplete(); + if (!SemVersion.TryParse(project.Version, out var version)) + { + output.WriteInfoLine($"No version or invalid version '{project.Version}' found, using default."); + version = new SemVersion(0, 1, 0); + project.Version = version.ToString(); } return Task.CompletedTask; } - private static void EnsureMSBuildRegistered(FileInfo projectFile) + private static void EnsureMSBuildRegistered(OutputContext? output, FileInfo projectFile) { if (!registered) { - // It says VisualStudio - but we'll just use .NET SDK - var instances = MSBuildLocator.QueryVisualStudioInstances(new VisualStudioInstanceQueryOptions() + lock (@lock) { - DiscoveryTypes = DiscoveryType.DotNetSdk, - - // Using the project as the working directory. We're making the assumption that - // all of the projects want to use the same SDK version. This library is going - // load a single version of the SDK's assemblies into our process, so we can't - // use supprt SDKs at once without getting really tricky. - // - // The .NET SDK-based discovery uses `dotnet --info` and returns the SDK - // in use for the directory. - // - // https://github.com/microsoft/MSBuildLocator/blob/master/src/MSBuildLocator/MSBuildLocator.cs#L320 - WorkingDirectory = projectFile.DirectoryName, - }); + output?.WriteDebugLine("Locating .NET SDK..."); - var instance = instances.SingleOrDefault(); - if (instance == null) - { - throw new CommandException("Failed to find dotnet. Make sure the .NET SDK is installed and on the PATH."); - } + // It says VisualStudio - but we'll just use .NET SDK + var instances = MSBuildLocator.QueryVisualStudioInstances(new VisualStudioInstanceQueryOptions() + { + DiscoveryTypes = DiscoveryType.DotNetSdk, + + // Using the project as the working directory. We're making the assumption that + // all of the projects want to use the same SDK version. This library is going + // load a single version of the SDK's assemblies into our process, so we can't + // use supprt SDKs at once without getting really tricky. + // + // The .NET SDK-based discovery uses `dotnet --info` and returns the SDK + // in use for the directory. + // + // https://github.com/microsoft/MSBuildLocator/blob/master/src/MSBuildLocator/MSBuildLocator.cs#L320 + WorkingDirectory = projectFile.DirectoryName, + }); + + var instance = instances.SingleOrDefault(); + if (instance == null) + { + throw new CommandException("Failed to find dotnet. Make sure the .NET SDK is installed and on the PATH."); + } - MSBuildLocator.RegisterInstance(instance); - registered = true; + output?.WriteDebugLine("Found .NET SDK at: " + instance.MSBuildPath); + + try + { + MSBuildLocator.RegisterInstance(instance); + output?.WriteDebug("Registered .NET SDK."); + } + finally + { + registered = true; + } + } } } @@ -96,16 +137,25 @@ namespace Microsoft.Tye { var sw = Stopwatch.StartNew(); + // We need to isolate projects from each other for testing. MSBuild does not support + // loading the same project twice in the same collection. + var projectCollection = new ProjectCollection(); + ProjectInstance projectInstance; + try { output.WriteDebugLine($"Loading project '{projectFile.FullName}'."); - projectInstance = ProjectInstance.FromFile(projectFile.FullName, new ProjectOptions()); + var msbuildProject = Microsoft.Build.Evaluation.Project.FromFile(projectFile.FullName, new ProjectOptions() + { + ProjectCollection = projectCollection, + }); + projectInstance = msbuildProject.CreateProjectInstance(); output.WriteDebugLine($"Loaded project '{projectFile.FullName}'."); } - catch + catch (Exception ex) { - throw new CommandException($"Failed to load project: '{projectFile.FullName}'."); + throw new CommandException($"Failed to load project: '{projectFile.FullName}'.", ex); } // Currently we only log at debug level. diff --git a/src/Microsoft.Tye.Hosting/DockerRunner.cs b/src/Microsoft.Tye.Hosting/DockerRunner.cs index 7c83bfed..86ca02e1 100644 --- a/src/Microsoft.Tye.Hosting/DockerRunner.cs +++ b/src/Microsoft.Tye.Hosting/DockerRunner.cs @@ -129,6 +129,10 @@ namespace Microsoft.Tye.Hosting environment["PORT"] = string.Join(";", ports.Select(p => $"{p.InternalPort ?? p.Port}")); } + // See: https://github.com/docker/for-linux/issues/264 + // + // The way we do proxying here doesn't really work for multi-container scenarios on linux + // without some more setup. application.PopulateEnvironment(service, (key, value) => environment[key] = value, "host.docker.internal"); environment["APP_INSTANCE"] = replica; diff --git a/src/Microsoft.Tye.Hosting/Microsoft.Tye.Hosting.csproj b/src/Microsoft.Tye.Hosting/Microsoft.Tye.Hosting.csproj index 2eb8638b..10d54720 100644 --- a/src/Microsoft.Tye.Hosting/Microsoft.Tye.Hosting.csproj +++ b/src/Microsoft.Tye.Hosting/Microsoft.Tye.Hosting.csproj @@ -19,7 +19,6 @@ - diff --git a/src/shared/TempDirectory.cs b/src/shared/TempDirectory.cs index 0f9f0bb0..ad87d47f 100644 --- a/src/shared/TempDirectory.cs +++ b/src/shared/TempDirectory.cs @@ -4,16 +4,30 @@ using System; using System.IO; +using System.Runtime.InteropServices; namespace Microsoft.Tye { internal class TempDirectory : IDisposable { - public static TempDirectory Create() + public static TempDirectory Create(bool preferUserDirectoryOnMacOS = false) { - var directoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - var directoryInfo = Directory.CreateDirectory(directoryPath); - return new TempDirectory(directoryPath, directoryInfo); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && preferUserDirectoryOnMacOS) + { + var baseDirectory = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + baseDirectory = Path.Combine(baseDirectory, ".tye"); + Directory.CreateDirectory(baseDirectory); + + var directoryPath = Path.Combine(baseDirectory, Path.GetRandomFileName()); + var directoryInfo = Directory.CreateDirectory(directoryPath); + return new TempDirectory(directoryPath, directoryInfo); + } + else + { + var directoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var directoryInfo = Directory.CreateDirectory(directoryPath); + return new TempDirectory(directoryPath, directoryInfo); + } } private TempDirectory(string directoryPath, DirectoryInfo directoryInfo) diff --git a/src/tye/ConfigModel/ConfigFactory.cs b/src/tye/ConfigModel/ConfigFactory.cs index 4568798e..3d24405a 100644 --- a/src/tye/ConfigModel/ConfigFactory.cs +++ b/src/tye/ConfigModel/ConfigFactory.cs @@ -57,25 +57,11 @@ namespace Microsoft.Tye.ConfigModel Source = file, }; - var solution = SolutionFile.Parse(file.FullName); - foreach (var project in solution.ProjectsInOrder) + // BE CAREFUL modifying this code. Avoid proliferating MSBuild types + // throughout the code, because we load them dynamically. + foreach (var projectFile in ProjectReader.EnumerateProjects(file)) { - if (project.ProjectType != SolutionProjectType.KnownToBeMSBuildFormat) - { - continue; - } - - var extension = Path.GetExtension(project.AbsolutePath).ToLower(); - switch (extension) - { - case ".csproj": - case ".fsproj": - break; - default: - continue; - } - - var description = CreateService(new FileInfo(project.AbsolutePath.Replace('\\', '/'))); + var description = CreateService(projectFile); if (description != null) { application.Services.Add(description); diff --git a/test/E2ETest/E2ETest.csproj b/test/E2ETest/E2ETest.csproj index f18097d9..0df1fa54 100644 --- a/test/E2ETest/E2ETest.csproj +++ b/test/E2ETest/E2ETest.csproj @@ -13,6 +13,16 @@ + + + + + + + diff --git a/test/E2ETest/Infrastructure/ConditionalFactAttribute.cs b/test/E2ETest/Infrastructure/ConditionalFactAttribute.cs index 4559f87d..6047c821 100644 --- a/test/E2ETest/Infrastructure/ConditionalFactAttribute.cs +++ b/test/E2ETest/Infrastructure/ConditionalFactAttribute.cs @@ -5,7 +5,7 @@ using Xunit.Sdk; namespace E2ETest { [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - [XunitTestCaseDiscoverer("E2ETest." + nameof(ConditionalFactDiscoverer), "E2ETest")] + [XunitTestCaseDiscoverer("E2ETest." + nameof(ConditionalFactDiscoverer), "Microsoft.Tye.E2ETest")] public class ConditionalFactAttribute : FactAttribute { } diff --git a/test/E2ETest/Infrastructure/SkipOnLinuxAttribute.cs b/test/E2ETest/Infrastructure/SkipOnLinuxAttribute.cs new file mode 100644 index 00000000..37d040c2 --- /dev/null +++ b/test/E2ETest/Infrastructure/SkipOnLinuxAttribute.cs @@ -0,0 +1,23 @@ +// 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 System.Runtime.InteropServices; + +namespace E2ETest +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)] + internal class SkipOnLinuxAttribute : Attribute, ITestCondition + { + public SkipOnLinuxAttribute() + { + IsMet = !RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + SkipReason = "This is linux. Here's a free bus-ticket to skipsville."; + } + + public bool IsMet { get; } + + public string SkipReason { get; set; } + } +} diff --git a/test/E2ETest/TestOutputLogEventSink.cs b/test/E2ETest/TestOutputLogEventSink.cs index b7b2e70a..d42f2c67 100644 --- a/test/E2ETest/TestOutputLogEventSink.cs +++ b/test/E2ETest/TestOutputLogEventSink.cs @@ -32,7 +32,8 @@ namespace E2ETest public void Write(string value) { - output.WriteLine(value); + // our usage of IConsole includes newlines, so strip them out. + output.WriteLine(value.TrimEnd('\r', '\n')); } } } diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index f5463103..4196f3e6 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -137,10 +137,11 @@ namespace E2ETest [ConditionalFact] [SkipIfDockerNotRunning] + [SkipOnLinux] public async Task FrontendBackendRunTestWithDocker() { var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "frontend-backend")); - using var tempDirectory = TempDirectory.Create(); + using var tempDirectory = TempDirectory.Create(preferUserDirectoryOnMacOS: true); DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); @@ -165,8 +166,8 @@ namespace E2ETest var dashboardUri = new Uri(host.DashboardWebApplication!.Addresses.First()); - await CheckServiceIsUp(host.Application, client, "backend", dashboardUri); - await CheckServiceIsUp(host.Application, client, "frontend", dashboardUri); + await CheckServiceIsUp(host.Application, client, "backend", dashboardUri, timeout: TimeSpan.FromSeconds(60)); + await CheckServiceIsUp(host.Application, client, "frontend", dashboardUri, timeout: TimeSpan.FromSeconds(60)); } finally { @@ -174,7 +175,7 @@ namespace E2ETest } } - private async Task CheckServiceIsUp(Microsoft.Tye.Hosting.Model.Application application, HttpClient client, string serviceName, Uri dashboardUri) + private async Task CheckServiceIsUp(Microsoft.Tye.Hosting.Model.Application application, HttpClient client, string serviceName, Uri dashboardUri, TimeSpan? timeout = default) { // make sure backend is up before frontend var dashboardString = await client.GetStringAsync($"{dashboardUri}api/v1/services/{serviceName}"); @@ -183,11 +184,28 @@ namespace E2ETest var binding = service.Description!.Bindings.Where(b => b.Protocol == "http").Single(); var uriBackendProcess = new Uri($"{binding.Protocol}://localhost:{binding.Port}"); + var startTime = DateTime.UtcNow; try { + // Wait up until the timeout to see if we can access the service. + // For instance if we have to pull a base-image it can take a while. + while (timeout.HasValue && startTime + timeout.Value > DateTime.UtcNow) + { + try + { + await client.GetAsync(uriBackendProcess); + break; + } + catch (HttpRequestException) + { + await Task.Delay(TimeSpan.FromSeconds(3)); + } + } + var appResponse = await client.GetAsync(uriBackendProcess); - Assert.Equal(HttpStatusCode.OK, appResponse.StatusCode); var content = await appResponse.Content.ReadAsStringAsync(); + output.WriteLine(content); + Assert.Equal(HttpStatusCode.OK, appResponse.StatusCode); if (serviceName == "frontend") { Assert.Matches("Frontend Listening IP: (.+)\n", content); diff --git a/test/E2ETest/testassets/generate/frontend-backend.yaml b/test/E2ETest/testassets/generate/frontend-backend.yaml index 5b2ef157..a4e061e9 100644 --- a/test/E2ETest/testassets/generate/frontend-backend.yaml +++ b/test/E2ETest/testassets/generate/frontend-backend.yaml @@ -23,6 +23,8 @@ spec: env: - name: ASPNETCORE_URLS value: 'http://*' + - name: PORT + value: '80' - name: SERVICE__FRONTEND__PROTOCOL value: 'http' - name: SERVICE__FRONTEND__PORT @@ -76,6 +78,8 @@ spec: env: - name: ASPNETCORE_URLS value: 'http://*' + - name: PORT + value: '80' - name: SERVICE__BACKEND__PROTOCOL value: 'http' - name: SERVICE__BACKEND__PORT diff --git a/test/E2ETest/testassets/generate/multi-project.yaml b/test/E2ETest/testassets/generate/multi-project.yaml index 630dc259..7734f44e 100644 --- a/test/E2ETest/testassets/generate/multi-project.yaml +++ b/test/E2ETest/testassets/generate/multi-project.yaml @@ -23,6 +23,8 @@ spec: env: - name: ASPNETCORE_URLS value: 'http://*:7000' + - name: PORT + value: '7000' - name: SERVICE__FRONTEND__PROTOCOL value: 'http' - name: SERVICE__FRONTEND__PORT @@ -87,6 +89,8 @@ spec: env: - name: ASPNETCORE_URLS value: 'http://*:8000' + - name: PORT + value: '8000' - name: SERVICE__BACKEND__PROTOCOL value: 'http' - name: SERVICE__BACKEND__PORT diff --git a/test/E2ETest/testassets/generate/single-project-noregistry.yaml b/test/E2ETest/testassets/generate/single-project-noregistry.yaml index f5d5d0ec..9e33747f 100644 --- a/test/E2ETest/testassets/generate/single-project-noregistry.yaml +++ b/test/E2ETest/testassets/generate/single-project-noregistry.yaml @@ -23,6 +23,8 @@ spec: env: - name: ASPNETCORE_URLS value: 'http://*' + - name: PORT + value: '80' ports: - containerPort: 80 ... diff --git a/test/E2ETest/testassets/generate/single-project.yaml b/test/E2ETest/testassets/generate/single-project.yaml index 0717d032..70c70507 100644 --- a/test/E2ETest/testassets/generate/single-project.yaml +++ b/test/E2ETest/testassets/generate/single-project.yaml @@ -23,6 +23,8 @@ spec: env: - name: ASPNETCORE_URLS value: 'http://*' + - name: PORT + value: '80' ports: - containerPort: 80 ...