Multiple test databases, similar setup to LocalDbTestDatabase.DatabasePool

This commit is contained in:
Paul Johnson
2020-11-30 12:14:53 +00:00
parent 51f20119a2
commit 897fe804b0
4 changed files with 171 additions and 45 deletions

View File

@@ -24,6 +24,7 @@ public class TestsSetup
public void TearDown()
{
LocalDbTestDatabase.KillLocalDb();
SqlDeveloperTestDatabase.Instance?.Finish();
Console.WriteLine("TOTAL TESTS DURATION: {0}", _stopwatch.Elapsed);
}
}

View File

@@ -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++)
{

View File

@@ -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
/// </remarks>
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<int, TestDbMeta> _testDatabases;
private UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands;
private BlockingCollection<TestDbMeta> _prepareQueue;
private BlockingCollection<TestDbMeta> _readySchemaQueue;
private BlockingCollection<TestDbMeta> _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<SqlDeveloperTestDatabase>();
}
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<TestDbMeta>();
_readySchemaQueue = new BlockingCollection<TestDbMeta>();
_readyEmptyQueue = new BlockingCollection<TestDbMeta>();
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);
}
}
}
}

View File

@@ -250,7 +250,7 @@ namespace Umbraco.Tests.Integration.Testing
var databaseFactory = serviceProvider.GetRequiredService<IUmbracoDatabaseFactory>();
// 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<ILoggerFactory>(), state, TestHelper.WorkingDirectory);
TestDBConnectionString = databaseFactory.ConnectionString;
InMemoryConfiguration["ConnectionStrings:" + Constants.System.UmbracoConnectionName] = TestDBConnectionString;
}