Gets the DB installing in integration tests, changes integratino tests to use Generic Host builder

This commit is contained in:
Shannon
2020-03-24 11:53:56 +11:00
parent 679b88a2fb
commit 919d418920
14 changed files with 319 additions and 91 deletions

View File

@@ -33,13 +33,13 @@ namespace Umbraco.Core.Composing
/// <param name="ioHelper">An IOHelper</param>
public Composition(IRegister register, TypeLoader typeLoader, IProfilingLogger logger, IRuntimeState runtimeState, Configs configs, IIOHelper ioHelper, AppCaches appCaches)
{
_register = register;
TypeLoader = typeLoader;
Logger = logger;
RuntimeState = runtimeState;
Configs = configs;
IOHelper = ioHelper;
AppCaches = appCaches;
_register = register ?? throw new ArgumentNullException(nameof(register));
TypeLoader = typeLoader ?? throw new ArgumentNullException(nameof(typeLoader));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
RuntimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState));
Configs = configs ?? throw new ArgumentNullException(nameof(configs));
IOHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
AppCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches));
}
#region Services

View File

@@ -13,8 +13,9 @@ namespace Umbraco.Core.Composing
/// <param name="builder"></param>
/// <returns></returns>
public static IHostBuilder UseUmbraco(this IHostBuilder builder)
{
return builder.UseServiceProviderFactory(new UmbracoServiceProviderFactory());
}
=> builder.UseUmbraco(new UmbracoServiceProviderFactory());
public static IHostBuilder UseUmbraco(this IHostBuilder builder, UmbracoServiceProviderFactory umbracoServiceProviderFactory)
=> builder.UseServiceProviderFactory(umbracoServiceProviderFactory);
}
}

View File

@@ -46,7 +46,7 @@ namespace Umbraco.Core.Migrations.Install
IIOHelper ioHelper,
IUmbracoVersion umbracoVersion,
IDbProviderFactoryCreator dbProviderFactoryCreator,
IConfigManipulator configManipulator)
IConfigManipulator configManipulator)
{
_scopeProvider = scopeProvider;
_globalSettings = globalSettings;

View File

@@ -176,6 +176,8 @@ namespace Umbraco.Core.Runtime
// Grid config is not a real config file as we know them
composition.RegisterUnique<IGridConfig, GridConfig>();
// Config manipulator
composition.RegisterUnique<IConfigManipulator, JsonConfigManipulator>();
}
}
}

View File

