From 51f20119a240a21f768dab4e4ecd569b1496806b Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Fri, 27 Nov 2020 19:15:49 +0000 Subject: [PATCH 1/7] Enable running integrations tests on Linux --- .gitattributes | 1 + .../Persistence/LocalDb.cs | 2 +- .../Testing/ITestDatabase.cs | 10 ++ .../Testing/IntegrationTestComposer.cs | 2 + .../Testing/LocalDbTestDatabase.cs | 20 +-- .../Testing/SqlDeveloperTestDatabase.cs | 163 ++++++++++++++++++ .../Testing/TestDatabaseFactory.cs | 44 +++++ .../Testing/UmbracoIntegrationTest.cs | 9 +- .../Services/Importing/ImportResources.resx | 4 +- .../Umbraco.Tests.Integration.csproj | 4 + 10 files changed, 232 insertions(+), 27 deletions(-) create mode 100644 src/Umbraco.Tests.Integration/Testing/ITestDatabase.cs create mode 100644 src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs create mode 100644 src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs diff --git a/.gitattributes b/.gitattributes index c8987ade67..3241b6511c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -43,6 +43,7 @@ *.hs text=auto *.json text=auto *.xml text=auto +*.resx text=auto *.csproj text=auto merge=union *.vbproj text=auto merge=union diff --git a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs index 4ec233e17f..89fce803b2 100644 --- a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs +++ b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs @@ -937,7 +937,7 @@ namespace Umbraco.Core.Persistence /// This is a C# implementation of T-SQL QUOTEDNAME. /// is optional, it can be '[' (default), ']', '\'' or '"'. /// - private static string QuotedName(string name, char quote = '[') + internal static string QuotedName(string name, char quote = '[') { switch (quote) { diff --git a/src/Umbraco.Tests.Integration/Testing/ITestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/ITestDatabase.cs new file mode 100644 index 0000000000..250e062ee4 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/ITestDatabase.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Tests.Integration.Testing +{ + public interface ITestDatabase + { + string ConnectionString { get; } + int AttachEmpty(); + int AttachSchema(); + void Detach(int id); + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs b/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs index dacfd950e0..39d74f8869 100644 --- a/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs +++ b/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs @@ -49,8 +49,10 @@ namespace Umbraco.Tests.Integration.Testing // we don't want persisted nucache files in tests builder.Services.AddTransient(factory => new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }); + #if IS_WINDOWS // ensure all lucene indexes are using RAM directory (no file system) builder.Services.AddUnique(); + #endif // replace this service so that it can lookup the correct file locations builder.Services.AddUnique(GetLocalizedTextService); diff --git a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs index 39f9ca5592..bb83914b63 100644 --- a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs @@ -20,7 +20,7 @@ namespace Umbraco.Tests.Integration.Testing /// /// Manages a pool of LocalDb databases for integration testing /// - public class LocalDbTestDatabase + public class LocalDbTestDatabase : ITestDatabase { public const string InstanceName = "UmbracoTests"; public const string DatabaseName = "UmbracoTests"; @@ -139,7 +139,7 @@ namespace Umbraco.Tests.Integration.Testing } - private static void AddParameter(IDbCommand cmd, UmbracoDatabase.ParameterInfo parameterInfo) + internal static void AddParameter(IDbCommand cmd, UmbracoDatabase.ParameterInfo parameterInfo) { var p = cmd.CreateParameter(); p.ParameterName = parameterInfo.Name; @@ -149,22 +149,6 @@ namespace Umbraco.Tests.Integration.Testing cmd.Parameters.Add(p); } - public void Clear() - { - var filename = Path.Combine(_filesPath, DatabaseName).ToUpper(); - - foreach (var database in _instance.GetDatabases()) - { - if (database.StartsWith(filename)) - _instance.DropDatabase(database); - } - - foreach (var file in Directory.EnumerateFiles(_filesPath)) - { - if (file.EndsWith(".mdf") == false && file.EndsWith(".ldf") == false) continue; - File.Delete(file); - } - } private static void ResetLocalDb(IDbCommand cmd) { diff --git a/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs new file mode 100644 index 0000000000..f5ae1661b8 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs @@ -0,0 +1,163 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Text.RegularExpressions; +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 +{ + /// + /// It's not meant to be pretty, rushed port of LocalDb.cs + LocalDbTestDatabase.cs + /// + public class SqlDeveloperTestDatabase : ITestDatabase + { + private readonly string _masterConnectionString; + private readonly string _databaseName; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _log; + private readonly IUmbracoDatabaseFactory _databaseFactory; + private UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands; + + 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; } + + public int AttachEmpty() + { + CreateDatabase(); + return -1; + } + + public int AttachSchema() + { + CreateDatabase(); + + _log.LogInformation($"Attaching schema {_databaseName}"); + + using (var connection = new SqlConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + RebuildSchema(command); + } + } + + return -1; + } + + 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(); + } + } + } + + private void CreateDatabase() + { + _log.LogInformation($"Creating database {_databaseName}"); + using (var connection = new SqlConnection(_masterConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + SetCommand(command, $@"CREATE DATABASE {LocalDb.QuotedName(_databaseName)}"); + var unused = command.ExecuteNonQuery(); + } + } + + ConnectionString = ConstructConnectionString(_masterConnectionString, _databaseName); + } + + 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]); + } + } + + private void RebuildSchema(IDbCommand command) + { + 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 + { + _databaseFactory.Configure(ConnectionString, Constants.DatabaseProviders.SqlServer); + + using (var database = (UmbracoDatabase)_databaseFactory.CreateDatabase()) + { + database.LogCommands = true; + + using (var transaction = database.GetTransaction()) + { + var schemaCreator = new DatabaseSchemaCreator(database, _loggerFactory.CreateLogger(), _loggerFactory, new UmbracoVersion()); + schemaCreator.InitializeDatabaseSchema(); + + transaction.Complete(); + + _cachedDatabaseInitCommands = database.Commands.ToArray(); + } + } + } + } + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs b/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs new file mode 100644 index 0000000000..0f403818b7 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using Umbraco.Core.Persistence; + +namespace Umbraco.Tests.Integration.Testing +{ + public class TestDatabaseFactory + { + public static ITestDatabase Create(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? CreateLocalDb(filesPath, loggerFactory, dbFactory) + : CreateSqlDeveloper(loggerFactory, dbFactory); + } + + private static ITestDatabase CreateLocalDb(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) + { + var localDb = new LocalDb(); + + if (!localDb.IsAvailable) + { + throw new InvalidOperationException("LocalDB is not available."); + } + + return new LocalDbTestDatabase(loggerFactory, localDb, filesPath, dbFactory); + } + + private static ITestDatabase CreateSqlDeveloper(ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) + { + // $ export SA_PASSWORD=Foobar123! + // $ export UmbracoIntegrationTestConnectionString="Server=localhost,1433;User Id=sa;Password=$SA_PASSWORD;" + // $ docker run -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=$SA_PASSWORD" -e 'MSSQL_PID=Developer' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu + var connectionString = Environment.GetEnvironmentVariable("UmbracoIntegrationTestConnectionString"); + + if (string.IsNullOrEmpty(connectionString)) + { + throw new InvalidOperationException("ENV: UmbracoIntegrationTestConnectionString is not set"); + } + + return new SqlDeveloperTestDatabase(loggerFactory, dbFactory, connectionString); + } + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index a8875de286..9627fdf42a 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -242,7 +242,7 @@ namespace Umbraco.Tests.Integration.Testing #region LocalDb private static readonly object _dbLocker = new object(); - private static LocalDbTestDatabase _dbInstance; + private static ITestDatabase _dbInstance; protected void UseTestLocalDb(IServiceProvider serviceProvider) { @@ -267,17 +267,14 @@ namespace Umbraco.Tests.Integration.Testing /// /// There must only be ONE instance shared between all tests in a session /// - private static LocalDbTestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) + private static ITestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) { lock (_dbLocker) { if (_dbInstance != null) return _dbInstance; - var localDb = new LocalDb(); - if (localDb.IsAvailable == false) - throw new InvalidOperationException("LocalDB is not available."); - _dbInstance = new LocalDbTestDatabase(loggerFactory, localDb, filesPath, dbFactory); + _dbInstance = TestDatabaseFactory.Create(filesPath, loggerFactory, dbFactory); return _dbInstance; } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.resx b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.resx index 5823fa1245..fdf7880297 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.resx +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.resx @@ -119,6 +119,6 @@ - dictionary-package.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + Dictionary-Package.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - \ No newline at end of file + diff --git a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index c5b42f9848..b996205712 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -7,6 +7,10 @@ 8 + + IS_WINDOWS + + From 897fe804b0c0e1bca21a1c180cd9bf7b0900666b Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Mon, 30 Nov 2020 12:14:53 +0000 Subject: [PATCH 2/7] Multiple test databases, similar setup to LocalDbTestDatabase.DatabasePool --- .../GlobalSetupTeardown.cs | 1 + .../Testing/LocalDbTestDatabase.cs | 4 +- .../Testing/SqlDeveloperTestDatabase.cs | 209 ++++++++++++++---- .../Testing/UmbracoIntegrationTest.cs | 2 +- 4 files changed, 171 insertions(+), 45 deletions(-) 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; } From 1b9d1428e6a2b16da21529c2f20e3d44238a2880 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Fri, 11 Dec 2020 14:35:12 +0000 Subject: [PATCH 3/7] Update ci pipeline build definition --- build/azure-pipelines.yml | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 8a99f941b0..d301b9c461 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -8,6 +8,19 @@ # Variables & their default values variables: buildConfiguration: 'Release' + SA_PASSWORD: UmbracoIntegration123! + +resources: + containers: + - container: mssql + image: mcr.microsoft.com/mssql/server:2017-latest + env: + ACCEPT_EULA: Y + SA_PASSWORD: $(SA_PASSWORD) + MSSQL_PID: Developer + ports: + - 1433:1433 + options: --name mssql stages: - stage: Linux @@ -31,6 +44,28 @@ stages: command: test projects: '**/*.Tests.UnitTests.csproj' + - job: Integration_Tests + services: + mssql: mssql + timeoutInMinutes: 120 + displayName: 'Integration Tests' + pool: + vmImage: 'ubuntu-latest' + steps: + + - task: UseDotNet@2 + displayName: 'Use .Net Core sdk 3.1.x' + inputs: + version: 3.1.x + + - task: DotNetCoreCLI@2 + displayName: 'dotnet test' + inputs: + command: test + projects: '**/Umbraco.Tests.Integration.csproj' + env: + UmbracoIntegrationTestConnectionString: 'Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD);' + - stage: macOS_X dependsOn: [] # this removes the implicit dependency on previous stage and causes this to run in parallel jobs: @@ -52,7 +87,6 @@ stages: command: test projects: '**/*.Tests.UnitTests.csproj' - - stage: Windows dependsOn: [] # this removes the implicit dependency on previous stage and causes this to run in parallel jobs: @@ -74,7 +108,6 @@ stages: command: test projects: '**\*.Tests.UnitTests.csproj' - - job: Integration_Tests timeoutInMinutes: 120 displayName: 'Integration Tests' @@ -102,7 +135,6 @@ stages: projects: '**\Umbraco.Tests.Integration.csproj' arguments: '--no-build' - - job: Build_Artifacts displayName: 'Build Artifacts' pool: From ae98983172418d0a0f3e8b12ccb45bfb014bca6d Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Fri, 11 Dec 2020 16:44:27 +0000 Subject: [PATCH 4/7] Fix PathTests --- .../Repositories/PartialViewRepositoryTests.cs | 11 ++++++----- .../Persistence/Repositories/ScriptRepositoryTest.cs | 12 ++++++------ .../Repositories/StylesheetRepositoryTest.cs | 12 ++++++------ 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs index 1ae46faa76..ffda46ed0d 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Tests.Testing; using System; +using System.IO; using Umbraco.Core.Hosting; using Umbraco.Tests.Integration.Testing; @@ -55,28 +56,28 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositor partialView = new PartialView(PartialViewType.PartialView, "path-2/test-path-2.cshtml") { Content = "// partialView" }; repository.Save(partialView); Assert.IsTrue(_fileSystem.FileExists("path-2/test-path-2.cshtml")); - Assert.AreEqual("path-2\\test-path-2.cshtml", partialView.Path); // fixed in 7.3 - 7.2.8 does not update the path + Assert.AreEqual("path-2\\test-path-2.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); // fixed in 7.3 - 7.2.8 does not update the path Assert.AreEqual("/Views/Partials/path-2/test-path-2.cshtml", partialView.VirtualPath); partialView = (PartialView) repository.Get("path-2/test-path-2.cshtml"); Assert.IsNotNull(partialView); - Assert.AreEqual("path-2\\test-path-2.cshtml", partialView.Path); + Assert.AreEqual("path-2\\test-path-2.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-2.cshtml", partialView.VirtualPath); partialView = new PartialView(PartialViewType.PartialView, "path-2\\test-path-3.cshtml") { Content = "// partialView" }; repository.Save(partialView); Assert.IsTrue(_fileSystem.FileExists("path-2/test-path-3.cshtml")); - Assert.AreEqual("path-2\\test-path-3.cshtml", partialView.Path); + Assert.AreEqual("path-2\\test-path-3.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-3.cshtml", partialView.VirtualPath); partialView = (PartialView) repository.Get("path-2/test-path-3.cshtml"); Assert.IsNotNull(partialView); - Assert.AreEqual("path-2\\test-path-3.cshtml", partialView.Path); + Assert.AreEqual("path-2\\test-path-3.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-3.cshtml", partialView.VirtualPath); partialView = (PartialView) repository.Get("path-2\\test-path-3.cshtml"); Assert.IsNotNull(partialView); - Assert.AreEqual("path-2\\test-path-3.cshtml", partialView.Path); + Assert.AreEqual("path-2\\test-path-3.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-3.cshtml", partialView.VirtualPath); partialView = new PartialView(PartialViewType.PartialView, "\\test-path-4.cshtml") { Content = "// partialView" }; diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs index 9e4ae80ec6..f9084332b7 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs @@ -280,36 +280,36 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositor repository.Save(script); Assert.IsTrue(_fileSystem.FileExists("scripts/path-2/test-path-2.js")); - Assert.AreEqual("scripts\\path-2\\test-path-2.js", script.Path); + Assert.AreEqual("scripts\\path-2\\test-path-2.js".Replace("\\", $"{Path.DirectorySeparatorChar}"), script.Path); Assert.AreEqual("/scripts/scripts/path-2/test-path-2.js", script.VirtualPath); script = new Script("path-2/test-path-2.js") { Content = "// script" }; repository.Save(script); Assert.IsTrue(_fileSystem.FileExists("path-2/test-path-2.js")); - Assert.AreEqual("path-2\\test-path-2.js", script.Path); // fixed in 7.3 - 7.2.8 does not update the path + Assert.AreEqual("path-2\\test-path-2.js".Replace("\\", $"{Path.DirectorySeparatorChar}"), script.Path);// fixed in 7.3 - 7.2.8 does not update the path Assert.AreEqual("/scripts/path-2/test-path-2.js", script.VirtualPath); script = repository.Get("path-2/test-path-2.js"); Assert.IsNotNull(script); - Assert.AreEqual("path-2\\test-path-2.js", script.Path); + Assert.AreEqual("path-2\\test-path-2.js".Replace("\\", $"{Path.DirectorySeparatorChar}"), script.Path); Assert.AreEqual("/scripts/path-2/test-path-2.js", script.VirtualPath); script = new Script("path-2\\test-path-3.js") { Content = "// script" }; repository.Save(script); Assert.IsTrue(_fileSystem.FileExists("path-2/test-path-3.js")); - Assert.AreEqual("path-2\\test-path-3.js", script.Path); + Assert.AreEqual("path-2\\test-path-3.js".Replace("\\", $"{Path.DirectorySeparatorChar}"), script.Path); Assert.AreEqual("/scripts/path-2/test-path-3.js", script.VirtualPath); script = repository.Get("path-2/test-path-3.js"); Assert.IsNotNull(script); - Assert.AreEqual("path-2\\test-path-3.js", script.Path); + Assert.AreEqual("path-2\\test-path-3.js".Replace("\\", $"{Path.DirectorySeparatorChar}"), script.Path); Assert.AreEqual("/scripts/path-2/test-path-3.js", script.VirtualPath); script = repository.Get("path-2\\test-path-3.js"); Assert.IsNotNull(script); - Assert.AreEqual("path-2\\test-path-3.js", script.Path); + Assert.AreEqual("path-2\\test-path-3.js".Replace("\\", $"{Path.DirectorySeparatorChar}"), script.Path); Assert.AreEqual("/scripts/path-2/test-path-3.js", script.VirtualPath); script = new Script("\\test-path-4.js") { Content = "// script" }; diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs index a576666e6e..b4b8316f83 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs @@ -135,7 +135,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositor stylesheet = repository.Get(stylesheet.Name); //Assert - Assert.That(stylesheet.Content, Is.EqualTo("body { color:#000; } .bold {font-weight:bold;}\r\n\r\n/**umb_name:Test*/\r\np {\r\n\tfont-size:2em;\r\n}")); + Assert.That(stylesheet.Content, Is.EqualTo("body { color:#000; } .bold {font-weight:bold;}\r\n\r\n/**umb_name:Test*/\r\np {\r\n\tfont-size:2em;\r\n}".Replace("\r\n", Environment.NewLine))); Assert.AreEqual(1, stylesheet.Properties.Count()); } } @@ -281,29 +281,29 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositor repository.Save(stylesheet); Assert.IsTrue(_fileSystem.FileExists("path-2/test-path-2.css")); - Assert.AreEqual("path-2\\test-path-2.css", stylesheet.Path); // fixed in 7.3 - 7.2.8 does not update the path + Assert.AreEqual("path-2\\test-path-2.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path);// fixed in 7.3 - 7.2.8 does not update the path Assert.AreEqual("/css/path-2/test-path-2.css", stylesheet.VirtualPath); stylesheet = repository.Get("path-2/test-path-2.css"); Assert.IsNotNull(stylesheet); - Assert.AreEqual("path-2\\test-path-2.css", stylesheet.Path); + Assert.AreEqual("path-2\\test-path-2.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); Assert.AreEqual("/css/path-2/test-path-2.css", stylesheet.VirtualPath); stylesheet = new Stylesheet("path-2\\test-path-3.css") { Content = "body { color:#000; } .bold {font-weight:bold;}" }; repository.Save(stylesheet); Assert.IsTrue(_fileSystem.FileExists("path-2/test-path-3.css")); - Assert.AreEqual("path-2\\test-path-3.css", stylesheet.Path); + Assert.AreEqual("path-2\\test-path-3.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); Assert.AreEqual("/css/path-2/test-path-3.css", stylesheet.VirtualPath); stylesheet = repository.Get("path-2/test-path-3.css"); Assert.IsNotNull(stylesheet); - Assert.AreEqual("path-2\\test-path-3.css", stylesheet.Path); + Assert.AreEqual("path-2\\test-path-3.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); Assert.AreEqual("/css/path-2/test-path-3.css", stylesheet.VirtualPath); stylesheet = repository.Get("path-2\\test-path-3.css"); Assert.IsNotNull(stylesheet); - Assert.AreEqual("path-2\\test-path-3.css", stylesheet.Path); + Assert.AreEqual("path-2\\test-path-3.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); Assert.AreEqual("/css/path-2/test-path-3.css", stylesheet.VirtualPath); stylesheet = new Stylesheet("\\test-path-4.css") { Content = "body { color:#000; } .bold {font-weight:bold;}" }; From 312ab962779e75fd4ea6f00c3359cf35105eafff Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Fri, 11 Dec 2020 18:20:07 +0000 Subject: [PATCH 5/7] Resolve issues with AdvancedMigrationTests --- .../Persistence/UmbracoDatabaseFactory.cs | 11 ----- .../UmbracoTestServerTestBase.cs | 3 +- .../Testing/SqlDeveloperTestDatabase.cs | 3 +- .../Testing/TestDatabaseFactory.cs | 6 +-- .../TestUmbracoDatabaseFactoryProvider.cs | 48 +++++++++++++++++++ .../Testing/UmbracoIntegrationTest.cs | 22 ++++----- 6 files changed, 64 insertions(+), 29 deletions(-) create mode 100644 src/Umbraco.Tests.Integration/Testing/TestUmbracoDatabaseFactoryProvider.cs diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index 7b98bd150e..5c3c984677 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -327,16 +327,5 @@ namespace Umbraco.Core.Persistence //db?.Dispose(); Volatile.Write(ref _initialized, false); } - - // during tests, the thread static var can leak between tests - // this method provides a way to force-reset the variable - internal void ResetForTests() - { - // TODO: remove all this eventually - //var db = _umbracoDatabaseAccessor.UmbracoDatabase; - //_umbracoDatabaseAccessor.UmbracoDatabase = null; - //db?.Dispose(); - //_databaseScopeAccessor.Scope = null; - } } } diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index d60f49971a..93769eaaed 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -132,11 +132,12 @@ namespace Umbraco.Tests.Integration.TestServerTest public override void ConfigureServices(IServiceCollection services) { + services.AddTransient(); var typeLoader = services.AddTypeLoader(GetType().Assembly, TestHelper.GetWebHostEnvironment(), TestHelper.GetHostingEnvironment(), TestHelper.ConsoleLoggerFactory, AppCaches.NoCache, Configuration, TestHelper.Profiler); var builder = new UmbracoBuilder(services, Configuration, typeLoader); - + builder .AddConfiguration() .AddTestCore(TestHelper) // This is the important one! diff --git a/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs index 560bcbe00a..52be6d5472 100644 --- a/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs @@ -31,7 +31,7 @@ namespace Umbraco.Tests.Integration.Testing private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly IDictionary _testDatabases; private UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands; - + private BlockingCollection _prepareQueue; private BlockingCollection _readySchemaQueue; private BlockingCollection _readyEmptyQueue; @@ -54,7 +54,6 @@ namespace Umbraco.Tests.Integration.Testing new TestDbMeta(2, false, masterConnectionString), new TestDbMeta(3, true, masterConnectionString), - new TestDbMeta(4, true, masterConnectionString), }.ToDictionary(x => x.Id); Instance = this; // For GlobalSetupTeardown.cs diff --git a/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs b/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs index 0f403818b7..3ed2b6b49a 100644 --- a/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs +++ b/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs @@ -7,11 +7,11 @@ namespace Umbraco.Tests.Integration.Testing { public class TestDatabaseFactory { - public static ITestDatabase Create(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) + public static ITestDatabase Create(string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) { return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? CreateLocalDb(filesPath, loggerFactory, dbFactory) - : CreateSqlDeveloper(loggerFactory, dbFactory); + ? CreateLocalDb(filesPath, loggerFactory, dbFactory.Create()) + : CreateSqlDeveloper(loggerFactory, dbFactory.Create()); } private static ITestDatabase CreateLocalDb(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) diff --git a/src/Umbraco.Tests.Integration/Testing/TestUmbracoDatabaseFactoryProvider.cs b/src/Umbraco.Tests.Integration/Testing/TestUmbracoDatabaseFactoryProvider.cs new file mode 100644 index 0000000000..3eb3757207 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/TestUmbracoDatabaseFactoryProvider.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Mappers; + +namespace Umbraco.Tests.Integration.Testing +{ + /// + /// I want to be able to create a database for integration testsing without setting the connection string on the + /// singleton database factory forever. + /// + public class TestUmbracoDatabaseFactoryProvider + { + private readonly ILoggerFactory _loggerFactory; + private readonly IOptions _globalSettings; + private readonly IOptions _connectionStrings; + private readonly Lazy _mappers; + private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + + public TestUmbracoDatabaseFactoryProvider( + ILoggerFactory loggerFactory, + IOptions globalSettings, + IOptions connectionStrings, + Lazy mappers, + IDbProviderFactoryCreator dbProviderFactoryCreator) + { + _loggerFactory = loggerFactory; + _globalSettings = globalSettings; + _connectionStrings = connectionStrings; + _mappers = mappers; + _dbProviderFactoryCreator = dbProviderFactoryCreator; + } + + public IUmbracoDatabaseFactory Create() + { + // ReSharper disable once ArrangeMethodOrOperatorBody + return new UmbracoDatabaseFactory( + _loggerFactory.CreateLogger(), + _loggerFactory, + _globalSettings.Value, + _connectionStrings.Value, + _mappers, + _dbProviderFactoryCreator); + } + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index a376aff38d..3da4ec94d6 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -165,6 +165,7 @@ namespace Umbraco.Tests.Integration.Testing public virtual void ConfigureServices(IServiceCollection services) { services.AddSingleton(TestHelper.DbProviderFactoryCreator); + services.AddTransient(); var webHostEnvironment = TestHelper.GetWebHostEnvironment(); services.AddRequiredNetCoreServices(TestHelper, webHostEnvironment); @@ -247,10 +248,11 @@ namespace Umbraco.Tests.Integration.Testing protected void UseTestLocalDb(IServiceProvider serviceProvider) { var state = serviceProvider.GetRequiredService(); + var testDatabaseFactoryProvider = serviceProvider.GetRequiredService(); var databaseFactory = serviceProvider.GetRequiredService(); // This will create a db, install the schema and ensure the app is configured to run - InstallTestLocalDb(databaseFactory, serviceProvider.GetRequiredService(), state, TestHelper.WorkingDirectory); + InstallTestLocalDb(testDatabaseFactoryProvider, databaseFactory, serviceProvider.GetRequiredService(), state, TestHelper.WorkingDirectory); TestDBConnectionString = databaseFactory.ConnectionString; InMemoryConfiguration["ConnectionStrings:" + Constants.System.UmbracoConnectionName] = TestDBConnectionString; } @@ -267,7 +269,7 @@ namespace Umbraco.Tests.Integration.Testing /// /// There must only be ONE instance shared between all tests in a session /// - private static ITestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) + private static ITestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) { lock (_dbLocker) { @@ -282,16 +284,12 @@ namespace Umbraco.Tests.Integration.Testing /// /// Creates a LocalDb instance to use for the test /// - /// - /// - /// - /// - /// - /// - /// private void InstallTestLocalDb( - IUmbracoDatabaseFactory databaseFactory, ILoggerFactory loggerFactory, - IRuntimeState runtimeState, string workingDirectory) + TestUmbracoDatabaseFactoryProvider testUmbracoDatabaseFactoryProvider, + IUmbracoDatabaseFactory databaseFactory, + ILoggerFactory loggerFactory, + IRuntimeState runtimeState, + string workingDirectory) { var dbFilePath = Path.Combine(workingDirectory, "LocalDb"); @@ -307,7 +305,7 @@ namespace Umbraco.Tests.Integration.Testing if (!Directory.Exists(dbFilePath)) Directory.CreateDirectory(dbFilePath); - var db = GetOrCreateDatabase(dbFilePath, loggerFactory, databaseFactory); + var db = GetOrCreateDatabase(dbFilePath, loggerFactory, testUmbracoDatabaseFactoryProvider); switch (testOptions.Database) { From 4dbe5d0c3888c664c0152eb08bbfdc78f72f2953 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Sat, 12 Dec 2020 11:33:57 +0000 Subject: [PATCH 6/7] Consolidate LocalDbTestDatabase and SqlDeveloperTestDatabase Resolve issue with multiple empties --- .../GlobalSetupTeardown.cs | 4 +- .../Testing/BaseTestDatabase.cs | 218 +++++++++++ .../Testing/ITestDatabase.cs | 9 +- .../Testing/LocalDbTestDatabase.cs | 351 ++++-------------- .../Testing/SqlDeveloperTestDatabase.cs | 218 ++--------- .../Testing/TestDatabaseFactory.cs | 5 +- .../Testing/TestDbMeta.cs | 39 ++ .../Testing/UmbracoIntegrationTest.cs | 31 +- .../TestHelpers/TestWithDatabaseBase.cs | 3 +- 9 files changed, 367 insertions(+), 511 deletions(-) create mode 100644 src/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs create mode 100644 src/Umbraco.Tests.Integration/Testing/TestDbMeta.cs diff --git a/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs b/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs index 0fdac242d9..6e86e97770 100644 --- a/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs +++ b/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; @@ -23,7 +23,7 @@ public class TestsSetup [OneTimeTearDown] public void TearDown() { - LocalDbTestDatabase.KillLocalDb(); + LocalDbTestDatabase.Instance?.Finish(); SqlDeveloperTestDatabase.Instance?.Finish(); Console.WriteLine("TOTAL TESTS DURATION: {0}", _stopwatch.Elapsed); } diff --git a/src/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs new file mode 100644 index 0000000000..02a3da676a --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using Microsoft.Extensions.Logging; +using Umbraco.Core.Configuration; +using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Persistence; + +namespace Umbraco.Tests.Integration.Testing +{ + public abstract class BaseTestDatabase + { + protected ILoggerFactory _loggerFactory; + protected IUmbracoDatabaseFactory _databaseFactory; + protected IEnumerable _testDatabases; + + protected UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands; + + protected BlockingCollection _prepareQueue; + protected BlockingCollection _readySchemaQueue; + protected BlockingCollection _readyEmptyQueue; + + protected abstract void Initialize(); + + public TestDbMeta AttachEmpty() + { + if (_prepareQueue == null) + { + Initialize(); + } + + return _readyEmptyQueue.Take(); + } + + public TestDbMeta AttachSchema() + { + if (_prepareQueue == null) + { + Initialize(); + } + + return _readySchemaQueue.Take(); + } + + public void Detach(TestDbMeta meta) + { + _prepareQueue.TryAdd(meta); + } + + protected void PrepareDatabase() + { + 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(); + ResetTestDatabase(cmd); + + if (!meta.IsEmpty) + { + RebuildSchema(cmd, meta); + } + } + + if (!meta.IsEmpty) + { + _readySchemaQueue.TryAdd(meta); + } + else + { + _readyEmptyQueue.TryAdd(meta); + } + } + }); + } + + protected void RebuildSchema(IDbCommand command, TestDbMeta meta) + { + 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) + { + AddParameter(command, parameterInfo); + } + + command.ExecuteNonQuery(); + } + } + else + { + _databaseFactory.Configure(meta.ConnectionString, Core.Constants.DatabaseProviders.SqlServer); + + using (var database = (UmbracoDatabase)_databaseFactory.CreateDatabase()) + { + database.LogCommands = true; + + using (var transaction = database.GetTransaction()) + { + var schemaCreator = new DatabaseSchemaCreator(database, _loggerFactory.CreateLogger(), _loggerFactory, new UmbracoVersion()); + schemaCreator.InitializeDatabaseSchema(); + + transaction.Complete(); + + _cachedDatabaseInitCommands = database.Commands.ToArray(); + } + } + } + } + + protected 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]); + } + } + + protected static void AddParameter(IDbCommand cmd, UmbracoDatabase.ParameterInfo parameterInfo) + { + var p = cmd.CreateParameter(); + p.ParameterName = parameterInfo.Name; + p.Value = parameterInfo.Value; + p.DbType = parameterInfo.DbType; + p.Size = parameterInfo.Size; + cmd.Parameters.Add(p); + } + + protected static void ResetTestDatabase(IDbCommand cmd) + { + // https://stackoverflow.com/questions/536350 + + cmd.CommandType = CommandType.Text; + cmd.CommandText = @" + declare @n char(1); + set @n = char(10); + declare @stmt nvarchar(max); + -- check constraints + select @stmt = isnull( @stmt + @n, '' ) + + 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' + from sys.check_constraints; + -- foreign keys + select @stmt = isnull( @stmt + @n, '' ) + + 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' + from sys.foreign_keys; + -- tables + select @stmt = isnull( @stmt + @n, '' ) + + 'drop table [' + schema_name(schema_id) + '].[' + name + ']' + from sys.tables; + exec sp_executesql @stmt; + "; + + // rudimentary retry policy since a db can still be in use when we try to drop + Retry(10, () => cmd.ExecuteNonQuery()); + } + + protected static void Retry(int maxIterations, Action action) + { + for (var i = 0; i < maxIterations; i++) + { + try + { + action(); + return; + } + catch (SqlException) + { + + //Console.Error.WriteLine($"SqlException occured, but we try again {i+1}/{maxIterations}.\n{e}"); + // This can occur when there's a transaction deadlock which means (i think) that the database is still in use and hasn't been closed properly yet + // so we need to just wait a little bit + Thread.Sleep(100 * i); + if (i == maxIterations - 1) + { + Debugger.Launch(); + throw; + } + } + catch (InvalidOperationException) + { + // Ignore + } + } + } + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/ITestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/ITestDatabase.cs index 250e062ee4..28d7e9c8bc 100644 --- a/src/Umbraco.Tests.Integration/Testing/ITestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/ITestDatabase.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Tests.Integration.Testing +namespace Umbraco.Tests.Integration.Testing { public interface ITestDatabase { - string ConnectionString { get; } - int AttachEmpty(); - int AttachSchema(); - void Detach(int id); + TestDbMeta AttachEmpty(); + TestDbMeta AttachSchema(); + void Detach(TestDbMeta id); } } diff --git a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs index 0a081f48e9..a9a842cdcd 100644 --- a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs @@ -1,18 +1,8 @@ -using System; +using System; using System.Collections.Concurrent; -using System.Configuration; -using System.Data; -using System.Data.Common; -using System.Data.SqlClient; -using System.Diagnostics; using System.IO; -using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Migrations.Install; using Umbraco.Core.Persistence; namespace Umbraco.Tests.Integration.Testing @@ -20,181 +10,104 @@ namespace Umbraco.Tests.Integration.Testing /// /// Manages a pool of LocalDb databases for integration testing /// - public class LocalDbTestDatabase : ITestDatabase + public class LocalDbTestDatabase : BaseTestDatabase, ITestDatabase { public const string InstanceName = "UmbracoTests"; public const string DatabaseName = "UmbracoTests"; - private readonly ILoggerFactory _loggerFactory; private readonly LocalDb _localDb; - private readonly IUmbracoVersion _umbracoVersion; - private static LocalDb.Instance _instance; + private static LocalDb.Instance _localDbInstance; private static string _filesPath; - private readonly IUmbracoDatabaseFactory _dbFactory; - private UmbracoDatabase.CommandInfo[] _dbCommands; - private string _currentCstr; - private static DatabasePool _emptyPool; - private static DatabasePool _schemaPool; - private DatabasePool _currentPool; + + private const int _threadCount = 2; + + public static LocalDbTestDatabase Instance { get; private set; } //It's internal because `Umbraco.Core.Persistence.LocalDb` is internal internal LocalDbTestDatabase(ILoggerFactory loggerFactory, LocalDb localDb, string filesPath, IUmbracoDatabaseFactory dbFactory) { - _umbracoVersion = new UmbracoVersion(); _loggerFactory = loggerFactory; + _databaseFactory = dbFactory; + _localDb = localDb; _filesPath = filesPath; - _dbFactory = dbFactory; - _instance = _localDb.GetInstance(InstanceName); - if (_instance != null) return; + Instance = this; // For GlobalSetupTeardown.cs + + _testDatabases = new[] + { + // With Schema + TestDbMeta.CreateWithoutConnectionString($"{DatabaseName}-1", false), + TestDbMeta.CreateWithoutConnectionString($"{DatabaseName}-2", false), + + // Empty (for migration testing etc) + TestDbMeta.CreateWithoutConnectionString($"{DatabaseName}-3", true), + TestDbMeta.CreateWithoutConnectionString($"{DatabaseName}-4", true), + }; + + _localDbInstance = _localDb.GetInstance(InstanceName); + if (_localDbInstance != null) + { + return; + } if (_localDb.CreateInstance(InstanceName) == false) + { throw new Exception("Failed to create a LocalDb instance."); - _instance = _localDb.GetInstance(InstanceName); + } + + _localDbInstance = _localDb.GetInstance(InstanceName); } - public string ConnectionString => _currentCstr ?? _instance.GetAttachedConnectionString("XXXXXX", _filesPath); - - private void Create() + protected override void Initialize() { var tempName = Guid.NewGuid().ToString("N"); - _instance.CreateDatabase(tempName, _filesPath); - _instance.DetachDatabase(tempName); + _localDbInstance.CreateDatabase(tempName, _filesPath); + _localDbInstance.DetachDatabase(tempName); + _prepareQueue = new BlockingCollection(); + _readySchemaQueue = new BlockingCollection(); + _readyEmptyQueue = new BlockingCollection(); - // there's probably a sweet spot to be found for size / parallel... - - var s = ConfigurationManager.AppSettings["Umbraco.Tests.LocalDbTestDatabase.EmptyPoolSize"]; - var emptySize = s == null ? 1 : int.Parse(s); - s = ConfigurationManager.AppSettings["Umbraco.Tests.LocalDbTestDatabase.EmptyPoolThreadCount"]; - var emptyParallel = s == null ? 1 : int.Parse(s); - s = ConfigurationManager.AppSettings["Umbraco.Tests.LocalDbTestDatabase.SchemaPoolSize"]; - var schemaSize = s == null ? 1 : int.Parse(s); - s = ConfigurationManager.AppSettings["Umbraco.Tests.LocalDbTestDatabase.SchemaPoolThreadCount"]; - var schemaParallel = s == null ? 1 : int.Parse(s); - - _emptyPool = new DatabasePool(_localDb, _instance, DatabaseName + "-Empty", tempName, _filesPath, emptySize, emptyParallel); - _schemaPool = new DatabasePool(_localDb, _instance, DatabaseName + "-Schema", tempName, _filesPath, schemaSize, schemaParallel, delete: true, prepare: RebuildSchema); - } - - public int AttachEmpty() - { - if (_emptyPool == null) - Create(); - - _currentCstr = _emptyPool.AttachDatabase(out var id); - _currentPool = _emptyPool; - return id; - } - - public int AttachSchema() - { - if (_schemaPool == null) - Create(); - - _currentCstr = _schemaPool.AttachDatabase(out var id); - _currentPool = _schemaPool; - return id; - } - - public void Detach(int id) - { - _currentPool.DetachDatabase(id); - } - - private void RebuildSchema(DbConnection conn, IDbCommand cmd) - { - - if (_dbCommands != null) + foreach (var meta in _testDatabases) { - foreach (var dbCommand in _dbCommands) - { - - if (dbCommand.Text.StartsWith("SELECT ")) continue; - - cmd.CommandText = dbCommand.Text; - cmd.Parameters.Clear(); - foreach (var parameterInfo in dbCommand.Parameters) - AddParameter(cmd, parameterInfo); - cmd.ExecuteNonQuery(); - } - } - else - { - _dbFactory.Configure(conn.ConnectionString, Constants.DatabaseProviders.SqlServer); - - using var database = (UmbracoDatabase)_dbFactory.CreateDatabase(); - // track each db command ran as part of creating the database so we can replay these - database.LogCommands = true; - - using var trans = database.GetTransaction(); - - var creator = new DatabaseSchemaCreator(database, _loggerFactory.CreateLogger(), _loggerFactory, _umbracoVersion); - creator.InitializeDatabaseSchema(); - - trans.Complete(); // commit it - - _dbCommands = database.Commands.ToArray(); + _localDb.CopyDatabaseFiles(tempName, _filesPath, targetDatabaseName: meta.Name, overwrite: true, delete: false); + meta.ConnectionString = _localDbInstance.GetAttachedConnectionString(meta.Name, _filesPath); + _prepareQueue.Add(meta); } - } - - internal static void AddParameter(IDbCommand cmd, UmbracoDatabase.ParameterInfo parameterInfo) - { - var p = cmd.CreateParameter(); - p.ParameterName = parameterInfo.Name; - p.Value = parameterInfo.Value; - p.DbType = parameterInfo.DbType; - p.Size = parameterInfo.Size; - cmd.Parameters.Add(p); - } - - - internal static void ResetLocalDb(IDbCommand cmd) - { - // https://stackoverflow.com/questions/536350 - - cmd.CommandType = CommandType.Text; - cmd.CommandText = @" - declare @n char(1); - set @n = char(10); - declare @stmt nvarchar(max); - -- check constraints - select @stmt = isnull( @stmt + @n, '' ) + - 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' - from sys.check_constraints; - -- foreign keys - select @stmt = isnull( @stmt + @n, '' ) + - 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' - from sys.foreign_keys; - -- tables - select @stmt = isnull( @stmt + @n, '' ) + - 'drop table [' + schema_name(schema_id) + '].[' + name + ']' - from sys.tables; - exec sp_executesql @stmt; - "; - - // rudimentary retry policy since a db can still be in use when we try to drop - Retry(10, () => + for (var i = 0; i < _threadCount; i++) { - cmd.ExecuteNonQuery(); - }); + var thread = new Thread(PrepareDatabase); + thread.Start(); + } } - public static void KillLocalDb() + public void Finish() { - _emptyPool?.Stop(); - _schemaPool?.Stop(); + if (_prepareQueue == null) + return; + + _prepareQueue.CompleteAdding(); + while (_prepareQueue.TryTake(out _)) + { } + + _readyEmptyQueue.CompleteAdding(); + while (_readyEmptyQueue.TryTake(out _)) + { } + + _readySchemaQueue.CompleteAdding(); + while (_readySchemaQueue.TryTake(out _)) + { } if (_filesPath == null) return; var filename = Path.Combine(_filesPath, DatabaseName).ToUpper(); - foreach (var database in _instance.GetDatabases()) + foreach (var database in _localDbInstance.GetDatabases()) { if (database.StartsWith(filename)) - _instance.DropDatabase(database); + _localDbInstance.DropDatabase(database); } foreach (var file in Directory.EnumerateFiles(_filesPath)) @@ -210,145 +123,5 @@ namespace Umbraco.Tests.Integration.Testing } } } - - internal static void Retry(int maxIterations, Action action) - { - for (var i = 0; i < maxIterations; i++) - { - try - { - action(); - return; - } - catch (SqlException) - { - - //Console.Error.WriteLine($"SqlException occured, but we try again {i+1}/{maxIterations}.\n{e}"); - // This can occur when there's a transaction deadlock which means (i think) that the database is still in use and hasn't been closed properly yet - // so we need to just wait a little bit - Thread.Sleep(100 * i); - if (i == maxIterations - 1) - { - Debugger.Launch(); - throw; - } - } - catch (InvalidOperationException) - { - - } - } - } - - private class DatabasePool - { - private readonly LocalDb _localDb; - private readonly LocalDb.Instance _instance; - private readonly string _filesPath; - private readonly string _name; - private readonly int _size; - private readonly string[] _cstrs; - private readonly BlockingCollection _prepareQueue, _readyQueue; - private readonly Action _prepare; - private int _current; - - public DatabasePool(LocalDb localDb, LocalDb.Instance instance, string name, string tempName, string filesPath, int size, int parallel = 1, Action prepare = null, bool delete = false) - { - _localDb = localDb; - _instance = instance; - _filesPath = filesPath; - _name = name; - _size = size; - _prepare = prepare; - _prepareQueue = new BlockingCollection(); - _readyQueue = new BlockingCollection(); - _cstrs = new string[_size]; - - for (var i = 0; i < size; i++) - localDb.CopyDatabaseFiles(tempName, filesPath, targetDatabaseName: name + "-" + i, overwrite: true, delete: delete && i == size - 1); - - if (prepare == null) - { - for (var i = 0; i < size; i++) - _readyQueue.Add(i); - } - else - { - for (var i = 0; i < size; i++) - _prepareQueue.Add(i); - } - - for (var i = 0; i < parallel; i++) - { - var thread = new Thread(PrepareThread); - thread.Start(); - } - } - - public string AttachDatabase(out int id) - { - _current = _readyQueue.Take(); - id = _current; - - return ConnectionString(_current); - } - - public void DetachDatabase(int id) - { - if (id != _current) - throw new InvalidOperationException("Cannot detatch the non-current db"); - - _prepareQueue.Add(_current); - } - - private string ConnectionString(int i) - { - return _cstrs[i] ?? (_cstrs[i] = _instance.GetAttachedConnectionString(_name + "-" + i, _filesPath)); - } - - private void PrepareThread() - { - Retry(10, () => - { - while (_prepareQueue.IsCompleted == false) - { - int i; - try - { - i = _prepareQueue.Take(); - } - catch (InvalidOperationException) - { - continue; - } - - using (var conn = new SqlConnection(ConnectionString(i))) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - ResetLocalDb(cmd); - - _prepare?.Invoke(conn, cmd); - - } - - if (!_readyQueue.IsAddingCompleted) - { - _readyQueue.Add(i); - } - } - }); - } - - public void Stop() - { - int i; - _prepareQueue.CompleteAdding(); - while (_prepareQueue.TryTake(out i)) { } - _readyQueue.CompleteAdding(); - while (_readyQueue.TryTake(out i)) { } - } - } - } } diff --git a/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs index 52be6d5472..4a7f602ac6 100644 --- a/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs @@ -1,15 +1,8 @@ -using System; +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; -using Umbraco.Core.Migrations.Install; using Umbraco.Core.Persistence; // ReSharper disable ConvertToUsingDeclaration @@ -19,82 +12,57 @@ namespace Umbraco.Tests.Integration.Testing /// /// It's not meant to be pretty, rushed port of LocalDb.cs + LocalDbTestDatabase.cs /// - public class SqlDeveloperTestDatabase : ITestDatabase + public class SqlDeveloperTestDatabase : BaseTestDatabase, 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 ILoggerFactory _loggerFactory; - private readonly ILogger _log; - private readonly IUmbracoDatabaseFactory _databaseFactory; - private readonly IDictionary _testDatabases; - private UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands; + public const string DatabaseName = "UmbracoTests"; - private BlockingCollection _prepareQueue; - private BlockingCollection _readySchemaQueue; - private BlockingCollection _readyEmptyQueue; - - private const string _databasePrefix = "UmbracoTest"; private const int _threadCount = 2; - public static SqlDeveloperTestDatabase Instance; + public static SqlDeveloperTestDatabase Instance { get; private set; } 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(); _testDatabases = new[] { - new TestDbMeta(1, false, masterConnectionString), - new TestDbMeta(2, false, masterConnectionString), + // With Schema + TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-1", false, masterConnectionString), + TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-2", false, masterConnectionString), - new TestDbMeta(3, true, masterConnectionString), - }.ToDictionary(x => x.Id); + // Empty (for migration testing etc) + TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-3", true, masterConnectionString), + TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-4", true, masterConnectionString), + }; Instance = this; // For GlobalSetupTeardown.cs } - public int AttachEmpty() + protected override void Initialize() { - if (_prepareQueue == null) + _prepareQueue = new BlockingCollection(); + _readySchemaQueue = new BlockingCollection(); + _readyEmptyQueue = new BlockingCollection(); + + foreach (var meta in _testDatabases) { - Initialize(); + CreateDatabase(meta); + _prepareQueue.Add(meta); } - var meta = _readyEmptyQueue.Take(); - - ConnectionString = meta.ConnectionString; - - return meta.Id; - } - - public int AttachSchema() - { - if (_prepareQueue == null) + for (var i = 0; i < _threadCount; i++) { - Initialize(); + var thread = new Thread(PrepareDatabase); + thread.Start(); } - - var meta = _readySchemaQueue.Take(); - - ConnectionString = meta.ConnectionString; - - return meta.Id; - } - - public void Detach(int id) - { - _prepareQueue.TryAdd(_testDatabases[id]); } private void CreateDatabase(TestDbMeta meta) { - _log.LogInformation($"Creating database {meta.Name}"); using (var connection = new SqlConnection(_masterConnectionString)) { connection.Open(); @@ -106,91 +74,8 @@ namespace Umbraco.Tests.Integration.Testing } } - 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]); - } - } - - private void RebuildSchema(IDbCommand command, TestDbMeta meta) - { - 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 - { - _databaseFactory.Configure(meta.ConnectionString, Constants.DatabaseProviders.SqlServer); - - using (var database = (UmbracoDatabase)_databaseFactory.CreateDatabase()) - { - database.LogCommands = true; - - using (var transaction = database.GetTransaction()) - { - var schemaCreator = new DatabaseSchemaCreator(database, _loggerFactory.CreateLogger(), _loggerFactory, new UmbracoVersion()); - schemaCreator.InitializeDatabaseSchema(); - - transaction.Complete(); - - _cachedDatabaseInitCommands = database.Commands.ToArray(); - } - } - } - } - - 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(); @@ -209,46 +94,6 @@ namespace Umbraco.Tests.Integration.Testing } } - 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) @@ -263,25 +108,10 @@ namespace Umbraco.Tests.Integration.Testing _readySchemaQueue.CompleteAdding(); while (_readySchemaQueue.TryTake(out _)) { } - foreach (var testDatabase in _testDatabases.Values) + foreach (var testDatabase in _testDatabases) { 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/TestDatabaseFactory.cs b/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs index 3ed2b6b49a..9bcbfa4d3a 100644 --- a/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs +++ b/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs @@ -1,5 +1,4 @@ -using System; -using System.Runtime.InteropServices; +using System; using Microsoft.Extensions.Logging; using Umbraco.Core.Persistence; @@ -9,7 +8,7 @@ namespace Umbraco.Tests.Integration.Testing { public static ITestDatabase Create(string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) { - return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + return string.IsNullOrEmpty(Environment.GetEnvironmentVariable("UmbracoIntegrationTestConnectionString")) ? CreateLocalDb(filesPath, loggerFactory, dbFactory.Create()) : CreateSqlDeveloper(loggerFactory, dbFactory.Create()); } diff --git a/src/Umbraco.Tests.Integration/Testing/TestDbMeta.cs b/src/Umbraco.Tests.Integration/Testing/TestDbMeta.cs new file mode 100644 index 0000000000..83702db8e5 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/TestDbMeta.cs @@ -0,0 +1,39 @@ +using System.Text.RegularExpressions; + +namespace Umbraco.Tests.Integration.Testing +{ + public class TestDbMeta + { + + public string Name { get; } + + public bool IsEmpty { get; } + + public string ConnectionString { get; set; } + + private TestDbMeta(string name, bool isEmpty, string connectionString) + { + IsEmpty = isEmpty; + Name = name; + ConnectionString = connectionString; + } + + private static string ConstructConnectionString(string masterConnectionString, string databaseName) + { + var prefix = Regex.Replace(masterConnectionString, "Database=.+?;", string.Empty); + var connectionString = $"{prefix};Database={databaseName};"; + return connectionString.Replace(";;", ";"); + } + + public static TestDbMeta CreateWithMasterConnectionString(string name, bool isEmpty, string masterConnectionString) + { + return new TestDbMeta(name, isEmpty, ConstructConnectionString(masterConnectionString, name)); + } + + // LocalDb mdf funtimes + public static TestDbMeta CreateWithoutConnectionString(string name, bool isEmpty) + { + return new TestDbMeta(name, isEmpty, null); + } + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 3da4ec94d6..43b2d236c7 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -244,6 +244,7 @@ namespace Umbraco.Tests.Integration.Testing private static readonly object _dbLocker = new object(); private static ITestDatabase _dbInstance; + private static TestDbMeta _fixtureDbMeta; protected void UseTestLocalDb(IServiceProvider serviceProvider) { @@ -253,8 +254,6 @@ namespace Umbraco.Tests.Integration.Testing // This will create a db, install the schema and ensure the app is configured to run InstallTestLocalDb(testDatabaseFactoryProvider, databaseFactory, serviceProvider.GetRequiredService(), state, TestHelper.WorkingDirectory); - TestDBConnectionString = databaseFactory.ConnectionString; - InMemoryConfiguration["ConnectionStrings:" + Constants.System.UmbracoConnectionName] = TestDBConnectionString; } /// @@ -312,15 +311,15 @@ namespace Umbraco.Tests.Integration.Testing case UmbracoTestOptions.Database.NewSchemaPerTest: // New DB + Schema - var newSchemaDbId = db.AttachSchema(); + var newSchemaDbMeta = db.AttachSchema(); // Add teardown callback - OnTestTearDown(() => db.Detach(newSchemaDbId)); + OnTestTearDown(() => db.Detach(newSchemaDbMeta)); // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings if (!databaseFactory.Configured) { - databaseFactory.Configure(db.ConnectionString, Constants.DatabaseProviders.SqlServer); + databaseFactory.Configure(newSchemaDbMeta.ConnectionString, Constants.DatabaseProviders.SqlServer); } // re-run the runtime level check @@ -330,15 +329,15 @@ namespace Umbraco.Tests.Integration.Testing break; case UmbracoTestOptions.Database.NewEmptyPerTest: - var newEmptyDbId = db.AttachEmpty(); + var newEmptyDbMeta = db.AttachEmpty(); // Add teardown callback - OnTestTearDown(() => db.Detach(newEmptyDbId)); + OnTestTearDown(() => db.Detach(newEmptyDbMeta)); // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings if (!databaseFactory.Configured) { - databaseFactory.Configure(db.ConnectionString, Constants.DatabaseProviders.SqlServer); + databaseFactory.Configure(newEmptyDbMeta.ConnectionString, Constants.DatabaseProviders.SqlServer); } // re-run the runtime level check @@ -354,16 +353,17 @@ namespace Umbraco.Tests.Integration.Testing if (FirstTestInFixture) { // New DB + Schema - var newSchemaFixtureDbId = db.AttachSchema(); + var newSchemaFixtureDbMeta = db.AttachSchema(); + _fixtureDbMeta = newSchemaFixtureDbMeta; // Add teardown callback - OnFixtureTearDown(() => db.Detach(newSchemaFixtureDbId)); + OnFixtureTearDown(() => db.Detach(newSchemaFixtureDbMeta)); } // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings if (!databaseFactory.Configured) { - databaseFactory.Configure(db.ConnectionString, Constants.DatabaseProviders.SqlServer); + databaseFactory.Configure(_fixtureDbMeta.ConnectionString, Constants.DatabaseProviders.SqlServer); } // re-run the runtime level check @@ -377,16 +377,17 @@ namespace Umbraco.Tests.Integration.Testing if (FirstTestInFixture) { // New DB + Schema - var newEmptyFixtureDbId = db.AttachEmpty(); + var newEmptyFixtureDbMeta = db.AttachEmpty(); + _fixtureDbMeta = newEmptyFixtureDbMeta; // Add teardown callback - OnFixtureTearDown(() => db.Detach(newEmptyFixtureDbId)); + OnFixtureTearDown(() => db.Detach(newEmptyFixtureDbMeta)); } // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings if (!databaseFactory.Configured) { - databaseFactory.Configure(db.ConnectionString, Constants.DatabaseProviders.SqlServer); + databaseFactory.Configure(_fixtureDbMeta.ConnectionString, Constants.DatabaseProviders.SqlServer); } break; @@ -407,8 +408,6 @@ namespace Umbraco.Tests.Integration.Testing public TestHelper TestHelper = new TestHelper(); - protected virtual string TestDBConnectionString { get; private set; } - protected virtual Action CustomTestSetup => services => { }; /// diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index faf387528d..8c7b9a00e2 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Configuration; using System.Data.SqlServerCe; using System.Threading; @@ -91,7 +91,6 @@ namespace Umbraco.Tests.TestHelpers var lazyMappers = new Lazy(f.GetRequiredService); var factory = new UmbracoDatabaseFactory(f.GetRequiredService>(), f.GetRequiredService(), GetDbConnectionString(), GetDbProviderName(), lazyMappers, TestHelper.DbProviderFactoryCreator); - factory.ResetForTests(); return factory; }); } From cb84a98b9cf5891e0ff94e4e3441af976da00d48 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Sat, 12 Dec 2020 13:15:08 +0000 Subject: [PATCH 7/7] Fix indent for MacOS Don't split build and test on Windows. --- build/azure-pipelines.yml | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index d301b9c461..c545d6884e 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -66,26 +66,26 @@ stages: env: UmbracoIntegrationTestConnectionString: 'Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD);' -- stage: macOS_X +- stage: MacOS dependsOn: [] # this removes the implicit dependency on previous stage and causes this to run in parallel jobs: - - job: Unit_Tests - displayName: 'Unit Tests' - pool: - vmImage: 'macOS-latest' - steps: + - job: Unit_Tests + displayName: 'Unit Tests' + pool: + vmImage: 'macOS-latest' + steps: - - task: UseDotNet@2 - displayName: 'Use .Net Core sdk 3.1.x' - inputs: - version: 3.1.x + - task: UseDotNet@2 + displayName: 'Use .Net Core sdk 3.1.x' + inputs: + version: 3.1.x - - task: DotNetCoreCLI@2 - displayName: 'dotnet test' - inputs: - command: test - projects: '**/*.Tests.UnitTests.csproj' + - task: DotNetCoreCLI@2 + displayName: 'dotnet test' + inputs: + command: test + projects: '**/*.Tests.UnitTests.csproj' - stage: Windows dependsOn: [] # this removes the implicit dependency on previous stage and causes this to run in parallel @@ -120,11 +120,6 @@ stages: inputs: version: 3.1.x - - task: DotNetCoreCLI@2 - displayName: 'dotnet build' - inputs: - projects: '**\Umbraco.Tests.Integration.csproj' - - powershell: 'sqllocaldb start mssqllocaldb' displayName: 'Start MSSQL LocalDb' @@ -133,7 +128,6 @@ stages: inputs: command: test projects: '**\Umbraco.Tests.Integration.csproj' - arguments: '--no-build' - job: Build_Artifacts displayName: 'Build Artifacts'