diff --git a/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs b/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs index fe1d604dd9..0fdac242d9 100644 --- a/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs +++ b/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs @@ -24,6 +24,7 @@ public class TestsSetup public void TearDown() { LocalDbTestDatabase.KillLocalDb(); + SqlDeveloperTestDatabase.Instance?.Finish(); Console.WriteLine("TOTAL TESTS DURATION: {0}", _stopwatch.Elapsed); } } diff --git a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs index bb83914b63..0a081f48e9 100644 --- a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs @@ -150,7 +150,7 @@ namespace Umbraco.Tests.Integration.Testing } - private static void ResetLocalDb(IDbCommand cmd) + internal static void ResetLocalDb(IDbCommand cmd) { // https://stackoverflow.com/questions/536350 @@ -211,7 +211,7 @@ namespace Umbraco.Tests.Integration.Testing } } - private static void Retry(int maxIterations, Action action) + internal static void Retry(int maxIterations, Action action) { for (var i = 0; i < maxIterations; i++) { diff --git a/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs index f5ae1661b8..560bcbe00a 100644 --- a/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using Microsoft.Extensions.Logging; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -18,83 +21,90 @@ namespace Umbraco.Tests.Integration.Testing /// public class SqlDeveloperTestDatabase : ITestDatabase { + + // This is gross but it's how the other one works and I don't want to refactor everything. + public string ConnectionString { get; private set; } + private readonly string _masterConnectionString; - private readonly string _databaseName; private readonly ILoggerFactory _loggerFactory; private readonly ILogger _log; private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly IDictionary _testDatabases; private UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands; + + private BlockingCollection _prepareQueue; + private BlockingCollection _readySchemaQueue; + private BlockingCollection _readyEmptyQueue; + + private const string _databasePrefix = "UmbracoTest"; + private const int _threadCount = 2; + + public static SqlDeveloperTestDatabase Instance; public SqlDeveloperTestDatabase(ILoggerFactory loggerFactory, IUmbracoDatabaseFactory databaseFactory, string masterConnectionString) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _databaseFactory = databaseFactory ?? throw new ArgumentNullException(nameof(databaseFactory)); _masterConnectionString = masterConnectionString; - _databaseName = $"Umbraco_Integration_{Guid.NewGuid()}".Replace("-", string.Empty); _log = loggerFactory.CreateLogger(); - } - public string ConnectionString { get; private set; } + _testDatabases = new[] + { + new TestDbMeta(1, false, masterConnectionString), + new TestDbMeta(2, false, masterConnectionString), + + new TestDbMeta(3, true, masterConnectionString), + new TestDbMeta(4, true, masterConnectionString), + }.ToDictionary(x => x.Id); + + Instance = this; // For GlobalSetupTeardown.cs + } public int AttachEmpty() { - CreateDatabase(); - return -1; + if (_prepareQueue == null) + { + Initialize(); + } + + var meta = _readyEmptyQueue.Take(); + + ConnectionString = meta.ConnectionString; + + return meta.Id; } public int AttachSchema() { - CreateDatabase(); - - _log.LogInformation($"Attaching schema {_databaseName}"); - - using (var connection = new SqlConnection(ConnectionString)) + if (_prepareQueue == null) { - connection.Open(); - using (var command = connection.CreateCommand()) - { - RebuildSchema(command); - } + Initialize(); } - return -1; + var meta = _readySchemaQueue.Take(); + + ConnectionString = meta.ConnectionString; + + return meta.Id; } public void Detach(int id) { - _log.LogInformation($"Dropping database {_databaseName}"); - using (var connection = new SqlConnection(_masterConnectionString)) - { - connection.Open(); - using (var command = connection.CreateCommand()) - { - SetCommand(command, $@" - ALTER DATABASE{LocalDb.QuotedName(_databaseName)} - SET SINGLE_USER - WITH ROLLBACK IMMEDIATE - "); - command.ExecuteNonQuery(); - - SetCommand(command, $@"DROP DATABASE {LocalDb.QuotedName(_databaseName)}"); - command.ExecuteNonQuery(); - } - } + _prepareQueue.TryAdd(_testDatabases[id]); } - private void CreateDatabase() + private void CreateDatabase(TestDbMeta meta) { - _log.LogInformation($"Creating database {_databaseName}"); + _log.LogInformation($"Creating database {meta.Name}"); using (var connection = new SqlConnection(_masterConnectionString)) { connection.Open(); using (var command = connection.CreateCommand()) { - SetCommand(command, $@"CREATE DATABASE {LocalDb.QuotedName(_databaseName)}"); - var unused = command.ExecuteNonQuery(); + SetCommand(command, $@"CREATE DATABASE {LocalDb.QuotedName(meta.Name)}"); + command.ExecuteNonQuery(); } } - - ConnectionString = ConstructConnectionString(_masterConnectionString, _databaseName); } private static string ConstructConnectionString(string masterConnectionString, string databaseName) @@ -116,7 +126,7 @@ namespace Umbraco.Tests.Integration.Testing } } - private void RebuildSchema(IDbCommand command) + private void RebuildSchema(IDbCommand command, TestDbMeta meta) { if (_cachedDatabaseInitCommands != null) { @@ -141,7 +151,7 @@ namespace Umbraco.Tests.Integration.Testing } else { - _databaseFactory.Configure(ConnectionString, Constants.DatabaseProviders.SqlServer); + _databaseFactory.Configure(meta.ConnectionString, Constants.DatabaseProviders.SqlServer); using (var database = (UmbracoDatabase)_databaseFactory.CreateDatabase()) { @@ -159,5 +169,120 @@ namespace Umbraco.Tests.Integration.Testing } } } + + private void Initialize() + { + _prepareQueue = new BlockingCollection(); + _readySchemaQueue = new BlockingCollection(); + _readyEmptyQueue = new BlockingCollection(); + + foreach (var meta in _testDatabases.Values) + { + CreateDatabase(meta); + _prepareQueue.Add(meta); + } + + for (var i = 0; i < _threadCount; i++) + { + var thread = new Thread(PrepareThread); + thread.Start(); + } + } + + private void Drop(TestDbMeta meta) + { + _log.LogInformation($"Dropping database {meta.Name}"); + using (var connection = new SqlConnection(_masterConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + SetCommand(command, $@" + ALTER DATABASE{LocalDb.QuotedName(meta.Name)} + SET SINGLE_USER + WITH ROLLBACK IMMEDIATE + "); + command.ExecuteNonQuery(); + + SetCommand(command, $@"DROP DATABASE {LocalDb.QuotedName(meta.Name)}"); + command.ExecuteNonQuery(); + } + } + } + + private void PrepareThread() + { + LocalDbTestDatabase.Retry(10, () => + { + while (_prepareQueue.IsCompleted == false) + { + TestDbMeta meta; + try + { + meta = _prepareQueue.Take(); + } + catch (InvalidOperationException) + { + continue; + } + + using (var conn = new SqlConnection(meta.ConnectionString)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + LocalDbTestDatabase.ResetLocalDb(cmd); + + if (!meta.IsEmpty) + { + RebuildSchema(cmd, meta); + } + } + + if (!meta.IsEmpty) + { + _readySchemaQueue.TryAdd(meta); + } + else + { + _readyEmptyQueue.TryAdd(meta); + } + } + }); + } + + public void Finish() + { + if (_prepareQueue == null) + return; + + _prepareQueue.CompleteAdding(); + while (_prepareQueue.TryTake(out _)) { } + + _readyEmptyQueue.CompleteAdding(); + while (_readyEmptyQueue.TryTake(out _)) { } + + _readySchemaQueue.CompleteAdding(); + while (_readySchemaQueue.TryTake(out _)) { } + + foreach (var testDatabase in _testDatabases.Values) + { + Drop(testDatabase); + } + } + + private class TestDbMeta + { + public int Id { get; } + public string Name => $"{_databasePrefix}-{Id}"; + public bool IsEmpty { get; } + public string ConnectionString { get; } + + public TestDbMeta(int id, bool isEmpty, string masterConnectionString) + { + Id = id; + IsEmpty = isEmpty; + ConnectionString = ConstructConnectionString(masterConnectionString, Name); + } + } } } diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 9627fdf42a..a376aff38d 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -250,7 +250,7 @@ namespace Umbraco.Tests.Integration.Testing var databaseFactory = serviceProvider.GetRequiredService(); // This will create a db, install the schema and ensure the app is configured to run - InstallTestLocalDb(databaseFactory, TestHelper.ConsoleLoggerFactory, state, TestHelper.WorkingDirectory); + InstallTestLocalDb(databaseFactory, serviceProvider.GetRequiredService(), state, TestHelper.WorkingDirectory); TestDBConnectionString = databaseFactory.ConnectionString; InMemoryConfiguration["ConnectionStrings:" + Constants.System.UmbracoConnectionName] = TestDBConnectionString; }