@@ -115,6 +115,8 @@ namespace Umbraco.Core.Runtime
/// <inheritdoc/>
public virtual IFactory Boot(IRegister register)
{
if (register is null) throw new ArgumentNullException(nameof(register));
// create and register the essential services
// ie the bare minimum required to boot
@@ -152,6 +154,9 @@ namespace Umbraco.Core.Runtime
/// </summary>
protected virtual IFactory Boot(IRegister register, DisposableTimer timer)
{
if (register is null) throw new ArgumentNullException(nameof(register));
if (timer is null) throw new ArgumentNullException(nameof(timer));
Composition composition = null;
try

View File

@@ -0,0 +1,38 @@
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Umbraco.Tests.Integration
{
/// <summary>
/// Executes arbitrary code on start/end
/// </summary>
public class DelegateHostedService : IHostedService
{
private readonly Action _start;
private readonly Action _end;
public static DelegateHostedService Create(Action start, Action end) => new DelegateHostedService(start, end);
private DelegateHostedService(Action start, Action end)
{
_start = start;
_end = end;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_start();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_end?.Invoke();
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,55 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.SqlClient;
using System.IO;
using Umbraco.Core.Persistence;
using Umbraco.Tests.Integration.Testing;
namespace Umbraco.Tests.Integration
{
public static class HostBuilderExtensions
{
/// <summary>
/// Ensures the lifetime of the host ends as soon as code executes
/// </summary>
/// <param name="hostBuilder"></param>
/// <returns></returns>
public static IHostBuilder UseTestLifetime(this IHostBuilder hostBuilder)
{
hostBuilder.ConfigureServices((context, collection) => collection.AddSingleton<IHostLifetime, TestLifetime>());
return hostBuilder;
}
public static IHostBuilder UseLocalDb(this IHostBuilder hostBuilder, string dbFilePath)
{
// Need to register SqlClient manually
// TODO: Move this to someplace central
DbProviderFactories.RegisterFactory("System.Data.SqlClient", SqlClientFactory.Instance);
hostBuilder.ConfigureAppConfiguration(x =>
{
if (!Directory.Exists(dbFilePath))
Directory.CreateDirectory(dbFilePath);
var dbName = Guid.NewGuid().ToString("N");
var instance = TestLocalDb.EnsureLocalDbInstanceAndDatabase(dbName, dbFilePath);
x.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("ConnectionStrings:umbracoDbDSN", instance.GetConnectionString(dbName))
});
});
return hostBuilder;
}
}
}

View File

@@ -1,34 +0,0 @@
using System.Data.Common;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.SqlSyntax;
namespace Umbraco.Tests.Integration.Implementations
{
public class TestDbProviderFactoryCreator : IDbProviderFactoryCreator
{
public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName)
{
throw new System.NotImplementedException();
}
public void CreateDatabase()
{
throw new System.NotImplementedException();
}
public DbProviderFactory CreateFactory()
{
throw new System.NotImplementedException();
}
public DbProviderFactory CreateFactory(string providerName)
{
throw new System.NotImplementedException();
}
public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName)
{
throw new System.NotImplementedException();
}
}
}

View File

@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
using Moq;
using System.Data.Common;
using System.Net;
using Umbraco.Core;
using Umbraco.Core.Cache;
@@ -60,7 +61,7 @@ namespace Umbraco.Tests.Integration.Implementations
public IWebHostEnvironment GetWebHostEnvironment() => _hostEnvironment;
public override IDbProviderFactoryCreator DbProviderFactoryCreator => new TestDbProviderFactoryCreator();
public override IDbProviderFactoryCreator DbProviderFactoryCreator => new SqlServerDbProviderFactoryCreator("System.Data.SqlClient", DbProviderFactories.GetFactory);
public override IBulkSqlInsertProvider BulkSqlInsertProvider => new SqlServerBulkSqlInsertProvider();

View File

@@ -0,0 +1,18 @@
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;
namespace Umbraco.Tests.Integration
{
/// <summary>
/// Ensures the host lifetime ends as soon as code execution is done
/// </summary>
public class TestLifetime : IHostLifetime
{
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task WaitForStartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -7,19 +7,36 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Moq;
using NUnit.Framework;
using System.Collections.Generic;
using System.IO;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Composing.LightInject;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations.Install;
using Umbraco.Core.Persistence;
using Umbraco.Core.Runtime;
using Umbraco.Tests.Common;
using Umbraco.Tests.Integration.Implementations;
using Umbraco.Tests.Integration.Testing;
using Umbraco.Web.BackOffice.AspNetCore;
using static Umbraco.Core.Migrations.Install.DatabaseBuilder;
namespace Umbraco.Tests.Integration
{
[TestFixture]
public class RuntimeTests
{
[OneTimeTearDown]
public void FixtureTearDown()
{
TestLocalDb.Cleanup();
}
/// <summary>
/// Manually configure the containers/dependencies and call Boot on Core runtime
/// </summary>
[Test]
public void BootCoreRuntime()
{
@@ -52,40 +69,107 @@ namespace Umbraco.Tests.Integration
Assert.IsTrue(MyComponent.IsTerminated);
}
/// <summary>
/// Calling AddUmbracoCore to configure the container and boot the core runtime within a generic host
/// </summary>
[Test]
public void AddUmbracoCore()
{
var umbracoContainer = GetUmbracoContainer(out var serviceProviderFactory);
var host = Host.CreateDefaultBuilder()
.UseTestLifetime()
.UseUmbraco(serviceProviderFactory)
.ConfigureServices((hostContext, services) =>
{
var testHelper = new TestHelper();
AddRequiredNetCoreServices(services, testHelper);
// Add it!
services.AddUmbracoConfiguration();
services.AddUmbracoCore(umbracoContainer, GetType().Assembly);
// Run tests
services.AddHostedService(x => DelegateHostedService.Create(() =>
{
// assert results
var runtimeState = umbracoContainer.GetInstance<IRuntimeState>();
var mainDom = umbracoContainer.GetInstance<IMainDom>();
Assert.IsTrue(mainDom.IsMainDom);
Assert.IsNull(runtimeState.BootFailedException);
Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level);
Assert.IsTrue(MyComposer.IsComposed);
}, null));
})
.Build();
// NOTE: host.Run() could be used but that requires more work by manually calling IHostApplicationLifetime.StopApplication(). In these cases we don't need to wind down.
host.Start();
}
[Test]
public void UseUmbracoCore()
{
var umbracoContainer = GetUmbracoContainer(out var serviceProviderFactory);
var testHelper = new TestHelper();
// MSDI
var services = new ServiceCollection();
// These services are required
var host = Host.CreateDefaultBuilder()
//TODO: Need to have a configured umb version for the runtime state
.UseLocalDb(Path.Combine(testHelper.CurrentAssemblyDirectory, "LocalDb"))
.UseTestLifetime()
.UseUmbraco(serviceProviderFactory)
.ConfigureServices((hostContext, services) =>
{
AddRequiredNetCoreServices(services, testHelper);
// Add it!
services.AddUmbracoConfiguration();
services.AddUmbracoCore(umbracoContainer, GetType().Assembly);
// Run tests
services.AddHostedService(x => DelegateHostedService.Create(() =>
{
var runtimeState = (RuntimeState)umbracoContainer.GetInstance<IRuntimeState>();
Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level);
var dbBuilder = umbracoContainer.GetInstance<DatabaseBuilder>();
Assert.IsNotNull(dbBuilder);
var canConnect = dbBuilder.CanConnectToDatabase;
Assert.IsTrue(canConnect);
var dbResult = dbBuilder.CreateSchemaAndData();
Assert.IsTrue(dbResult.Success);
var dbFactory = umbracoContainer.GetInstance<IUmbracoDatabaseFactory>();
var profilingLogger = umbracoContainer.GetInstance<IProfilingLogger>();
runtimeState.DetermineRuntimeLevel(dbFactory, profilingLogger);
Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level);
}, null));
})
.Build();
// NOTE: host.Run() could be used but that requires more work by manually calling IHostApplicationLifetime.StopApplication(). In these cases we don't need to wind down.
host.Start();
}
private LightInjectContainer GetUmbracoContainer(out UmbracoServiceProviderFactory serviceProviderFactory)
{
var container = new ServiceContainer(ContainerOptions.Default.Clone().WithMicrosoftSettings().WithAspNetCoreSettings());
serviceProviderFactory = new UmbracoServiceProviderFactory(container);
var umbracoContainer = serviceProviderFactory.GetContainer();
return umbracoContainer;
}
private void AddRequiredNetCoreServices(IServiceCollection services, TestHelper testHelper)
{
services.AddSingleton<IHttpContextAccessor>(x => testHelper.GetHttpContextAccessor());
services.AddSingleton<IWebHostEnvironment>(x => testHelper.GetWebHostEnvironment());
services.AddSingleton<IHostApplicationLifetime>(x => Mock.Of<IHostApplicationLifetime>());
// LightInject / Umbraco
var container = new ServiceContainer(ContainerOptions.Default.Clone().WithMicrosoftSettings().WithAspNetCoreSettings());
var serviceProviderFactory = new UmbracoServiceProviderFactory(container);
var umbracoContainer = serviceProviderFactory.GetContainer();
// Some IConfiguration must exist in the container first
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddEnvironmentVariables();
services.AddSingleton<IConfiguration>(x => configurationBuilder.Build());
// Add it!
services.AddUmbracoConfiguration();
services.AddUmbracoCore(umbracoContainer, GetType().Assembly);
// assert results
var runtimeState = umbracoContainer.GetInstance<IRuntimeState>();
var mainDom = umbracoContainer.GetInstance<IMainDom>();
Assert.IsTrue(mainDom.IsMainDom);
Assert.IsNull(runtimeState.BootFailedException);
Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level);
Assert.IsTrue(MyComposer.IsComposed);
}
[RuntimeLevel(MinLevel = RuntimeLevel.Install)]

View File

@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Text;
using Umbraco.Core.Persistence;
namespace Umbraco.Tests.Integration.Testing
{
public static class TestLocalDb
{
private const string LocalDbInstanceName = "UmbTests";
private static LocalDb LocalDb { get; } = new LocalDb();
// TODO: We need to borrow logic from this old branch, this is the latest commit at the old branch where we had LocalDb
// working for tests. There's a lot of hoops to jump through to make it work 'fast'. Turns out it didn't actually run as
// fast as SqlCe due to the dropping/creating of DB instances since that is faster in SqlCe but this code was all heavily
// optimized to go as fast as possible.
// see https://github.com/umbraco/Umbraco-CMS/blob/3a8716ac7b1c48b51258724337086cd0712625a1/src/Umbraco.Tests/TestHelpers/LocalDbTestDatabase.cs
internal static LocalDb.Instance EnsureLocalDbInstanceAndDatabase(string dbName, string dbFilePath)
{
if (!LocalDb.InstanceExists(LocalDbInstanceName) && !LocalDb.CreateInstance(LocalDbInstanceName))
{
throw new InvalidOperationException(
$"Failed to create LocalDb instance {LocalDbInstanceName}, assuming LocalDb is not really available.");
}
var instance = LocalDb.GetInstance(LocalDbInstanceName);
if (instance == null)
{
throw new InvalidOperationException(
$"Failed to get LocalDb instance {LocalDbInstanceName}, assuming LocalDb is not really available.");
}
instance.CreateDatabase(dbName, dbFilePath);
return instance;
}
public static void Cleanup()
{
var instance = LocalDb.GetInstance(LocalDbInstanceName);
if (instance != null)
{
instance.DropDatabases();
}
}
}
}

View File

@@ -15,6 +15,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -13,6 +13,7 @@ using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Logging.Serilog;
@@ -63,26 +64,30 @@ namespace Umbraco.Web.BackOffice.AspNetCore
public static IServiceCollection AddUmbracoCore(this IServiceCollection services, IRegister umbContainer, Assembly entryAssembly)
{
if (services is null) throw new ArgumentNullException(nameof(services));
if (umbContainer is null) throw new ArgumentNullException(nameof(umbContainer));
if (entryAssembly is null) throw new ArgumentNullException(nameof(entryAssembly));
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
CreateCompositionRoot(services);
CreateCompositionRoot(services, out var logger, out var configs, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler);
// TODO: Get rid of this 'Current' requirement
var globalSettings = Current.Configs.Global();
var globalSettings = configs.Global();
var umbracoVersion = new UmbracoVersion(globalSettings);
// TODO: Currently we are not passing in any TypeFinderConfig (with ITypeFinderSettings) which we should do, however
// this is not critical right now and would require loading in some config before boot time so just leaving this as-is for now.
var typeFinder = new TypeFinder(Current.Logger, new DefaultUmbracoAssemblyProvider(entryAssembly));
var typeFinder = new TypeFinder(logger, new DefaultUmbracoAssemblyProvider(entryAssembly));
var coreRuntime = GetCoreRuntime(
Current.Configs,
configs,
umbracoVersion,
Current.IOHelper,
Current.Logger,
Current.Profiler,
Current.HostingEnvironment,
Current.BackOfficeInfo,
ioHelper,
logger,
profiler,
hostingEnvironment,
backOfficeInfo,
typeFinder);
var factory = coreRuntime.Boot(umbContainer);
@@ -115,7 +120,9 @@ namespace Umbraco.Web.BackOffice.AspNetCore
return coreRuntime;
}
private static void CreateCompositionRoot(IServiceCollection services)
private static void CreateCompositionRoot(IServiceCollection services,
out ILogger logger, out Configs configs, out IIOHelper ioHelper, out Core.Hosting.IHostingEnvironment hostingEnvironment,
out IBackOfficeInfo backOfficeInfo, out IProfiler profiler)
{
// TODO: This isn't the best to have to resolve the services now but to avoid this will
// require quite a lot of re-work.
@@ -123,9 +130,12 @@ namespace Umbraco.Web.BackOffice.AspNetCore
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
var webHostEnvironment = serviceProvider.GetRequiredService<IWebHostEnvironment>();
// TODO: I'm unsure about this, by doing this it means we are resolving a "Different" instance to the one
// that controls the whole app because the instances comes from a different service provider. This
// could cause some issues with shutdowns, etc... we need to investigate.
var hostApplicationLifetime = serviceProvider.GetRequiredService<IHostApplicationLifetime>();
var configs = serviceProvider.GetService<Configs>();
configs = serviceProvider.GetService<Configs>();
if (configs == null)
throw new InvalidOperationException($"Could not resolve type {typeof(Configs)} from the container, ensure {nameof(AddUmbracoConfiguration)} is called before calling {nameof(AddUmbracoCore)}");
@@ -133,17 +143,15 @@ namespace Umbraco.Web.BackOffice.AspNetCore
var coreDebug = configs.CoreDebug();
var globalSettings = configs.Global();
var hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment, httpContextAccessor, hostApplicationLifetime);
var ioHelper = new IOHelper(hostingEnvironment, globalSettings);
var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment,
hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment, httpContextAccessor, hostApplicationLifetime);
ioHelper = new IOHelper(hostingEnvironment, globalSettings);
logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment,
new AspNetCoreSessionIdResolver(httpContextAccessor),
() => serviceProvider.GetService<IRequestCache>(), coreDebug, ioHelper,
new AspNetCoreMarchal());
var backOfficeInfo = new AspNetCoreBackOfficeInfo(configs.Global());
var profiler = new LogProfiler(logger);
Current.Initialize(logger, configs, ioHelper, hostingEnvironment, backOfficeInfo, profiler);
backOfficeInfo = new AspNetCoreBackOfficeInfo(configs.Global());
profiler = new LogProfiler(logger);
}
private class AspNetCoreBootPermissionsChecker : IUmbracoBootPermissionChecker