diff --git a/test/E2ETests/RemoteDeploymentConfig.cs b/test/E2ETests/RemoteDeploymentConfig.cs index ba33a5ae0d..49465131fd 100644 --- a/test/E2ETests/RemoteDeploymentConfig.cs +++ b/test/E2ETests/RemoteDeploymentConfig.cs @@ -2,14 +2,42 @@ { public class RemoteDeploymentConfig { + /// + /// Name or IP address of the server to deploy to + /// public string ServerName { get; set; } + /// + /// Account name for the credentials required to setup a powershell session to the target server + /// public string AccountName { get; set; } + /// + /// Account password for the credentials required to setup a powershell session to the target server + /// public string AccountPassword { get; set; } + /// + /// The file share on the target server to copy the files to + /// public string FileSharePath { get; set; } - public string ExecutableRelativePath { get; set; } + /// + /// Location of the dotnet runtime zip file which is required for testing portable apps. + /// When both and properties + /// are provided, the property is preferred. + /// + public string DotnetRuntimeZipFilePath { get; set; } + + /// + /// Location of the dotnet runtime folder which is required for testing portable apps. + /// This property is probably more useful for users as they can point their local 'dotnet' runtime folder. + /// + public string DotnetRuntimeFolderPath { get; set; } + + /// + /// Path to the parent folder containing 'dotnet.exe' on remote server's file share + /// + public string DotnetRuntimePathOnShare { get; set; } } } diff --git a/test/E2ETests/SmokeTestsOnNanoServer.cs b/test/E2ETests/SmokeTestsOnNanoServer.cs index 20221f2c10..d6b8d9b0e3 100644 --- a/test/E2ETests/SmokeTestsOnNanoServer.cs +++ b/test/E2ETests/SmokeTestsOnNanoServer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Threading.Tasks; using E2ETests.Common; using Microsoft.AspNetCore.Server.Testing; @@ -12,24 +13,55 @@ using Xunit.Abstractions; namespace E2ETests { - public class SmokeTestsOnNanoServer : IDisposable + public class SmokeTestsOnNanoServerUsingStandaloneRuntime : IDisposable { + private readonly SmokeTestsOnNanoServer _smokeTestsOnNanoServer; private readonly XunitLogger _logger; private readonly RemoteDeploymentConfig _remoteDeploymentConfig; - public SmokeTestsOnNanoServer(ITestOutputHelper output) + public SmokeTestsOnNanoServerUsingStandaloneRuntime(ITestOutputHelper output) { _logger = new XunitLogger(output, LogLevel.Information); + _remoteDeploymentConfig = RemoteDeploymentConfigHelper.GetConfiguration(); + _smokeTestsOnNanoServer = new SmokeTestsOnNanoServer(output, _remoteDeploymentConfig, _logger); + } - var configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("remoteDeploymentConfig.json") - .AddUserSecrets() - .AddEnvironmentVariables() - .Build(); + [ConditionalTheory, Trait("E2Etests", "NanoServer")] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + [SkipIfEnvironmentVariableNotEnabled("RUN_TESTS_ON_NANO")] + [InlineData(ServerType.Kestrel, 5000, ApplicationType.Standalone)] + [InlineData(ServerType.WebListener, 5000, ApplicationType.Standalone)] + public async Task Test(ServerType serverType, int portToListen, ApplicationType applicationType) + { + var applicationBaseUrl = $"http://{_remoteDeploymentConfig.ServerName}:{portToListen}/"; + await _smokeTestsOnNanoServer.RunTestsAsync(serverType, applicationBaseUrl, applicationType); + } - _remoteDeploymentConfig = new RemoteDeploymentConfig(); - configuration.GetSection("NanoServer").Bind(_remoteDeploymentConfig); + public void Dispose() + { + _logger.Dispose(); + } + } + + // Tests here test portable app scenario, so we copy the dotnet runtime onto the + // target server's file share and after setting up a remote session to the server, we update the PATH environment + // to have the path to this copied dotnet runtime folder in the share. + // The dotnet runtime is copied only once for all the tests in this class. + public class SmokeTestsOnNanoServerUsingSharedRuntime + : IClassFixture, IDisposable + { + private readonly SmokeTestsOnNanoServer _smokeTestsOnNanoServer; + private readonly RemoteDeploymentConfig _remoteDeploymentConfig; + private readonly XunitLogger _logger; + + public SmokeTestsOnNanoServerUsingSharedRuntime( + DotnetRuntimeSetupTestFixture dotnetRuntimeSetupTestFixture, ITestOutputHelper output) + { + _logger = new XunitLogger(output, LogLevel.Information); + _remoteDeploymentConfig = RemoteDeploymentConfigHelper.GetConfiguration(); + _remoteDeploymentConfig.DotnetRuntimePathOnShare = dotnetRuntimeSetupTestFixture.DotnetRuntimePathOnShare; + _smokeTestsOnNanoServer = new SmokeTestsOnNanoServer(output, _remoteDeploymentConfig, _logger); } [ConditionalTheory, Trait("E2Etests", "NanoServer")] @@ -37,21 +69,169 @@ namespace E2ETests [OSSkipCondition(OperatingSystems.MacOSX)] [SkipIfEnvironmentVariableNotEnabled("RUN_TESTS_ON_NANO")] [InlineData(ServerType.Kestrel, 5000, ApplicationType.Portable)] - [InlineData(ServerType.Kestrel, 5000, ApplicationType.Standalone)] [InlineData(ServerType.WebListener, 5000, ApplicationType.Portable)] - [InlineData(ServerType.WebListener, 5000, ApplicationType.Standalone)] public async Task Test(ServerType serverType, int portToListen, ApplicationType applicationType) { var applicationBaseUrl = $"http://{_remoteDeploymentConfig.ServerName}:{portToListen}/"; - await RunTestsAsync(serverType, applicationBaseUrl, applicationType); + await _smokeTestsOnNanoServer.RunTestsAsync(serverType, applicationBaseUrl, applicationType); } - private async Task RunTestsAsync(ServerType serverType, string applicationBaseUrl, ApplicationType applicationType) + public void Dispose() { - using (_logger.BeginScope("SmokeTestSuite")) + _logger.Dispose(); + } + + // Copies dotnet runtime to the target server's file share. + public class DotnetRuntimeSetupTestFixture : IDisposable + { + public DotnetRuntimeSetupTestFixture() + { + RemoteDeploymentConfig = RemoteDeploymentConfigHelper.GetConfiguration(); + + DotnetRuntimePathOnShare = Path.Combine(RemoteDeploymentConfig.FileSharePath, "dotnet"); + + // Prefer copying the zip file to fileshare and extracting on file share over copying the extracted + // dotnet runtime folder from source to file share as the size could be significantly huge. + if (!string.IsNullOrEmpty(RemoteDeploymentConfig.DotnetRuntimeZipFilePath)) + { + if (!File.Exists(RemoteDeploymentConfig.DotnetRuntimeZipFilePath)) + { + throw new InvalidOperationException( + $"Expected dotnet runtime zip file at '{RemoteDeploymentConfig.DotnetRuntimeFolderPath}', but didn't find one."); + } + + ZippedDotnetRuntimePathOnShare = Path.Combine( + RemoteDeploymentConfig.FileSharePath, + Path.GetFileName(RemoteDeploymentConfig.DotnetRuntimeZipFilePath)); + + if (!File.Exists(ZippedDotnetRuntimePathOnShare)) + { + File.Copy(RemoteDeploymentConfig.DotnetRuntimeZipFilePath, ZippedDotnetRuntimePathOnShare, overwrite: true); + Console.WriteLine($"Copied the local dotnet zip folder '{RemoteDeploymentConfig.DotnetRuntimeZipFilePath}' " + + $"to the file share path '{RemoteDeploymentConfig.FileSharePath}'"); + } + + if (Directory.Exists(DotnetRuntimePathOnShare)) + { + Directory.Delete(DotnetRuntimePathOnShare, recursive: true); + } + + ZipFile.ExtractToDirectory(ZippedDotnetRuntimePathOnShare, DotnetRuntimePathOnShare); + Console.WriteLine($"Extracted dotnet runtime to folder '{DotnetRuntimePathOnShare}'"); + } + else if (!string.IsNullOrEmpty(RemoteDeploymentConfig.DotnetRuntimeFolderPath)) + { + if (!Directory.Exists(RemoteDeploymentConfig.DotnetRuntimeFolderPath)) + { + throw new InvalidOperationException( + $"Expected dotnet runtime folder at '{RemoteDeploymentConfig.DotnetRuntimeFolderPath}', but didn't find one."); + } + + Console.WriteLine($"Copying dotnet runtime folder from '{RemoteDeploymentConfig.DotnetRuntimeFolderPath}' to '{DotnetRuntimePathOnShare}'."); + Console.WriteLine("This could take some time."); + + DirectoryCopy(RemoteDeploymentConfig.DotnetRuntimeFolderPath, DotnetRuntimePathOnShare, copySubDirs: true); + } + else + { + throw new InvalidOperationException("Dotnet runtime is required to be copied for testing portable apps scenario. " + + $"Either supply '{nameof(RemoteDeploymentConfig.DotnetRuntimeFolderPath)}' containing the unzipped dotnet runtime content or " + + $"supply the dotnet runtime zip file path via '{nameof(RemoteDeploymentConfig.DotnetRuntimeZipFilePath)}'."); + } + } + + public RemoteDeploymentConfig RemoteDeploymentConfig { get; } + + public string ZippedDotnetRuntimePathOnShare { get; } + + public string DotnetRuntimePathOnShare { get; } + + private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs) + { + var dir = new DirectoryInfo(sourceDirName); + + if (!dir.Exists) + { + throw new DirectoryNotFoundException( + "Source directory does not exist or could not be found: " + + sourceDirName); + } + + var dirs = dir.GetDirectories(); + if (!Directory.Exists(destDirName)) + { + Directory.CreateDirectory(destDirName); + } + + var files = dir.GetFiles(); + foreach (var file in files) + { + var temppath = Path.Combine(destDirName, file.Name); + file.CopyTo(temppath, false); + } + + if (copySubDirs) + { + foreach (var subdir in dirs) + { + var temppath = Path.Combine(destDirName, subdir.Name); + DirectoryCopy(subdir.FullName, temppath, copySubDirs); + } + } + } + + public void Dispose() + { + // In case the source is provided as a folder itself, then we wouldn't have the zip file to begin with. + if (!string.IsNullOrEmpty(ZippedDotnetRuntimePathOnShare)) + { + try + { + Console.WriteLine($"Deleting the dotnet runtime zip file '{ZippedDotnetRuntimePathOnShare}'"); + File.Delete(ZippedDotnetRuntimePathOnShare); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to delete the dotnet runtime zip file '{ZippedDotnetRuntimePathOnShare}'. Exception: " + + ex.ToString()); + } + } + + try + { + Console.WriteLine($"Deleting the dotnet runtime folder '{DotnetRuntimePathOnShare}'"); + Directory.Delete(DotnetRuntimePathOnShare, recursive: true); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to delete the dotnet runtime folder '{DotnetRuntimePathOnShare}'. Exception: " + + ex.ToString()); + } + } + } + } + + class SmokeTestsOnNanoServer + { + private readonly XunitLogger _logger; + private readonly RemoteDeploymentConfig _remoteDeploymentConfig; + + public SmokeTestsOnNanoServer(ITestOutputHelper output, RemoteDeploymentConfig config, XunitLogger logger) + { + _logger = logger; + _remoteDeploymentConfig = config; + } + + public async Task RunTestsAsync( + ServerType serverType, + string applicationBaseUrl, + ApplicationType applicationType) + { + using (_logger.BeginScope(nameof(SmokeTestsOnNanoServerUsingStandaloneRuntime))) { var deploymentParameters = new RemoteWindowsDeploymentParameters( Helpers.GetApplicationPath(applicationType), + _remoteDeploymentConfig.DotnetRuntimePathOnShare, serverType, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, @@ -75,10 +255,28 @@ namespace E2ETests } } } + } - public void Dispose() + static class RemoteDeploymentConfigHelper + { + private static RemoteDeploymentConfig _remoteDeploymentConfig; + + public static RemoteDeploymentConfig GetConfiguration() { - _logger.Dispose(); + if (_remoteDeploymentConfig == null) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("remoteDeploymentConfig.json") + .AddUserSecrets() + .AddEnvironmentVariables() + .Build(); + + _remoteDeploymentConfig = new RemoteDeploymentConfig(); + configuration.GetSection("NanoServer").Bind(_remoteDeploymentConfig); + } + + return _remoteDeploymentConfig; } } } diff --git a/test/E2ETests/remoteDeploymentConfig.json b/test/E2ETests/remoteDeploymentConfig.json index 3bdc799e85..1d2a55f1d1 100644 --- a/test/E2ETests/remoteDeploymentConfig.json +++ b/test/E2ETests/remoteDeploymentConfig.json @@ -4,6 +4,7 @@ "AccountName": "", "AccountPassword": "", "FileSharePath": "", - "ExecutableRelativePath": "" + "DotnetRuntimeFolderPath": "", + "DotnetRuntimeZipFilePath": "" } }