diff --git a/MusicStore.sln b/MusicStore.sln index 0c409e95f2..66539db055 100644 --- a/MusicStore.sln +++ b/MusicStore.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26405.2 +VisualStudioVersion = 15.0.26228.9 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7D749BDA-4638-4517-B66A-D40DEDEEB141}" ProjectSection(SolutionItems) = preProject @@ -17,6 +17,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MusicStore.Test", "test\Mus EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MusicStore.E2ETests", "test\MusicStore.E2ETests\MusicStore.E2ETests.csproj", "{72A5F455-121F-4954-BF28-D712C6BE88EA}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{88A30728-49E5-46C5-9CEC-9D8FD346A043}" + ProjectSection(SolutionItems) = preProject + build\dependencies.props = build\dependencies.props + build\repo.targets = build\repo.targets + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/build/dependencies.props b/build/dependencies.props index 181df87181..95607dedc7 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -13,6 +13,7 @@ 4.3.0 $(BundledNETStandardPackageVersion) + 4.0.0 15.0.0 2.2.0 diff --git a/build/repo.targets b/build/repo.targets index 6940b72667..f0b024c20b 100644 --- a/build/repo.targets +++ b/build/repo.targets @@ -1,10 +1,44 @@ + $(PrepareDependsOn);UpdateNuGetConfig $(RepositoryRoot)test\MusicStore.E2ETests\MusicStore.E2ETests.csproj - + + + + + + + + + + + VSTestTestCaseFilter=E2ETests=NanoServer + + + + + + + + + VSTestTestCaseFilter=smoketests=usestore + + + + + @@ -12,4 +46,5 @@ + diff --git a/samples/MusicStore/MusicStore.csproj b/samples/MusicStore/MusicStore.csproj index 1b97253010..51ff611dfd 100644 --- a/samples/MusicStore/MusicStore.csproj +++ b/samples/MusicStore/MusicStore.csproj @@ -7,32 +7,15 @@ netcoreapp2.0 $(DefineConstants);DEMO true - win7-x86;win7-x64;linux-x64;osx-x64 + win7-x86;win7-x64;linux-x64;osx-x64 - + - + - - - - - - - - - - - - - - - - - diff --git a/samples/MusicStore/Program.cs b/samples/MusicStore/Program.cs index 7c491669dd..a0a71b10dc 100644 --- a/samples/MusicStore/Program.cs +++ b/samples/MusicStore/Program.cs @@ -53,11 +53,9 @@ namespace MusicStore builder.ConfigureLogging(factory => { - factory.AddConsole(); - var logLevel = string.Equals(environment, "Development", StringComparison.Ordinal) ? LogLevel.Information : LogLevel.Warning; - factory.AddFilter("Console", level => level >= logLevel); + factory.AddConsole(); }); var host = builder.Build(); diff --git a/test/MusicStore.E2ETests/MusicStore.E2ETests.csproj b/test/MusicStore.E2ETests/MusicStore.E2ETests.csproj index c78ca94989..7bda4710aa 100644 --- a/test/MusicStore.E2ETests/MusicStore.E2ETests.csproj +++ b/test/MusicStore.E2ETests/MusicStore.E2ETests.csproj @@ -15,11 +15,14 @@ + + + diff --git a/test/MusicStore.E2ETests/SmokeTestsUsingStore/BaseStoreSetupFixture.cs b/test/MusicStore.E2ETests/SmokeTestsUsingStore/BaseStoreSetupFixture.cs new file mode 100644 index 0000000000..c53658f22e --- /dev/null +++ b/test/MusicStore.E2ETests/SmokeTestsUsingStore/BaseStoreSetupFixture.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace E2ETests +{ + public class BaseStoreSetupFixture : IDisposable + { + private readonly IDisposable _logToken; + private readonly ILogger _logger; + private readonly Store _store; + + public BaseStoreSetupFixture(bool createInDefaultLocation, string loggerName) + { + if (!Store.IsEnabled()) + { + return; + } + + var testLog = AssemblyTestLog.ForAssembly(typeof(BaseStoreSetupFixture).Assembly); + ILoggerFactory loggerFactory; + _logToken = testLog.StartTestLog(null, loggerName, out loggerFactory, testName: loggerName); + _logger = loggerFactory.CreateLogger(); + + CreateStoreInDefaultLocation = createInDefaultLocation; + + _logger.LogInformation( + "Setting up store in the location: {location}", + createInDefaultLocation ? "default" : "custom"); + + _store = new Store(loggerFactory); + + StoreDirectory = _store.CreateStore(createInDefaultLocation); + } + + public bool CreateStoreInDefaultLocation { get; } + + public string StoreDirectory { get; } + + public void Dispose() + { + if (_store != null) + { + _store.Dispose(); + } + + if (_logToken != null) + { + _logToken.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/test/MusicStore.E2ETests/SmokeTestsUsingStore/SetupFixtures.cs b/test/MusicStore.E2ETests/SmokeTestsUsingStore/SetupFixtures.cs new file mode 100644 index 0000000000..97ace4d70d --- /dev/null +++ b/test/MusicStore.E2ETests/SmokeTestsUsingStore/SetupFixtures.cs @@ -0,0 +1,22 @@ +namespace E2ETests +{ + public class DefaultLocationSetupFixture : BaseStoreSetupFixture + { + public DefaultLocationSetupFixture() : + base( + createInDefaultLocation: true, + loggerName: nameof(DefaultLocationSetupFixture)) + { + } + } + + public class CustomLocationSetupFixture : BaseStoreSetupFixture + { + public CustomLocationSetupFixture() : + base( + createInDefaultLocation: false, + loggerName: nameof(CustomLocationSetupFixture)) + { + } + } +} diff --git a/test/MusicStore.E2ETests/SmokeTestsUsingStore/SmokeTestsUsingStore.cs b/test/MusicStore.E2ETests/SmokeTestsUsingStore/SmokeTestsUsingStore.cs new file mode 100644 index 0000000000..f8a970ecf3 --- /dev/null +++ b/test/MusicStore.E2ETests/SmokeTestsUsingStore/SmokeTestsUsingStore.cs @@ -0,0 +1,92 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; +using Xunit.Abstractions; + +namespace E2ETests +{ + public class SmokeTestsUsingDefaultLocation : IClassFixture + { + private readonly DefaultLocationSetupFixture _testFixture; + private readonly ITestOutputHelper _output; + + public SmokeTestsUsingDefaultLocation( + DefaultLocationSetupFixture testFixure, + ITestOutputHelper output) + { + _testFixture = testFixure; + _output = output; + } + + [EnvironmentVariableSkipCondition(Store.MusicStoreAspNetCoreStoreFeed, null, SkipOnMatch = false)] + [ConditionalFact] + [Trait("smoketests", "usestore")] + [Trait("smoketests", "usestore-defaultlocation")] + public async Task DefaultLocation_Kestrel() + { + var tests = new SmokeTestsUsingStoreHelper(_output); + await tests.SmokeTestSuite( + ServerType.Kestrel, + _testFixture.CreateStoreInDefaultLocation, + _testFixture.StoreDirectory); + } + + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + [EnvironmentVariableSkipCondition(Store.MusicStoreAspNetCoreStoreFeed, null, SkipOnMatch = false)] + [ConditionalFact] + [Trait("smoketests", "usestore")] + [Trait("smoketests", "usestore-defaultlocation")] + public async Task DefaultLocation_WebListener() + { + var tests = new SmokeTestsUsingStoreHelper(_output); + await tests.SmokeTestSuite( + ServerType.WebListener, + _testFixture.CreateStoreInDefaultLocation, + _testFixture.StoreDirectory); + } + } + + public class SmokeTestsUsingInCustomLocation : IClassFixture + { + private readonly CustomLocationSetupFixture _testFixture; + private readonly ITestOutputHelper _output; + + public SmokeTestsUsingInCustomLocation( + CustomLocationSetupFixture testFixure, + ITestOutputHelper output) + { + _testFixture = testFixure; + _output = output; + } + + [EnvironmentVariableSkipCondition(Store.MusicStoreAspNetCoreStoreFeed, null, SkipOnMatch = false)] + [ConditionalFact] + [Trait("smoketests", "usestore")] + [Trait("smoketests", "usestore-customlocation")] + public async Task CustomLocation_Kestrel() + { + var tests = new SmokeTestsUsingStoreHelper(_output); + await tests.SmokeTestSuite( + ServerType.Kestrel, + _testFixture.CreateStoreInDefaultLocation, + _testFixture.StoreDirectory); + } + + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + [EnvironmentVariableSkipCondition(Store.MusicStoreAspNetCoreStoreFeed, null, SkipOnMatch = false)] + [ConditionalFact] + [Trait("smoketests", "usestore")] + [Trait("smoketests", "usestore-customlocation")] + public async Task CustomLocation_WebListener() + { + var tests = new SmokeTestsUsingStoreHelper(_output); + await tests.SmokeTestSuite( + ServerType.WebListener, + _testFixture.CreateStoreInDefaultLocation, + _testFixture.StoreDirectory); + } + } +} \ No newline at end of file diff --git a/test/MusicStore.E2ETests/SmokeTestsUsingStore/SmokeTestsUsingStoreHelper.cs b/test/MusicStore.E2ETests/SmokeTestsUsingStore/SmokeTestsUsingStoreHelper.cs new file mode 100644 index 0000000000..4961248e61 --- /dev/null +++ b/test/MusicStore.E2ETests/SmokeTestsUsingStore/SmokeTestsUsingStoreHelper.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.Extensions.Logging.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace E2ETests +{ + public class SmokeTestsUsingStoreHelper : LoggedTest + { + public SmokeTestsUsingStoreHelper(ITestOutputHelper output) : base(output) + { + } + + public async Task SmokeTestSuite(ServerType serverType, bool isStoreInDefaultLocation, string storeDirectory) + { + var targetFramework = "netcoreapp2.0"; + var testName = $"SmokeTestsUsingStore_{serverType}"; + using (StartLog(out var loggerFactory, testName)) + { + var logger = loggerFactory.CreateLogger(nameof(SmokeTestsUsingStoreHelper)); + var musicStoreDbName = DbUtils.GetUniqueName(); + + var deploymentParameters = new DeploymentParameters( + Helpers.GetApplicationPath(ApplicationType.Portable), serverType, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64) + { + EnvironmentName = "SocialTesting", + SiteName = "MusicStoreTestSiteUsingStore", + PublishApplicationBeforeDeployment = true, + PreservePublishedApplicationForDebugging = Helpers.PreservePublishedApplicationForDebugging, + TargetFramework = targetFramework, + Configuration = Helpers.GetCurrentBuildConfiguration(), + ApplicationType = ApplicationType.Portable, + UserAdditionalCleanup = parameters => + { + DbUtils.DropDatabase(musicStoreDbName, logger); + } + }; + + // Override the connection strings using environment based configuration + deploymentParameters.EnvironmentVariables + .Add(new KeyValuePair( + MusicStoreConfig.ConnectionStringKey, + DbUtils.CreateConnectionString(musicStoreDbName))); + + if (!isStoreInDefaultLocation) + { + deploymentParameters.EnvironmentVariables.Add( + new KeyValuePair("DOTNET_SHARED_STORE", storeDirectory)); + } + + using (var deployer = ApplicationDeployerFactory.Create(deploymentParameters, loggerFactory)) + { + var deploymentResult = await deployer.DeployAsync(); + + var mvcCoreDllPath = Path.Combine(deploymentResult.ContentRoot, "Microsoft.AspNetCore.Mvc.Core.dll"); + var fileInfo = new FileInfo(mvcCoreDllPath); + Assert.False( + File.Exists(mvcCoreDllPath), + $"The file '{fileInfo.Name}.{fileInfo.Extension}' was not expected to be present in the publish directory"); + + await SmokeTestHelper.RunTestsAsync(deploymentResult, logger); + } + } + } + } +} diff --git a/test/MusicStore.E2ETests/SmokeTestsUsingStore/Store.cs b/test/MusicStore.E2ETests/SmokeTestsUsingStore/Store.cs new file mode 100644 index 0000000000..806d71e998 --- /dev/null +++ b/test/MusicStore.E2ETests/SmokeTestsUsingStore/Store.cs @@ -0,0 +1,247 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Logging; +using NuGet.Configuration; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace E2ETests +{ + internal class Store : IDisposable + { + public const string MusicStoreAspNetCoreStoreFeed = "MUSICSTORE_ASPNETCORE_STORE_FEED"; + private readonly ILogger _logger; + private string _storeDir; + private string _tempDir; + + public Store(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public string CreateStore(bool createInDefaultLocation) + { + var storeParentDir = GetStoreParentDirectory(createInDefaultLocation); + + InstallStore(storeParentDir); + + _storeDir = Path.Combine(storeParentDir, "store"); + + return _storeDir; + } + + public void Dispose() + { + if (string.IsNullOrEmpty(_storeDir)) + { + return; + } + + if (Helpers.PreservePublishedApplicationForDebugging) + { + _logger.LogInformation("Skipping deleting the store as it has been disabled"); + } + else + { + _logger.LogInformation("Deleting the store..."); + + //RetryHelper.RetryOperation( + // () => Directory.Delete(_storeDir, recursive: true), + // e => _logger.LogError($"Failed to delete directory : {e.Message}"), + // retryCount: 3, + // retryDelayMilliseconds: 100); + + RetryHelper.RetryOperation( + () => Directory.Delete(_tempDir, recursive: true), + e => _logger.LogError($"Failed to delete directory : {e.Message}"), + retryCount: 3, + retryDelayMilliseconds: 100); + } + } + + public static bool IsEnabled() + { + var storeFeed = Environment.GetEnvironmentVariable(MusicStoreAspNetCoreStoreFeed); + return !string.IsNullOrEmpty(storeFeed); + } + + private void InstallStore(string storeParentDir) + { + var packageId = "Build.RS"; + var storeFeed = Environment.GetEnvironmentVariable(MusicStoreAspNetCoreStoreFeed); + if (string.IsNullOrEmpty(storeFeed)) + { + _logger.LogError("The feed for the store package was not provided." + + $"Set the environment variable '{MusicStoreAspNetCoreStoreFeed}' and try again."); + + throw new InvalidOperationException( + $"The environment variable '{MusicStoreAspNetCoreStoreFeed}' is not defined or is empty."); + } + else + { + _logger.LogInformation($"Using the feed {storeFeed} for the store package"); + } + + // Get the version information from the attribute which is typically the same as the nuget package version + // Example: + // [assembly: AssemblyInformationalVersion("2.0.0-preview1-24847")] + var aspnetCoreHttpAssembly = typeof(Microsoft.AspNetCore.Http.FormCollection).Assembly; + var obj = aspnetCoreHttpAssembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), inherit: false).FirstOrDefault(); + var assemblyInformationVersionAttribute = obj as AssemblyInformationalVersionAttribute; + if (assemblyInformationVersionAttribute == null) + { + throw new InvalidOperationException($"Could not find {nameof(assemblyInformationVersionAttribute)} from the assembly {aspnetCoreHttpAssembly.FullName}"); + } + + _logger.LogInformation($"Downloading package with id {packageId} and version {assemblyInformationVersionAttribute.InformationalVersion} from feed {storeFeed}"); + + _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var sourceRepository = Repository.Factory.GetCoreV2(new PackageSource(storeFeed)); + var downloadResource = sourceRepository.GetResource(); + var result = downloadResource.GetDownloadResourceResultAsync( + new PackageIdentity(packageId, NuGetVersion.Parse(assemblyInformationVersionAttribute.InformationalVersion)), + new PackageDownloadContext( + new SourceCacheContext() { NoCache = true, DirectDownload = true }, + _tempDir, + directDownload: true), + null, + NuGet.Common.NullLogger.Instance, + CancellationToken.None) + .Result; + + if (result.Status != DownloadResourceResultStatus.Available) + { + _logger.LogError($"Failed to download the package. Status: {result.Status}"); + throw new InvalidOperationException("Unable to download the store package"); + } + + var zipFile = Path.Combine(_tempDir, "Build.RS.zip"); + using (var targetStream = File.Create(zipFile)) + { + using (result.PackageStream) + { + result.PackageStream.CopyTo(targetStream); + } + } + + _logger.LogInformation($"Package downloaded and saved as zip file at {zipFile}"); + + var zipFileExtracted = Path.Combine(_tempDir, "extracted"); + ZipFile.ExtractToDirectory(zipFile, zipFileExtracted); + + _logger.LogInformation($"Package extracted at {zipFileExtracted}"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + string fileNameWithExtension = null; + foreach (var file in new DirectoryInfo(zipFileExtracted).GetFiles()) + { + if (file.Name.StartsWith($"{packageId}.winx64")) + { + using (var zipArchive = ZipFile.Open(file.FullName, ZipArchiveMode.Read)) + { + var mvcCoreDllEntry = zipArchive.Entries + .Where(entry => string.Equals(entry.Name, "Microsoft.AspNetCore.Mvc.Core.dll", StringComparison.OrdinalIgnoreCase)) + .FirstOrDefault(); + if (mvcCoreDllEntry != null && mvcCoreDllEntry.FullName.Contains(assemblyInformationVersionAttribute.InformationalVersion)) + { + fileNameWithExtension = file.Name; + break; + } + } + } + } + + if (string.IsNullOrEmpty(fileNameWithExtension)) + { + throw new InvalidOperationException( + $"Could not find a store zip file with version {assemblyInformationVersionAttribute.InformationalVersion}"); + } + + var storeZipFile = Path.Combine(zipFileExtracted, fileNameWithExtension); + ZipFile.ExtractToDirectory(storeZipFile, storeParentDir); + _logger.LogInformation($"Extracted the store zip file '{storeZipFile}' to '{storeParentDir}'"); + } + else + { + string packageIdPrefix; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + packageIdPrefix = $"{packageId}.linux"; + } + else + { + packageIdPrefix = $"{packageId}.osx"; + } + + string fileNameWithExtension = null; + foreach (var file in new DirectoryInfo(zipFileExtracted).GetFiles()) + { + if (file.Name.StartsWith(packageIdPrefix) + && !string.Equals($"{packageIdPrefix}.tar.gz", file.Name, StringComparison.OrdinalIgnoreCase)) + { + fileNameWithExtension = file.FullName; + break; + } + } + + if (string.IsNullOrEmpty(fileNameWithExtension)) + { + throw new InvalidOperationException( + $"Could not find a store zip file with version {assemblyInformationVersionAttribute.InformationalVersion}"); + } + + Directory.CreateDirectory(storeParentDir); + + var startInfo = new ProcessStartInfo() + { + FileName = "tar", + Arguments = $"xvzf {fileNameWithExtension}", + WorkingDirectory = storeParentDir, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + RedirectStandardInput = true + }; + var tarProcess = new Process() { StartInfo = startInfo }; + tarProcess.EnableRaisingEvents = true; + tarProcess.StartAndCaptureOutAndErrToLogger("tar", _logger); + + if (tarProcess.HasExited && tarProcess.ExitCode != 0) + { + var message = $"Error occurred while extracting the file '{fileNameWithExtension}' in working directory '{storeParentDir}'"; + _logger.LogError(message); + throw new InvalidOperationException(message); + } + } + } + + private string GetStoreParentDirectory(bool createInDefaultLocation) + { + string storeParentDir; + if (createInDefaultLocation) + { + // On Windows: ..\.dotnet\x64\dotnet.exe + // On Linux : ../.dotnet/dotnet + var dotnetDir = new FileInfo(DotNetMuxer.MuxerPath).Directory.FullName; + storeParentDir = dotnetDir; + } + else + { + storeParentDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + } + return storeParentDir; + } + } +}