2020-11-27 19:15:49 +00:00
|
|
|
|
using System;
|
2020-11-30 12:14:53 +00:00
|
|
|
|
using System.Collections.Concurrent;
|
|
|
|
|
|
using System.Collections.Generic;
|
2020-11-27 19:15:49 +00:00
|
|
|
|
using System.Data;
|
|
|
|
|
|
using System.Data.SqlClient;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Text.RegularExpressions;
|
2020-11-30 12:14:53 +00:00
|
|
|
|
using System.Threading;
|
2020-11-27 19:15:49 +00:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
using Umbraco.Core;
|
|
|
|
|
|
using Umbraco.Core.Configuration;
|
|
|
|
|
|
using Umbraco.Core.Migrations.Install;
|
|
|
|
|
|
using Umbraco.Core.Persistence;
|
|
|
|
|
|
|
|
|
|
|
|
// ReSharper disable ConvertToUsingDeclaration
|
|
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Tests.Integration.Testing
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// It's not meant to be pretty, rushed port of LocalDb.cs + LocalDbTestDatabase.cs
|
|
|
|
|
|
/// </remarks>
|
|
|
|
|
|
public class SqlDeveloperTestDatabase : ITestDatabase
|
|
|
|
|
|
{
|
2020-11-30 12:14:53 +00:00
|
|
|
|
|
|
|
|
|
|
// 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; }
|
|
|
|
|
|
|
2020-11-27 19:15:49 +00:00
|
|
|
|
private readonly string _masterConnectionString;
|
|
|
|
|
|
private readonly ILoggerFactory _loggerFactory;
|
|
|
|
|
|
private readonly ILogger _log;
|
|
|
|
|
|
private readonly IUmbracoDatabaseFactory _databaseFactory;
|
2020-11-30 12:14:53 +00:00
|
|
|
|
private readonly IDictionary<int, TestDbMeta> _testDatabases;
|
2020-11-27 19:15:49 +00:00
|
|
|
|
private UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands;
|
2020-12-11 18:20:07 +00:00
|
|
|
|
|
2020-11-30 12:14:53 +00:00
|
|
|
|
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;
|
2020-11-27 19:15:49 +00:00
|
|
|
|
|
|
|
|
|
|
public SqlDeveloperTestDatabase(ILoggerFactory loggerFactory, IUmbracoDatabaseFactory databaseFactory, string masterConnectionString)
|
|
|
|
|
|
{
|
|
|
|
|
|
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
|
|
|
|
|
_databaseFactory = databaseFactory ?? throw new ArgumentNullException(nameof(databaseFactory));
|
|
|
|
|
|
_masterConnectionString = masterConnectionString;
|
|
|
|
|
|
_log = loggerFactory.CreateLogger<SqlDeveloperTestDatabase>();
|
|
|
|
|
|
|
2020-11-30 12:14:53 +00:00
|
|
|
|
_testDatabases = new[]
|
|
|
|
|
|
{
|
|
|
|
|
|
new TestDbMeta(1, false, masterConnectionString),
|
|
|
|
|
|
new TestDbMeta(2, false, masterConnectionString),
|
|
|
|
|
|
|
|
|
|
|
|
new TestDbMeta(3, true, masterConnectionString),
|
|
|
|
|
|
}.ToDictionary(x => x.Id);
|
|
|
|
|
|
|
|
|
|
|
|
Instance = this; // For GlobalSetupTeardown.cs
|
|
|
|
|
|
}
|
2020-11-27 19:15:49 +00:00
|
|
|
|
|
|
|
|
|
|
public int AttachEmpty()
|
|
|
|
|
|
{
|
2020-11-30 12:14:53 +00:00
|
|
|
|
if (_prepareQueue == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Initialize();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var meta = _readyEmptyQueue.Take();
|
|
|
|
|
|
|
|
|
|
|
|
ConnectionString = meta.ConnectionString;
|
|
|
|
|
|
|
|
|
|
|
|
return meta.Id;
|
2020-11-27 19:15:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int AttachSchema()
|
|
|
|
|
|
{
|
2020-11-30 12:14:53 +00:00
|
|
|
|
if (_prepareQueue == null)
|
2020-11-27 19:15:49 +00:00
|
|
|
|
{
|
2020-11-30 12:14:53 +00:00
|
|
|
|
Initialize();
|
2020-11-27 19:15:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-11-30 12:14:53 +00:00
|
|
|
|
var meta = _readySchemaQueue.Take();
|
|
|
|
|
|
|
|
|
|
|
|
ConnectionString = meta.ConnectionString;
|
|
|
|
|
|
|
|
|
|
|
|
return meta.Id;
|
2020-11-27 19:15:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Detach(int id)
|
|
|
|
|
|
{
|
2020-11-30 12:14:53 +00:00
|
|
|
|
_prepareQueue.TryAdd(_testDatabases[id]);
|
2020-11-27 19:15:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-11-30 12:14:53 +00:00
|
|
|
|
private void CreateDatabase(TestDbMeta meta)
|
2020-11-27 19:15:49 +00:00
|
|
|
|
{
|
2020-11-30 12:14:53 +00:00
|
|
|
|
_log.LogInformation($"Creating database {meta.Name}");
|
2020-11-27 19:15:49 +00:00
|
|
|
|
using (var connection = new SqlConnection(_masterConnectionString))
|
|
|
|
|
|
{
|
|
|
|
|
|
connection.Open();
|
|
|
|
|
|
using (var command = connection.CreateCommand())
|
|
|
|
|
|
{
|
2020-11-30 12:14:53 +00:00
|
|
|
|
SetCommand(command, $@"CREATE DATABASE {LocalDb.QuotedName(meta.Name)}");
|
|
|
|
|
|
command.ExecuteNonQuery();
|
2020-11-27 19:15:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string ConstructConnectionString(string masterConnectionString, string databaseName)
|
|
|
|
|
|
{
|
|
|
|
|
|
var prefix = Regex.Replace(masterConnectionString, "Database=.+?;", string.Empty);
|
|
|
|
|
|
var connectionString = $"{prefix};Database={databaseName};";
|
|
|
|
|
|
return connectionString.Replace(";;", ";");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void SetCommand(SqlCommand command, string sql, params object[] args)
|
|
|
|
|
|
{
|
|
|
|
|
|
command.CommandType = CommandType.Text;
|
|
|
|
|
|
command.CommandText = sql;
|
|
|
|
|
|
command.Parameters.Clear();
|
|
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < args.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
command.Parameters.AddWithValue("@" + i, args[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-11-30 12:14:53 +00:00
|
|
|
|
private void RebuildSchema(IDbCommand command, TestDbMeta meta)
|
2020-11-27 19:15:49 +00:00
|
|
|
|
{
|
|
|
|
|
|
if (_cachedDatabaseInitCommands != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var dbCommand in _cachedDatabaseInitCommands)
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
if (dbCommand.Text.StartsWith("SELECT "))
|
|
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
command.CommandText = dbCommand.Text;
|
|
|
|
|
|
command.Parameters.Clear();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var parameterInfo in dbCommand.Parameters)
|
|
|
|
|
|
{
|
|
|
|
|
|
LocalDbTestDatabase.AddParameter(command, parameterInfo);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
command.ExecuteNonQuery();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2020-11-30 12:14:53 +00:00
|
|
|
|
_databaseFactory.Configure(meta.ConnectionString, Constants.DatabaseProviders.SqlServer);
|
2020-11-27 19:15:49 +00:00
|
|
|
|
|
|
|
|
|
|
using (var database = (UmbracoDatabase)_databaseFactory.CreateDatabase())
|
|
|
|
|
|
{
|
|
|
|
|
|
database.LogCommands = true;
|
|
|
|
|
|
|
|
|
|
|
|
using (var transaction = database.GetTransaction())
|
|
|
|
|
|
{
|
|
|
|
|
|
var schemaCreator = new DatabaseSchemaCreator(database, _loggerFactory.CreateLogger<DatabaseSchemaCreator>(), _loggerFactory, new UmbracoVersion());
|
|
|
|
|
|
schemaCreator.InitializeDatabaseSchema();
|
|
|
|
|
|
|
|
|
|
|
|
transaction.Complete();
|
|
|
|
|
|
|
|
|
|
|
|
_cachedDatabaseInitCommands = database.Commands.ToArray();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2020-11-30 12:14:53 +00:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2020-11-27 19:15:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|