diff --git a/global.json b/global.json index 3d635576..cab64861 100644 --- a/global.json +++ b/global.json @@ -1,11 +1,11 @@ { "sdk": { - "version": "8.0.100", + "version": "8.0.200", "allowPrerelease": false, "rollForward": "major" }, "tools": { - "dotnet": "8.0.100", + "dotnet": "8.0.200", "runtimes": { "aspnetcore": [ "6.0.25", diff --git a/grate.sln b/grate.sln index d00c30ff..bc6ce4d7 100644 --- a/grate.sln +++ b/grate.sln @@ -60,6 +60,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine.PostgreSQL", "u EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine.SqlServer", "unittests\CommandLine\CommandLine.SqlServer\CommandLine.SqlServer.csproj", "{3CD81B8C-9348-4F45-9E50-12713D47B1FB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker tests", "Docker tests", "{1F3AC4DA-B220-4DD7-ACD4-B309B60A5910}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.SqlServer", "unittests\Docker\Docker.SqlServer\Docker.SqlServer.csproj", "{26A2069A-ECE7-46A3-8CDC-8D864062B1E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.MariaDB", "unittests\Docker\Docker.MariaDB\Docker.MariaDB.csproj", "{9134186E-F1A8-4582-BF61-7DDC79D7BF2E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.Common", "unittests\Docker\Docker.Common\Docker.Common.csproj", "{E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.Sqlite", "unittests\Docker\Docker.Sqlite\Docker.Sqlite.csproj", "{73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.Oracle", "unittests\Docker\Docker.Oracle\Docker.Oracle.csproj", "{11D21F9B-1819-4C5D-A032-E556520FF607}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.PostgreSQL", "unittests\Docker\Docker.PostgreSQL\Docker.PostgreSQL.csproj", "{542958B4-117C-4E00-A555-00B9630A7066}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -322,6 +336,78 @@ Global {3CD81B8C-9348-4F45-9E50-12713D47B1FB}.Release|x64.Build.0 = Release|Any CPU {3CD81B8C-9348-4F45-9E50-12713D47B1FB}.Release|x86.ActiveCfg = Release|Any CPU {3CD81B8C-9348-4F45-9E50-12713D47B1FB}.Release|x86.Build.0 = Release|Any CPU + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9}.Debug|x64.ActiveCfg = Debug|Any CPU + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9}.Debug|x64.Build.0 = Debug|Any CPU + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9}.Debug|x86.ActiveCfg = Debug|Any CPU + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9}.Debug|x86.Build.0 = Debug|Any CPU + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9}.Release|Any CPU.Build.0 = Release|Any CPU + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9}.Release|x64.ActiveCfg = Release|Any CPU + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9}.Release|x64.Build.0 = Release|Any CPU + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9}.Release|x86.ActiveCfg = Release|Any CPU + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9}.Release|x86.Build.0 = Release|Any CPU + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E}.Debug|x64.ActiveCfg = Debug|Any CPU + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E}.Debug|x64.Build.0 = Debug|Any CPU + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E}.Debug|x86.ActiveCfg = Debug|Any CPU + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E}.Debug|x86.Build.0 = Debug|Any CPU + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E}.Release|Any CPU.Build.0 = Release|Any CPU + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E}.Release|x64.ActiveCfg = Release|Any CPU + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E}.Release|x64.Build.0 = Release|Any CPU + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E}.Release|x86.ActiveCfg = Release|Any CPU + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E}.Release|x86.Build.0 = Release|Any CPU + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}.Debug|x64.ActiveCfg = Debug|Any CPU + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}.Debug|x64.Build.0 = Debug|Any CPU + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}.Debug|x86.ActiveCfg = Debug|Any CPU + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}.Debug|x86.Build.0 = Debug|Any CPU + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}.Release|Any CPU.Build.0 = Release|Any CPU + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}.Release|x64.ActiveCfg = Release|Any CPU + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}.Release|x64.Build.0 = Release|Any CPU + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}.Release|x86.ActiveCfg = Release|Any CPU + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2}.Release|x86.Build.0 = Release|Any CPU + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}.Debug|x64.ActiveCfg = Debug|Any CPU + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}.Debug|x64.Build.0 = Debug|Any CPU + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}.Debug|x86.ActiveCfg = Debug|Any CPU + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}.Debug|x86.Build.0 = Debug|Any CPU + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}.Release|Any CPU.Build.0 = Release|Any CPU + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}.Release|x64.ActiveCfg = Release|Any CPU + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}.Release|x64.Build.0 = Release|Any CPU + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}.Release|x86.ActiveCfg = Release|Any CPU + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10}.Release|x86.Build.0 = Release|Any CPU + {11D21F9B-1819-4C5D-A032-E556520FF607}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11D21F9B-1819-4C5D-A032-E556520FF607}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11D21F9B-1819-4C5D-A032-E556520FF607}.Debug|x64.ActiveCfg = Debug|Any CPU + {11D21F9B-1819-4C5D-A032-E556520FF607}.Debug|x64.Build.0 = Debug|Any CPU + {11D21F9B-1819-4C5D-A032-E556520FF607}.Debug|x86.ActiveCfg = Debug|Any CPU + {11D21F9B-1819-4C5D-A032-E556520FF607}.Debug|x86.Build.0 = Debug|Any CPU + {11D21F9B-1819-4C5D-A032-E556520FF607}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11D21F9B-1819-4C5D-A032-E556520FF607}.Release|Any CPU.Build.0 = Release|Any CPU + {11D21F9B-1819-4C5D-A032-E556520FF607}.Release|x64.ActiveCfg = Release|Any CPU + {11D21F9B-1819-4C5D-A032-E556520FF607}.Release|x64.Build.0 = Release|Any CPU + {11D21F9B-1819-4C5D-A032-E556520FF607}.Release|x86.ActiveCfg = Release|Any CPU + {11D21F9B-1819-4C5D-A032-E556520FF607}.Release|x86.Build.0 = Release|Any CPU + {542958B4-117C-4E00-A555-00B9630A7066}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {542958B4-117C-4E00-A555-00B9630A7066}.Debug|Any CPU.Build.0 = Debug|Any CPU + {542958B4-117C-4E00-A555-00B9630A7066}.Debug|x64.ActiveCfg = Debug|Any CPU + {542958B4-117C-4E00-A555-00B9630A7066}.Debug|x64.Build.0 = Debug|Any CPU + {542958B4-117C-4E00-A555-00B9630A7066}.Debug|x86.ActiveCfg = Debug|Any CPU + {542958B4-117C-4E00-A555-00B9630A7066}.Debug|x86.Build.0 = Debug|Any CPU + {542958B4-117C-4E00-A555-00B9630A7066}.Release|Any CPU.ActiveCfg = Release|Any CPU + {542958B4-117C-4E00-A555-00B9630A7066}.Release|Any CPU.Build.0 = Release|Any CPU + {542958B4-117C-4E00-A555-00B9630A7066}.Release|x64.ActiveCfg = Release|Any CPU + {542958B4-117C-4E00-A555-00B9630A7066}.Release|x64.Build.0 = Release|Any CPU + {542958B4-117C-4E00-A555-00B9630A7066}.Release|x86.ActiveCfg = Release|Any CPU + {542958B4-117C-4E00-A555-00B9630A7066}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -352,5 +438,12 @@ Global {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8} = {04731BDA-05F0-4275-AC70-3E60E87F9461} {89CBE842-BD34-4833-96B4-9E410C5C6AD7} = {04731BDA-05F0-4275-AC70-3E60E87F9461} {3CD81B8C-9348-4F45-9E50-12713D47B1FB} = {04731BDA-05F0-4275-AC70-3E60E87F9461} + {1F3AC4DA-B220-4DD7-ACD4-B309B60A5910} = {CE4BF96D-E795-49EC-BB36-EDCC7E542685} + {26A2069A-ECE7-46A3-8CDC-8D864062B1E9} = {1F3AC4DA-B220-4DD7-ACD4-B309B60A5910} + {9134186E-F1A8-4582-BF61-7DDC79D7BF2E} = {1F3AC4DA-B220-4DD7-ACD4-B309B60A5910} + {E16F876A-A2A0-46FB-9FEA-C0C64DAB89D2} = {1F3AC4DA-B220-4DD7-ACD4-B309B60A5910} + {73EBC3A0-E8DC-4AC1-8788-FE7DB3F53C10} = {1F3AC4DA-B220-4DD7-ACD4-B309B60A5910} + {11D21F9B-1819-4C5D-A032-E556520FF607} = {1F3AC4DA-B220-4DD7-ACD4-B309B60A5910} + {542958B4-117C-4E00-A555-00B9630A7066} = {1F3AC4DA-B220-4DD7-ACD4-B309B60A5910} EndGlobalSection EndGlobal diff --git a/installers/docker/Dockerfile b/installers/docker/Dockerfile index e0cff4d5..7978d467 100644 --- a/installers/docker/Dockerfile +++ b/installers/docker/Dockerfile @@ -1,9 +1,15 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build + +ARG os=linux-musl +ARG arch=amd64 +ENV OS $os +ENV ARCH $arch + WORKDIR . COPY *.props ./ COPY *.ruleset ./ COPY src/ ./src/ -RUN dotnet publish ./src/grate/grate.csproj -r linux-musl-x64 -c release --self-contained -p:SelfContained=true -o ./publish/app +RUN dotnet publish ./src/grate/grate.csproj --os $OS --arch $ARCH -c release --self-contained -p:SelfContained=true -o ./publish/app FROM alpine:3 AS base WORKDIR /app @@ -16,12 +22,12 @@ ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false # Add the grate executable ENTRYPOINT ./grate \ - --sqlfilesdirectory=/db \ - --version=${VERSION:-1.0.0} \ - --connstring="$APP_CONNSTRING" \ - --databasetype=${DATABASE_TYPE:-sqlserver} \ - --silent \ - --outputPath=/output \ - --createdatabase=${CREATE_DATABASE:-true} \ - --environment=${ENVIRONMENT:-LOCAL} \ - --transaction=${TRANSACTION:-false} +--sqlfilesdirectory=/db \ +--version=${VERSION:-1.0.0} \ +--connstring="$APP_CONNSTRING" \ +--databasetype=${DATABASE_TYPE:-sqlserver} \ +--silent \ +--outputPath=/output \ +--createdatabase=${CREATE_DATABASE:-true} \ +--environment=${ENVIRONMENT:-LOCAL} \ +--transaction=${TRANSACTION:-false} diff --git a/src/grate.core/Configuration/FoldersCommand.cs b/src/grate.core/Configuration/FoldersCommand.cs index a18a7cfc..807323ee 100644 --- a/src/grate.core/Configuration/FoldersCommand.cs +++ b/src/grate.core/Configuration/FoldersCommand.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using grate.Exceptions; namespace grate.Configuration; @@ -135,6 +136,7 @@ private static void ApplyConfig(MigrationsFolder folder, string folderConfig) } } + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] static readonly Type FolderType = typeof(MigrationsFolder); private static (MethodInfo? setter, Type? propertyType) GetProperty(string propertyName) diff --git a/src/grate.core/DependencyInjection/RegistrationExtensions.cs b/src/grate.core/DependencyInjection/RegistrationExtensions.cs index 4b98c772..02360882 100644 --- a/src/grate.core/DependencyInjection/RegistrationExtensions.cs +++ b/src/grate.core/DependencyInjection/RegistrationExtensions.cs @@ -1,4 +1,5 @@ -using grate.Configuration; +using System.Diagnostics.CodeAnalysis; +using grate.Configuration; using grate.Infrastructure; using grate.Migration; using Microsoft.Extensions.DependencyInjection; @@ -36,7 +37,7 @@ public static IServiceCollection AddGrate(this IServiceCollection serviceCollect return AddGrateService(serviceCollection); } - public static IServiceCollection AddGrate( + public static IServiceCollection AddGrate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TDatabase>( this IServiceCollection serviceCollection, GrateConfiguration presetGrateConfiguration, Action? builder = null) where TDatabase : class, IDatabase { @@ -45,7 +46,7 @@ public static IServiceCollection AddGrate( .UseDatabase(); } - public static IServiceCollection AddGrate( + public static IServiceCollection AddGrate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TDatabase>( this IServiceCollection serviceCollection, Action? builder = null) where TDatabase : class, IDatabase { @@ -54,14 +55,14 @@ public static IServiceCollection AddGrate( .UseDatabase(); } - public static IServiceCollection UseDatabase(this IServiceCollection serviceCollection) + public static IServiceCollection UseDatabase<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TDatabase>(this IServiceCollection serviceCollection) where TDatabase : class, IDatabase { serviceCollection.TryAddTransient(); return serviceCollection; } - public static IServiceCollection UseDatabase(this IServiceCollection serviceCollection, Type databaseType) + public static IServiceCollection UseDatabase(this IServiceCollection serviceCollection, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type databaseType) { serviceCollection.TryAddTransient(typeof(IDatabase), databaseType); return serviceCollection; diff --git a/src/grate.core/Migration/AnsiSqlDatabase.cs b/src/grate.core/Migration/AnsiSqlDatabase.cs index a547eb09..bf4caf0a 100644 --- a/src/grate.core/Migration/AnsiSqlDatabase.cs +++ b/src/grate.core/Migration/AnsiSqlDatabase.cs @@ -468,7 +468,7 @@ private async Task> GetAllScriptsRun() } catch (Exception ex) when (DryRunOrBootstrap()) { - Logger.LogDebug(ex, "Ignoring error getting ScriptsRun when in --dryrun, probable missing table"); + Logger.LogDebug(ex, "Ignoring error getting ScriptsRun when in --dryrun, probably missing table"); return new Dictionary(); // return empty set if nothing has ever been run } } diff --git a/src/grate.core/Migration/GrateMigrator.cs b/src/grate.core/Migration/GrateMigrator.cs index 5fca449e..5005ada8 100644 --- a/src/grate.core/Migration/GrateMigrator.cs +++ b/src/grate.core/Migration/GrateMigrator.cs @@ -80,9 +80,10 @@ public async Task Migrate() var runInTransaction = MakeSureWeCanRunInTransaction(config.Transaction, silent, dbMigrator); var changeDropFolder = ChangeDropFolder(config, database.ServerName, database.DatabaseName); + _logger.LogDebug("The change_drop (output) folder is: {ChangeDropFolder}", changeDropFolder); + CreateChangeDropFolder(changeDropFolder); - _logger.LogDebug("The change_drop (output) folder is: {ChangeDropFolder}", changeDropFolder); Separator('='); _logger.LogInformation("Setup, Backup, Create/Restore/Drop"); @@ -573,19 +574,28 @@ private async Task RunInternalMigrations(string internalFolderName) } } - private async Task GetBootstrapInternalGrateConfiguration(string internalFolderName) => - await GetInternalGrateConfiguration(internalFolderName, "grate-internal") with + private async Task GetBootstrapInternalGrateConfiguration(string internalFolderName) + { + var bootstrapInternalGrateConfiguration = await GetInternalGrateConfiguration(internalFolderName, "grate-internal") with { - UserTokens = [ + UserTokens = + [ "ScriptsRunTable=GrateScriptsRun", "ScriptsRunErrorsTable=GrateScriptsRunErrors", "VersionTable=GrateVersion" ], DeferWritingToRunTables = true, Environment = GrateEnvironment.InternalBootstrap, - Baseline = false + Baseline = false, }; - + bootstrapInternalGrateConfiguration = bootstrapInternalGrateConfiguration with + { + OutputPath = bootstrapInternalGrateConfiguration.OutputPath.Parent!.CreateSubdirectory("grate-internal-bootstrap") + }; + + return bootstrapInternalGrateConfiguration; + } + private async Task GetInternalGrateConfiguration(string internalFolderName, string? sqlFolderNamePrefix = null) { var thisConfig = this.Configuration; @@ -606,11 +616,16 @@ private async Task GetInternalGrateConfiguration(string inte AccessToken = thisConfig.AccessToken, CommandTimeout = thisConfig.CommandTimeout, AdminCommandTimeout = thisConfig.AdminCommandTimeout, + Verbosity = LogLevel.Critical, + OutputPath = thisConfig.OutputPath.CreateSubdirectory("grate-internal"), Baseline = baseline, NonInteractive = true, SqlFilesDirectory = new DirectoryInfo(internalMigrationFolders), CreateDatabase = false, + AlterDatabase = false, + DryRun = false, + Drop = false, Restore = null, Transaction = false, diff --git a/src/grate.core/grate.core.csproj b/src/grate.core/grate.core.csproj index 270d1000..5d474ff0 100644 --- a/src/grate.core/grate.core.csproj +++ b/src/grate.core/grate.core.csproj @@ -5,6 +5,7 @@ enable grate build$([System.DateTime]::UtcNow.ToString("O")) + @@ -17,11 +18,19 @@ + + + + + + + + diff --git a/src/grate/Commands/MigrateCommand.cs b/src/grate/Commands/MigrateCommand.cs index dd5b4e86..21839c85 100644 --- a/src/grate/Commands/MigrateCommand.cs +++ b/src/grate/Commands/MigrateCommand.cs @@ -79,7 +79,7 @@ private static Option OutputPath() => new[] { "--output", "-o", "--outputPath" }, () => new DirectoryInfo(DefaultOutputPath), "This is where everything related to the migration is stored. This includes any backups, all items that ran, permission dumps, logs, etc." - ).ExistingOnly(); + ); private static Option Folders() => new Option( diff --git a/src/grate/Program.cs b/src/grate/Program.cs index fb7f7190..36974ea3 100644 --- a/src/grate/Program.cs +++ b/src/grate/Program.cs @@ -100,10 +100,17 @@ private static async Task WaitForLoggerToFinish() var delay = 100; await Task.Delay(1); - while (ThreadPool.PendingWorkItemCount > 0 && waitedTime < maxWaitTime) + try { - await Task.Delay(delay); - waitedTime += delay; + while (ThreadPool.PendingWorkItemCount > 0 && waitedTime < maxWaitTime) + { + await Task.Delay(delay); + waitedTime += delay; + } + } + catch (Exception) + { + // We don't want to fail on exit. Nevermind, just exit, and get on with it. } } @@ -136,7 +143,7 @@ private static ServiceProvider BuildServiceProvider(CommandLineGrateConfiguratio return services.BuildServiceProvider(); } - private static Option Verbosity() => new( + internal static Option Verbosity() => new( new[] { "-v", "--verbosity" }, "Verbosity level (as defined here: https://docs.microsoft.com/dotnet/api/Microsoft.Extensions.Logging.LogLevel)"); diff --git a/src/grate/grate.csproj b/src/grate/grate.csproj index ca42d5c9..9d4c834d 100644 --- a/src/grate/grate.csproj +++ b/src/grate/grate.csproj @@ -5,8 +5,53 @@ net8.0 Embedded enable + + true + true + false + + + None + jammy-chiseled-extra + + grate + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -15,6 +60,13 @@ + + + + + + + @@ -36,8 +88,6 @@ None - - win-x64;win-x86;win-arm64;linux-musl-x64;linux-musl-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64 grate diff --git a/unittests/Basic_tests/CommandLineParsing/Basic_CommandLineParsing.cs b/unittests/Basic_tests/CommandLineParsing/Basic_CommandLineParsing.cs index 809cf8ad..28631374 100644 --- a/unittests/Basic_tests/CommandLineParsing/Basic_CommandLineParsing.cs +++ b/unittests/Basic_tests/CommandLineParsing/Basic_CommandLineParsing.cs @@ -2,9 +2,11 @@ using System.CommandLine.Invocation; using System.CommandLine.NamingConventionBinder; using System.CommandLine.Parsing; +using System.Configuration; using FluentAssertions; using grate.Commands; using grate.Configuration; +using grate.Exceptions; using grate.Infrastructure; namespace Basic_tests.CommandLineParsing; @@ -69,11 +71,11 @@ public async Task AdminConnectionString(string argName) [InlineData("--sqlfilesdirectory=")] public async Task SqlFilesDirectory(string argName) { - var database = "C:\\tmp"; - var commandline = argName + database; + var folder = Directory.CreateTempSubdirectory(); + var commandline = argName + folder; var cfg = await ParseGrateConfiguration(commandline); - cfg?.SqlFilesDirectory.ToString().Should().Be(database); + cfg?.SqlFilesDirectory.ToString().Should().Be(folder.ToString()); } [Theory] @@ -84,11 +86,14 @@ public async Task SqlFilesDirectory(string argName) [InlineData("--outputPath ")] public async Task OutputPath(string argName) { - var database = "C:\\tmp"; - var commandline = argName + database; - var cfg = await ParseGrateConfiguration(commandline); + var nonExistingDirectory = Path.Join(Path.GetTempPath() + Guid.NewGuid()); + var commandline = argName + nonExistingDirectory; + + CommandLineGrateConfiguration? cfg = null; + var ex = await Record.ExceptionAsync(async () => cfg = await ParseGrateConfiguration(commandline)); - cfg?.OutputPath.ToString().Should().Be(database); + ex.Should().BeNull(); + cfg?.OutputPath.ToString().Should().Be(nonExistingDirectory); } [Theory] @@ -376,11 +381,28 @@ public async Task IgnoreDirectoryNames(string args, bool expected) private static async Task ParseGrateConfiguration(string commandline) { + // All parsing fails if the connectionstring is not supplied, so we need to add it here, if it's not in the commandline. + if ( + (!commandline.Contains("--connectionstring=")) && + (!commandline.Contains("--connstring=")) && + (!commandline.Contains("-cs ")) && + (!commandline.Contains("-c "))) + { + commandline += " -c \"Server=.;Database=master;Trusted_Connection=True;\""; + } + CommandLineGrateConfiguration? cfg = null; var cmd = CommandHandler.Create((CommandLineGrateConfiguration config) => cfg = config); ParseResult p = new Parser(new MigrateCommand(null!)).Parse(commandline); + + if (p.Errors.Any()) + { + var exceptions = p.Errors.Select(error => new ConfigurationErrorsException(error.Message)).ToList(); + throw new MigrationFailed(exceptions); + } + await cmd.InvokeAsync(new InvocationContext(p)); return cfg; } diff --git a/unittests/Docker/Docker.Common/Docker.Common.csproj b/unittests/Docker/Docker.Common/Docker.Common.csproj new file mode 100644 index 00000000..ceb8fca8 --- /dev/null +++ b/unittests/Docker/Docker.Common/Docker.Common.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + + false + false + + Docker.Common.Startup + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/unittests/Docker/Docker.Common/Startup_T.cs b/unittests/Docker/Docker.Common/Startup_T.cs new file mode 100644 index 00000000..f91bf11e --- /dev/null +++ b/unittests/Docker/Docker.Common/Startup_T.cs @@ -0,0 +1,36 @@ +using Docker.Common.TestInfrastructure; +using DotNet.Testcontainers.Networks; +using grate.Configuration; +using grate.Migration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using TestCommon.TestInfrastructure; + +namespace Docker.Common; + +// ReSharper disable once UnusedType.Global +public abstract class Startup< + TTestContainerDatabase, + TExternalDatabase, + TGrateTestContext>: TestCommon.Startup + where TTestContainerDatabase : ITestDatabase + where TExternalDatabase : ITestDatabase + where TGrateTestContext : IGrateTestContext +{ + protected abstract DatabaseType DatabaseType { get; } + + protected override void ConfigureExtraServices(IServiceCollection services, HostBuilderContext context) + { + services.AddSingleton(provider => + new DockerGrateMigrator( + DatabaseType, + provider.GetRequiredService>(), + provider.GetRequiredService() + ) + ); + } + + protected override GrateTestConfig ExtraConfiguration(GrateTestConfig config) + => config with {ConnectToDockerInternal = true}; +} diff --git a/unittests/Docker/Docker.Common/TestInfrastructure/DockerGrateMigrator.cs b/unittests/Docker/Docker.Common/TestInfrastructure/DockerGrateMigrator.cs new file mode 100644 index 00000000..59a83b67 --- /dev/null +++ b/unittests/Docker/Docker.Common/TestInfrastructure/DockerGrateMigrator.cs @@ -0,0 +1,194 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Networks; +using grate; +using grate.Commands; +using grate.Configuration; +using grate.Exceptions; +using grate.Migration; +using Microsoft.Extensions.Logging; + +namespace Docker.Common.TestInfrastructure; + +public record DockerGrateMigrator( + DatabaseType DatabaseType, + ILogger Logger, + INetwork Network + ) + : IGrateMigrator +{ + public DatabaseType DatabaseType { get; set; } = DatabaseType; + + private static string DockerImage => "erikbra/grate"; + //private static string DockerImage => "grate"; + + public async Task Migrate() + { + // Convert configuration to command-line arguments + var convertToDockerArguments = ConvertToDockerArguments(Configuration); + var dockerArguments = convertToDockerArguments.ToList(); + + // Add the database type + dockerArguments.Add("--databasetype=" + DatabaseType.ToString().ToLowerInvariant()); + + // Needed when overriding the entrypoint, not the command + dockerArguments.Insert(0, "./grate"); + + // Need to map the SQL files directory to the container + var sqlFilesDirectory = Configuration.SqlFilesDirectory.ToString(); + + var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + var token = cancellationTokenSource.Token; + + await using var container = new ContainerBuilder() + .WithImage(DockerImage) + .WithEnvironment("DOTNET_BUNDLE_EXTRACT_BASE_DIR", "/tmp/dotnet-bundle-extract") + .WithEntrypoint(dockerArguments.ToArray()) + // This is dependent on changing the image to only use grate as ENTRYPOINT, and the rest as CMD + //.WithCommand(dockerArguments.ToArray()) + .WithBindMount(sqlFilesDirectory, sqlFilesDirectory) + .WithCreateParameterModifier(param => param.HostConfig.ReadonlyRootfs = true) + .WithTmpfsMount("/tmp") + .WithNetwork(Network) + .WithLogger(Logger) + .Build(); + + try + { + try + { + await container.StartAsync(token); + } + catch (Exception e) + { + throw new MigrationFailed(e); + } + finally + { + var ct = new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token; + long? exitCode = null; + try + { + exitCode = await container.GetExitCodeAsync(ct); + } + catch (Exception e) + { + throw new MigrationFailed(e); + } + finally + { + ct = new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token; + var (output, error) = await container.GetLogsAsync(ct: ct); + if (exitCode != 0) + { + throw new Exception($"grate failed with exit code {exitCode}.\nOutput:\n{output}\nError:\n{error}"); + } + } + } + } + // Try to just ignore if it times out, and see. If the assertions succeed afterwards, we really don't care + // that much. + //catch (TimeoutException) {} + //catch (OperationCanceledException) {} + catch (Exception e) + { + throw new MigrationFailed(e); + } + } + + public GrateConfiguration Configuration { get; private init; } = null!; + + public IGrateMigrator WithConfiguration(GrateConfiguration configuration) + { + return this with + { + Configuration = configuration with + { + // Need to overwrite the output path, as we don't have the same tmp folders on the host as in the container, + // and the root file system is read-only in the test container + OutputPath = new DirectoryInfo(Path.Combine("/tmp", "grate-tests-output", Directory.CreateTempSubdirectory().Name)), + Verbosity = LogLevel.Debug + } + }; + } + + public IGrateMigrator WithConfiguration(Action builder) + { + var b = GrateConfigurationBuilder.Create(Configuration); + builder.Invoke(b); + return this with { Configuration = b.Build() with + { + // Need to overwrite the output path, as we don't have the same tmp folders on the host as in the container, + // and the root file system is read-only in the test container + OutputPath = new DirectoryInfo(Path.Combine("/tmp", "grate-tests-output", Directory.CreateTempSubdirectory().Name)) + }}; + } + + public IGrateMigrator WithDatabase(IDatabase database) => this with { Database = database }; + public IDatabase? Database { get; set; } + + + private List ConvertToDockerArguments(GrateConfiguration configuration) + { + List result = new(); + + var properties = configuration.GetType().GetProperties(); + + var cmd = new MigrateCommand(this); + cmd.Add(Program.Verbosity()); + + foreach (var prop in properties) + { + // Skip properties with default values + var value = prop.GetValue(configuration); + var defaultValue = prop.GetValue(GrateConfiguration.Default); + + var serializedValue = JsonSerializer.Serialize(value?.ToString(), SerializerOptions); + var serializedDefault = JsonSerializer.Serialize(defaultValue?.ToString(), SerializerOptions); + + if (serializedValue.Equals(serializedDefault)) + { + continue; + } + + // Convert IFoldersConfiguration to string + if (value is IFoldersConfiguration foldersConfiguration) + { + value = string.Join(';', foldersConfiguration.Values); + } + + var name = prop.Name; + var option = cmd.Options.FirstOrDefault(o => string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); + + if (option is not null && value is not null) + { + var optionName = option.Aliases.FirstOrDefault(alias => alias.StartsWith("--")) + ?? option.Aliases.First(); + + if (value is string[] arr) + { + result.AddRange(arr.Select(v => $"{optionName}={v}")); + } + else + { + result.Add($"{optionName}={value}"); + } + } + } + + return result; +#pragma warning restore CS0162 // Unreachable code detected + } + + private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerOptions.Default) + { + ReferenceHandler = ReferenceHandler.IgnoreCycles + }; + + public ValueTask DisposeAsync() + { + return new ValueTask(Task.CompletedTask); + } + +} diff --git a/unittests/Docker/Docker.MariaDB/Docker.MariaDB.csproj b/unittests/Docker/Docker.MariaDB/Docker.MariaDB.csproj new file mode 100644 index 00000000..daf980d5 --- /dev/null +++ b/unittests/Docker/Docker.MariaDB/Docker.MariaDB.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + ../../MariaDB + + + + diff --git a/unittests/Docker/Docker.MariaDB/Running_MigrationScripts/Failing_Scripts.cs b/unittests/Docker/Docker.MariaDB/Running_MigrationScripts/Failing_Scripts.cs new file mode 100644 index 00000000..cce9a6bb --- /dev/null +++ b/unittests/Docker/Docker.MariaDB/Running_MigrationScripts/Failing_Scripts.cs @@ -0,0 +1,24 @@ +using MariaDB.TestInfrastructure; +using TestCommon.TestInfrastructure; + +namespace Docker.MariaDB.Running_MigrationScripts; + +[Collection(nameof(MariaDbGrateTestContext))] +// ReSharper disable once InconsistentNaming +public class Failing_Scripts(MariaDbGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.Failing_Scripts(testContext, testOutput) +{ + protected override string ExpectedStartOfErrorMessageForInvalidSql => "Not relevant"; + protected override IDictionary ExpectedErrorDetails => new Dictionary(); + + [Fact(Skip = "Cannot check on the exact error message when running the command line tool. The error message is not available.")] + public override Task Aborts_the_run_giving_an_error_message() => Task.CompletedTask; + + [Fact(Skip = "Cannot check on the exact error details when running the command line tool. The error details are not available.")] + public override Task Exception_includes_details_on_the_failed_script() => Task.CompletedTask; + + [Fact(Skip = + "We do not have the ability to check for transient errors when running the command line tool. The error details are not available.")] + public override Task Exception_is_set_to_transient_based_on_inner_exceptions() => Task.CompletedTask; + +} diff --git a/unittests/Docker/Docker.MariaDB/Running_MigrationScripts/One_time_scripts.cs b/unittests/Docker/Docker.MariaDB/Running_MigrationScripts/One_time_scripts.cs new file mode 100644 index 00000000..d00492d3 --- /dev/null +++ b/unittests/Docker/Docker.MariaDB/Running_MigrationScripts/One_time_scripts.cs @@ -0,0 +1,60 @@ +using Dapper; +using FluentAssertions; +using grate.Configuration; +using grate.Exceptions; +using MariaDB.TestInfrastructure; +using TestCommon.TestInfrastructure; +using static grate.Configuration.KnownFolderKeys; + +namespace Docker.MariaDB.Running_MigrationScripts; + +[Collection(nameof(MariaDbGrateTestContext))] +// ReSharper disable once InconsistentNaming +public class One_time_scripts(MariaDbGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.One_time_scripts(testContext, testOutput) +{ + + [Fact] + public override async Task Fails_if_changed_between_runs() + { + var db = TestConfig.RandomDatabase(); + + var parent = CreateRandomTempDirectory(); + var knownFolders = Folders.Default; + CreateDummySql(parent, knownFolders[Up]); + + var config = GrateConfigurationBuilder.Create(Context.DefaultConfiguration) + .WithConnectionString(Context.ConnectionString(db)) + .WithFolders(knownFolders) + .WithSqlFilesDirectory(parent) + .Build(); + + await using (var migrator = Context.Migrator.WithConfiguration(config)) + { + await migrator.Migrate(); + } + + WriteSomeOtherSql(parent, knownFolders[Up]); + + await using (var migrator = Context.Migrator.WithConfiguration(config)) + { + var ex = await Assert.ThrowsAsync(() => migrator.Migrate()); + } + + string[] scripts; + string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + + using (var conn = Context.External.CreateDbConnection(db)) + { + scripts = (await conn.QueryAsync(sql)).ToArray(); + } + + scripts.Should().HaveCount(1); + scripts.First().Should().Be(Context.Sql.SelectVersion); + + //await Context.DropDatabase(db); + } + +} + + diff --git a/unittests/Docker/Docker.MariaDB/Startup.cs b/unittests/Docker/Docker.MariaDB/Startup.cs new file mode 100644 index 00000000..289f9049 --- /dev/null +++ b/unittests/Docker/Docker.MariaDB/Startup.cs @@ -0,0 +1,12 @@ +using grate.Configuration; +using MariaDB.TestInfrastructure; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Docker.MariaDB; + +// ReSharper disable once UnusedType.Global +public class Startup: Docker.Common.Startup +{ + protected override DatabaseType DatabaseType => DatabaseType.MariaDB; +} diff --git a/unittests/Docker/Docker.Oracle/Docker.Oracle.csproj b/unittests/Docker/Docker.Oracle/Docker.Oracle.csproj new file mode 100644 index 00000000..e19627be --- /dev/null +++ b/unittests/Docker/Docker.Oracle/Docker.Oracle.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + ../../Oracle + + + + diff --git a/unittests/Docker/Docker.Oracle/Running_MigrationScripts/Failing_Scripts.cs b/unittests/Docker/Docker.Oracle/Running_MigrationScripts/Failing_Scripts.cs new file mode 100644 index 00000000..5098ad4d --- /dev/null +++ b/unittests/Docker/Docker.Oracle/Running_MigrationScripts/Failing_Scripts.cs @@ -0,0 +1,23 @@ +using Oracle.TestInfrastructure; + +namespace Docker.Oracle.Running_MigrationScripts; + +[Collection(nameof(OracleGrateTestContext))] +// ReSharper disable once InconsistentNaming +public class Failing_Scripts(OracleGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.Failing_Scripts(testContext, testOutput) +{ + protected override string ExpectedStartOfErrorMessageForInvalidSql => "Not relevant"; + protected override IDictionary ExpectedErrorDetails => new Dictionary(); + + [Fact(Skip = "Cannot check on the exact error message when running the command line tool. The error message is not available.")] + public override Task Aborts_the_run_giving_an_error_message() => Task.CompletedTask; + + [Fact(Skip = "Cannot check on the exact error details when running the command line tool. The error details are not available.")] + public override Task Exception_includes_details_on_the_failed_script() => Task.CompletedTask; + + [Fact(Skip = + "We do not have the ability to check for transient errors when running the command line tool. The error details are not available.")] + public override Task Exception_is_set_to_transient_based_on_inner_exceptions() => Task.CompletedTask; + +} diff --git a/unittests/Docker/Docker.Oracle/Running_MigrationScripts/One_time_scripts.cs b/unittests/Docker/Docker.Oracle/Running_MigrationScripts/One_time_scripts.cs new file mode 100644 index 00000000..5558f035 --- /dev/null +++ b/unittests/Docker/Docker.Oracle/Running_MigrationScripts/One_time_scripts.cs @@ -0,0 +1,62 @@ +using Dapper; +using FluentAssertions; +using grate.Configuration; +using grate.Exceptions; +using Oracle.TestInfrastructure; +using TestCommon.TestInfrastructure; +using static grate.Configuration.KnownFolderKeys; + +namespace Docker.Oracle.Running_MigrationScripts; + +[Collection(nameof(OracleGrateTestContext))] +// ReSharper disable once InconsistentNaming +public class One_time_scripts(OracleGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.One_time_scripts(testContext, testOutput) +{ + protected override string CreateView1 => base.CreateView1 + " FROM DUAL"; + protected override string CreateView2 => base.CreateView2 + " FROM DUAL"; + + [Fact] + public override async Task Fails_if_changed_between_runs() + { + var db = TestConfig.RandomDatabase(); + + var parent = CreateRandomTempDirectory(); + var knownFolders = Folders.Default; + CreateDummySql(parent, knownFolders[Up]); + + var config = GrateConfigurationBuilder.Create(Context.DefaultConfiguration) + .WithConnectionString(Context.ConnectionString(db)) + .WithFolders(knownFolders) + .WithSqlFilesDirectory(parent) + .Build(); + + await using (var migrator = Context.Migrator.WithConfiguration(config)) + { + await migrator.Migrate(); + } + + WriteSomeOtherSql(parent, knownFolders[Up]); + + await using (var migrator = Context.Migrator.WithConfiguration(config)) + { + var ex = await Assert.ThrowsAsync(() => migrator.Migrate()); + } + + string[] scripts; + string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + + using (var conn = Context.CreateDbConnection(db)) + { + scripts = (await conn.QueryAsync(sql)).ToArray(); + } + + scripts.Should().HaveCount(1); + scripts.First().Should().Be(Context.Sql.SelectVersion); + + //await Context.DropDatabase(db); + } + +} + + diff --git a/unittests/Docker/Docker.Oracle/Startup.cs b/unittests/Docker/Docker.Oracle/Startup.cs new file mode 100644 index 00000000..db07c6ca --- /dev/null +++ b/unittests/Docker/Docker.Oracle/Startup.cs @@ -0,0 +1,14 @@ +using grate.Configuration; +using Oraclde.TestInfrastructure; +using Oracle.TestInfrastructure; +using TestCommon.TestInfrastructure; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Docker.Oracle; + +// ReSharper disable once UnusedType.Global +public class Startup: Common.Startup +{ + protected override DatabaseType DatabaseType => DatabaseType.Oracle; +} diff --git a/unittests/Docker/Docker.PostgreSQL/Docker.PostgreSQL.csproj b/unittests/Docker/Docker.PostgreSQL/Docker.PostgreSQL.csproj new file mode 100644 index 00000000..fa2a1710 --- /dev/null +++ b/unittests/Docker/Docker.PostgreSQL/Docker.PostgreSQL.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + ../../PostgreSQL + + + + diff --git a/unittests/Docker/Docker.PostgreSQL/Running_MigrationScripts/Failing_Scripts.cs b/unittests/Docker/Docker.PostgreSQL/Running_MigrationScripts/Failing_Scripts.cs new file mode 100644 index 00000000..5e0a7601 --- /dev/null +++ b/unittests/Docker/Docker.PostgreSQL/Running_MigrationScripts/Failing_Scripts.cs @@ -0,0 +1,24 @@ +using PostgreSQL.TestInfrastructure; +using TestCommon.TestInfrastructure; + +namespace Docker.PostgreSQL.Running_MigrationScripts; + +[Collection(nameof(PostgreSqlGrateTestContext))] +// ReSharper disable once InconsistentNaming +public class Failing_Scripts(PostgreSqlGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.Failing_Scripts(testContext, testOutput) +{ + protected override string ExpectedStartOfErrorMessageForInvalidSql => "Not relevant"; + protected override IDictionary ExpectedErrorDetails => new Dictionary(); + + [Fact(Skip = "Cannot check on the exact error message when running the command line tool. The error message is not available.")] + public override Task Aborts_the_run_giving_an_error_message() => Task.CompletedTask; + + [Fact(Skip = "Cannot check on the exact error details when running the command line tool. The error details are not available.")] + public override Task Exception_includes_details_on_the_failed_script() => Task.CompletedTask; + + [Fact(Skip = + "We do not have the ability to check for transient errors when running the command line tool. The error details are not available.")] + public override Task Exception_is_set_to_transient_based_on_inner_exceptions() => Task.CompletedTask; + +} diff --git a/unittests/Docker/Docker.PostgreSQL/Running_MigrationScripts/One_time_scripts.cs b/unittests/Docker/Docker.PostgreSQL/Running_MigrationScripts/One_time_scripts.cs new file mode 100644 index 00000000..a0113ddd --- /dev/null +++ b/unittests/Docker/Docker.PostgreSQL/Running_MigrationScripts/One_time_scripts.cs @@ -0,0 +1,59 @@ +using Dapper; +using FluentAssertions; +using grate.Configuration; +using grate.Exceptions; +using PostgreSQL.TestInfrastructure; +using TestCommon.TestInfrastructure; +using static grate.Configuration.KnownFolderKeys; + +namespace Docker.PostgreSQL.Running_MigrationScripts; + +[Collection(nameof(PostgreSqlGrateTestContext))] +// ReSharper disable once InconsistentNaming +public class One_time_scripts(PostgreSqlGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.One_time_scripts(testContext, testOutput) +{ + [Fact] + public override async Task Fails_if_changed_between_runs() + { + var db = TestConfig.RandomDatabase(); + + var parent = CreateRandomTempDirectory(); + var knownFolders = Folders.Default; + CreateDummySql(parent, knownFolders[Up]); + + var config = GrateConfigurationBuilder.Create(Context.DefaultConfiguration) + .WithConnectionString(Context.ConnectionString(db)) + .WithFolders(knownFolders) + .WithSqlFilesDirectory(parent) + .Build(); + + await using (var migrator = Context.Migrator.WithConfiguration(config)) + { + await migrator.Migrate(); + } + + WriteSomeOtherSql(parent, knownFolders[Up]); + + await using (var migrator = Context.Migrator.WithConfiguration(config)) + { + var ex = await Assert.ThrowsAsync(() => migrator.Migrate()); + } + + string[] scripts; + string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + + using (var conn = Context.External.CreateDbConnection(db)) + { + scripts = (await conn.QueryAsync(sql)).ToArray(); + } + + scripts.Should().HaveCount(1); + scripts.First().Should().Be(Context.Sql.SelectVersion); + + //await Context.DropDatabase(db); + } + +} + + diff --git a/unittests/Docker/Docker.PostgreSQL/Startup.cs b/unittests/Docker/Docker.PostgreSQL/Startup.cs new file mode 100644 index 00000000..a3ffd74c --- /dev/null +++ b/unittests/Docker/Docker.PostgreSQL/Startup.cs @@ -0,0 +1,13 @@ +using grate.Configuration; +using PostgreSQL.TestInfrastructure; +using TestCommon.TestInfrastructure; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Docker.PostgreSQL; + +// ReSharper disable once UnusedType.Global +public class Startup: Docker.Common.Startup +{ + protected override DatabaseType DatabaseType => DatabaseType.PostgreSQL; +} diff --git a/unittests/Docker/Docker.SqlServer/Docker.SqlServer.csproj b/unittests/Docker/Docker.SqlServer/Docker.SqlServer.csproj new file mode 100644 index 00000000..daa4f49b --- /dev/null +++ b/unittests/Docker/Docker.SqlServer/Docker.SqlServer.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + Imported\TestInfrastructure\SqlServerExternalDatabase.cs + + + + + ../../SqlServer + + + + diff --git a/unittests/Docker/Docker.SqlServer/Running_MigrationScripts/Failing_Scripts.cs b/unittests/Docker/Docker.SqlServer/Running_MigrationScripts/Failing_Scripts.cs new file mode 100644 index 00000000..02f0f35f --- /dev/null +++ b/unittests/Docker/Docker.SqlServer/Running_MigrationScripts/Failing_Scripts.cs @@ -0,0 +1,24 @@ +using TestCommon.TestInfrastructure; +using SqlServer.TestInfrastructure; + +namespace Docker.SqlServer.Running_MigrationScripts; + +[Collection(nameof(SqlServerGrateTestContext))] +// ReSharper disable once InconsistentNaming +public class Failing_Scripts(SqlServerGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.Failing_Scripts(testContext, testOutput) +{ + protected override string ExpectedStartOfErrorMessageForInvalidSql => "Not relevant"; + protected override IDictionary ExpectedErrorDetails => new Dictionary(); + + [Fact(Skip = "Cannot check on the exact error message when running the command line tool. The error message is not available.")] + public override Task Aborts_the_run_giving_an_error_message() => Task.CompletedTask; + + [Fact(Skip = "Cannot check on the exact error details when running the command line tool. The error details are not available.")] + public override Task Exception_includes_details_on_the_failed_script() => Task.CompletedTask; + + [Fact(Skip = + "We do not have the ability to check for transient errors when running the command line tool. The error details are not available.")] + public override Task Exception_is_set_to_transient_based_on_inner_exceptions() => Task.CompletedTask; + +} diff --git a/unittests/Docker/Docker.SqlServer/Running_MigrationScripts/One_time_scripts.cs b/unittests/Docker/Docker.SqlServer/Running_MigrationScripts/One_time_scripts.cs new file mode 100644 index 00000000..7455b00c --- /dev/null +++ b/unittests/Docker/Docker.SqlServer/Running_MigrationScripts/One_time_scripts.cs @@ -0,0 +1,59 @@ +using Dapper; +using FluentAssertions; +using grate.Configuration; +using grate.Exceptions; +using TestCommon.TestInfrastructure; +using SqlServer.TestInfrastructure; +using static grate.Configuration.KnownFolderKeys; + +namespace Docker.SqlServer.Running_MigrationScripts; + +[Collection(nameof(SqlServerGrateTestContext))] +// ReSharper disable once InconsistentNaming +public class One_time_scripts(SqlServerGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.One_time_scripts(testContext, testOutput) +{ + [Fact] + public override async Task Fails_if_changed_between_runs() + { + var db = TestConfig.RandomDatabase(); + + var parent = CreateRandomTempDirectory(); + var knownFolders = Folders.Default; + CreateDummySql(parent, knownFolders[Up]); + + var config = GrateConfigurationBuilder.Create(Context.DefaultConfiguration) + .WithConnectionString(Context.ConnectionString(db)) + .WithFolders(knownFolders) + .WithSqlFilesDirectory(parent) + .Build(); + + await using (var migrator = Context.Migrator.WithConfiguration(config)) + { + await migrator.Migrate(); + } + + WriteSomeOtherSql(parent, knownFolders[Up]); + + await using (var migrator = Context.Migrator.WithConfiguration(config)) + { + var ex = await Assert.ThrowsAsync(() => migrator.Migrate()); + } + + string[] scripts; + string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + + using (var conn = Context.CreateDbConnection(db)) + { + scripts = (await conn.QueryAsync(sql)).ToArray(); + } + + scripts.Should().HaveCount(1); + scripts.First().Should().Be(Context.Sql.SelectVersion); + + //await Context.DropDatabase(db); + } + +} + + diff --git a/unittests/Docker/Docker.SqlServer/Startup.cs b/unittests/Docker/Docker.SqlServer/Startup.cs new file mode 100644 index 00000000..44a4dab3 --- /dev/null +++ b/unittests/Docker/Docker.SqlServer/Startup.cs @@ -0,0 +1,12 @@ +using grate.Configuration; +using SqlServer.TestInfrastructure; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Docker.SqlServer; + +// ReSharper disable once UnusedType.Global +public class Startup: Docker.Common.Startup +{ + protected override DatabaseType DatabaseType => DatabaseType.SQLServer; +} diff --git a/unittests/Docker/Docker.Sqlite/Docker.Sqlite.csproj b/unittests/Docker/Docker.Sqlite/Docker.Sqlite.csproj new file mode 100644 index 00000000..ceb02f9d --- /dev/null +++ b/unittests/Docker/Docker.Sqlite/Docker.Sqlite.csproj @@ -0,0 +1,43 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + Imported/TestInfrastructure/ + Imported\TestInfrastructure\ExternalSqliteDatabase.cs + + + + + ../../Sqlite + + + + diff --git a/unittests/Docker/Docker.Sqlite/Running_MigrationScripts/Failing_Scripts.cs b/unittests/Docker/Docker.Sqlite/Running_MigrationScripts/Failing_Scripts.cs new file mode 100644 index 00000000..54f720f2 --- /dev/null +++ b/unittests/Docker/Docker.Sqlite/Running_MigrationScripts/Failing_Scripts.cs @@ -0,0 +1,24 @@ +using Sqlite.TestInfrastructure; +using TestCommon.TestInfrastructure; + +namespace Docker.Sqlite.Running_MigrationScripts; + +[Collection(nameof(SqliteGrateTestContext))] +// ReSharper disable once InconsistentNaming +public class Failing_Scripts(SqliteGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.Failing_Scripts(testContext, testOutput) +{ + protected override string ExpectedStartOfErrorMessageForInvalidSql => "Not relevant"; + protected override IDictionary ExpectedErrorDetails => new Dictionary(); + + [Fact(Skip = "Cannot check on the exact error message when running the command line tool. The error message is not available.")] + public override Task Aborts_the_run_giving_an_error_message() => Task.CompletedTask; + + [Fact(Skip = "Cannot check on the exact error details when running the command line tool. The error details are not available.")] + public override Task Exception_includes_details_on_the_failed_script() => Task.CompletedTask; + + [Fact(Skip = + "We do not have the ability to check for transient errors when running the command line tool. The error details are not available.")] + public override Task Exception_is_set_to_transient_based_on_inner_exceptions() => Task.CompletedTask; + +} diff --git a/unittests/Docker/Docker.Sqlite/Running_MigrationScripts/One_time_scripts.cs b/unittests/Docker/Docker.Sqlite/Running_MigrationScripts/One_time_scripts.cs new file mode 100644 index 00000000..cd2452ae --- /dev/null +++ b/unittests/Docker/Docker.Sqlite/Running_MigrationScripts/One_time_scripts.cs @@ -0,0 +1,59 @@ +using Dapper; +using FluentAssertions; +using grate.Configuration; +using grate.Exceptions; +using Sqlite.TestInfrastructure; +using TestCommon.TestInfrastructure; +using static grate.Configuration.KnownFolderKeys; + +namespace Docker.Sqlite.Running_MigrationScripts; + +[Collection(nameof(SqliteGrateTestContext))] +// ReSharper disable once InconsistentNaming +public class One_time_scripts(SqliteGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.One_time_scripts(testContext, testOutput) +{ + [Fact] + public override async Task Fails_if_changed_between_runs() + { + var db = TestConfig.RandomDatabase(); + + var parent = CreateRandomTempDirectory(); + var knownFolders = Folders.Default; + CreateDummySql(parent, knownFolders[Up]); + + var config = GrateConfigurationBuilder.Create(Context.DefaultConfiguration) + .WithConnectionString(Context.ConnectionString(db)) + .WithFolders(knownFolders) + .WithSqlFilesDirectory(parent) + .Build(); + + await using (var migrator = Context.Migrator.WithConfiguration(config)) + { + await migrator.Migrate(); + } + + WriteSomeOtherSql(parent, knownFolders[Up]); + + await using (var migrator = Context.Migrator.WithConfiguration(config)) + { + var ex = await Assert.ThrowsAsync(() => migrator.Migrate()); + } + + string[] scripts; + string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + + using (var conn = Context.CreateDbConnection(db)) + { + scripts = (await conn.QueryAsync(sql)).ToArray(); + } + + scripts.Should().HaveCount(1); + scripts.First().Should().Be(Context.Sql.SelectVersion); + + //await Context.DropDatabase(db); + } + +} + + diff --git a/unittests/Docker/Docker.Sqlite/Startup.cs b/unittests/Docker/Docker.Sqlite/Startup.cs new file mode 100644 index 00000000..9b4181e9 --- /dev/null +++ b/unittests/Docker/Docker.Sqlite/Startup.cs @@ -0,0 +1,13 @@ +using grate.Configuration; +using Sqlite.TestInfrastructure; +using TestCommon.TestInfrastructure; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Docker.Sqlite; + +// ReSharper disable once UnusedType.Global +public class Startup: Docker.Common.Startup +{ + protected override DatabaseType DatabaseType => DatabaseType.SQLite; +} diff --git a/unittests/Docker/IncludeTests.targets b/unittests/Docker/IncludeTests.targets new file mode 100644 index 00000000..4d217022 --- /dev/null +++ b/unittests/Docker/IncludeTests.targets @@ -0,0 +1,24 @@ + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/unittests/MariaDB/TestInfrastructure/MariaDbGrateTestContext.cs b/unittests/MariaDB/TestInfrastructure/MariaDbGrateTestContext.cs index d4ba2e0a..b9f8ca02 100644 --- a/unittests/MariaDB/TestInfrastructure/MariaDbGrateTestContext.cs +++ b/unittests/MariaDB/TestInfrastructure/MariaDbGrateTestContext.cs @@ -11,7 +11,7 @@ namespace MariaDB.TestInfrastructure; [CollectionDefinition(nameof(MariaDbGrateTestContext))] public class MariaDbTestCollection : ICollectionFixture; -public class MariaDbGrateTestContext( +public record MariaDbGrateTestContext( IGrateMigrator migrator, ITestDatabase testDatabase) : GrateTestContext(migrator, testDatabase) { diff --git a/unittests/MariaDB/TestInfrastructure/MariaDbTestContainerDatabase.cs b/unittests/MariaDB/TestInfrastructure/MariaDbTestContainerDatabase.cs index ae0434c8..f959d0e3 100644 --- a/unittests/MariaDB/TestInfrastructure/MariaDbTestContainerDatabase.cs +++ b/unittests/MariaDB/TestInfrastructure/MariaDbTestContainerDatabase.cs @@ -1,6 +1,5 @@ -using DotNet.Testcontainers.Builders; -using DotNet.Testcontainers.Configurations; -using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; using Microsoft.Extensions.Logging; using TestCommon.TestInfrastructure; using Testcontainers.MariaDb; @@ -8,25 +7,41 @@ namespace MariaDB.TestInfrastructure; // ReSharper disable once ClassNeverInstantiated.Global -public class MariaDbTestContainerDatabase( - GrateTestConfig grateTestConfig, - ILogger logger) : TestContainerDatabase(logger) +public record MariaDbTestContainerDatabase( + GrateTestConfig GrateTestConfig, + ILogger Logger, + INetwork Network + ) : TestContainerDatabase(GrateTestConfig) { - public override string DockerImage => grateTestConfig.DockerImage ?? "mariadb:10.10"; - protected override int InternalPort => 3306; - - protected override IContainer InitializeTestContainer(ILogger logger) + public override string DockerImage => GrateTestConfig.DockerImage ?? "mariadb:10.10"; + protected override int InternalPort => MariaDbBuilder.MariaDbPort; + protected override string NetworkAlias => "mariadb-test-container"; + + protected override IContainer InitializeTestContainer() { return new MariaDbBuilder() + .WithCommand("--max_connections=10000") .WithImage(DockerImage) .WithPassword(AdminPassword) .WithPortBinding(InternalPort, true) - .WithCommand("--max_connections=10000") - .WithLogger(logger) + .WithNetworkAliases(NetworkAlias) + .WithNetwork(Network) + .WithLogger(Logger) .Build(); } + + public override async Task InitializeAsync() + { + await Network.CreateAsync(); + await base.InitializeAsync(); + } - private string Hostname => TestContainer.Hostname; + public override async Task DisposeAsync() + { + await Network.DeleteAsync(); + await Network.DisposeAsync(); + await base.DisposeAsync(); + } public override string AdminConnectionString => $"Server={Hostname};Port={Port};Database=mysql;Uid=root;Pwd={AdminPassword}"; public override string ConnectionString(string database) => $"Server={Hostname};Port={Port};Database={database};Uid=root;Pwd={AdminPassword}"; diff --git a/unittests/Oracle/TestInfrastructure/OracleGrateTestContext.cs b/unittests/Oracle/TestInfrastructure/OracleGrateTestContext.cs index a850e51c..9e8eeacc 100644 --- a/unittests/Oracle/TestInfrastructure/OracleGrateTestContext.cs +++ b/unittests/Oracle/TestInfrastructure/OracleGrateTestContext.cs @@ -13,7 +13,7 @@ namespace Oracle.TestInfrastructure; [CollectionDefinition(nameof(OracleGrateTestContext))] public class OracleTestCollection : ICollectionFixture; -public class OracleGrateTestContext( +public record OracleGrateTestContext( IGrateMigrator migrator, ITestDatabase testDatabase) : GrateTestContext(migrator, testDatabase) { diff --git a/unittests/Oracle/TestInfrastructure/OracleTestContainerDatabase.cs b/unittests/Oracle/TestInfrastructure/OracleTestContainerDatabase.cs index 34329ea7..ffa5f75a 100644 --- a/unittests/Oracle/TestInfrastructure/OracleTestContainerDatabase.cs +++ b/unittests/Oracle/TestInfrastructure/OracleTestContainerDatabase.cs @@ -1,17 +1,22 @@ using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; using Microsoft.Extensions.Logging; using Testcontainers.Oracle; namespace TestCommon.TestInfrastructure; -public class OracleTestContainerDatabase( - GrateTestConfig grateTestConfig, - ILogger logger) : TestContainerDatabase(logger) + +// ReSharper disable once ClassNeverInstantiated.Global +public record OracleTestContainerDatabase( + GrateTestConfig GrateTestConfig, + ILogger Logger, + INetwork Network) : TestContainerDatabase(GrateTestConfig) { //public override string DockerImage => "gvenzl/oracle-xe:21.3.0-slim-faststart"; - public override string DockerImage => grateTestConfig.DockerImage ?? "gvenzl/oracle-free:latest-faststart"; - protected override int InternalPort => 1521; + public override string DockerImage => GrateTestConfig.DockerImage ?? "gvenzl/oracle-free:latest-faststart"; + protected override int InternalPort => OracleBuilder.OraclePort; + protected override string NetworkAlias => "oracle-test-container"; - protected override IContainer InitializeTestContainer(ILogger logger) + protected override IContainer InitializeTestContainer() { return new OracleBuilder() .WithImage(DockerImage) @@ -19,7 +24,7 @@ protected override IContainer InitializeTestContainer(ILogger logger) .WithEnvironment("ORACLE_PDB", "FREEPDB1") .WithPassword(AdminPassword) .WithPortBinding(InternalPort, true) - .WithLogger(logger) + .WithLogger(Logger) .Build(); } diff --git a/unittests/PostgreSQL/Running_MigrationScripts/Everytime_scripts.cs b/unittests/PostgreSQL/Running_MigrationScripts/Everytime_scripts.cs index 73322cc3..87115e0e 100644 --- a/unittests/PostgreSQL/Running_MigrationScripts/Everytime_scripts.cs +++ b/unittests/PostgreSQL/Running_MigrationScripts/Everytime_scripts.cs @@ -59,7 +59,7 @@ USING btree string[] scripts; string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } diff --git a/unittests/PostgreSQL/TestInfrastructure/PostgreSqlGrateTestContext.cs b/unittests/PostgreSQL/TestInfrastructure/PostgreSqlGrateTestContext.cs index d690ba11..bc10230d 100644 --- a/unittests/PostgreSQL/TestInfrastructure/PostgreSqlGrateTestContext.cs +++ b/unittests/PostgreSQL/TestInfrastructure/PostgreSqlGrateTestContext.cs @@ -12,7 +12,7 @@ namespace PostgreSQL.TestInfrastructure; [CollectionDefinition(nameof(PostgreSqlGrateTestContext))] public class PostgresqlTestCollection : ICollectionFixture; -public class PostgreSqlGrateTestContext( +public record PostgreSqlGrateTestContext( IGrateMigrator migrator, ITestDatabase testDatabase) : GrateTestContext(migrator, testDatabase) { diff --git a/unittests/PostgreSQL/TestInfrastructure/PostgreSqlTestContainerDatabase.cs b/unittests/PostgreSQL/TestInfrastructure/PostgreSqlTestContainerDatabase.cs index 48e024a8..38405b36 100644 --- a/unittests/PostgreSQL/TestInfrastructure/PostgreSqlTestContainerDatabase.cs +++ b/unittests/PostgreSQL/TestInfrastructure/PostgreSqlTestContainerDatabase.cs @@ -1,34 +1,42 @@ using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; using Microsoft.Extensions.Logging; namespace TestCommon.TestInfrastructure; -public class PostgreSqlTestContainerDatabase( - GrateTestConfig grateTestConfig, - ILogger logger) - : TestContainerDatabase(logger) + +// ReSharper disable once ClassNeverInstantiated.Global +public record PostgreSqlTestContainerDatabase( + GrateTestConfig GrateTestConfig, + ILogger Logger, + INetwork Network + ) + : TestContainerDatabase(GrateTestConfig) { - public override string DockerImage => grateTestConfig.DockerImage ?? "postgres:16"; - protected override int InternalPort => 5432; + public override string DockerImage => GrateTestConfig.DockerImage ?? "postgres:16"; + protected override int InternalPort => PostgreSqlBuilder.PostgreSqlPort; + protected override string NetworkAlias => "postgresql-test-container"; - protected override IContainer InitializeTestContainer(ILogger logger) + protected override IContainer InitializeTestContainer() { return new PostgreSqlBuilder() .WithImage(DockerImage) .WithPassword(AdminPassword) .WithPortBinding(InternalPort, true) - .WithLogger(logger) + .WithNetworkAliases(NetworkAlias) + .WithNetwork(Network) + .WithLogger(Logger) .Build(); } public override string AdminConnectionString => - $"Host={TestContainer.Hostname};Port={Port};Database=postgres;Username=postgres;Password={AdminPassword};Include Error Detail=true;Pooling=false"; + $"Host={Hostname};Port={Port};Database=postgres;Username=postgres;Password={AdminPassword};Include Error Detail=true;Pooling=false"; public override string ConnectionString(string database) => - $"Host={TestContainer.Hostname};Port={Port};Database={database};Username=postgres;Password={AdminPassword};Include Error Detail=true;Pooling=false"; + $"Host={Hostname};Port={Port};Database={database};Username=postgres;Password={AdminPassword};Include Error Detail=true;Pooling=false"; public override string UserConnectionString(string database) => - $"Host={TestContainer.Hostname};Port={Port};Database={database};Username=postgres;Password={AdminPassword};Include Error Detail=true;Pooling=false"; + $"Host={Hostname};Port={Port};Database={database};Username=postgres;Password={AdminPassword};Include Error Detail=true;Pooling=false"; } diff --git a/unittests/SqlServer/TestInfrastructure/SqlServerGrateTestContext.cs b/unittests/SqlServer/TestInfrastructure/SqlServerGrateTestContext.cs index 1bd30b82..732c5057 100644 --- a/unittests/SqlServer/TestInfrastructure/SqlServerGrateTestContext.cs +++ b/unittests/SqlServer/TestInfrastructure/SqlServerGrateTestContext.cs @@ -11,7 +11,7 @@ namespace SqlServer.TestInfrastructure; [CollectionDefinition(nameof(SqlServerGrateTestContext))] public class SqlServerTestCollection : ICollectionFixture; -public class SqlServerGrateTestContext : GrateTestContext +public record SqlServerGrateTestContext : GrateTestContext { public SqlServerGrateTestContext( IGrateMigrator migrator, diff --git a/unittests/SqlServer/TestInfrastructure/SqlServerTestContainerDatabase.cs b/unittests/SqlServer/TestInfrastructure/SqlServerTestContainerDatabase.cs index a80e7de5..5870a68d 100644 --- a/unittests/SqlServer/TestInfrastructure/SqlServerTestContainerDatabase.cs +++ b/unittests/SqlServer/TestInfrastructure/SqlServerTestContainerDatabase.cs @@ -1,20 +1,24 @@ using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; using Microsoft.Extensions.Logging; using TestCommon.TestInfrastructure; using Testcontainers.MsSql; namespace SqlServer.TestInfrastructure; -public class SqlServerTestContainerDatabase( - GrateTestConfig grateTestConfig, - ILogger logger) - : TestContainerDatabase(logger) + +// ReSharper disable once ClassNeverInstantiated.Global +public record SqlServerTestContainerDatabase( + GrateTestConfig GrateTestConfig, + ILogger Logger, + INetwork Network) + : TestContainerDatabase(GrateTestConfig) { // Run with linux/amd86 on ARM architectures too, the docker emulation is good enough - //public override string DockerImage => "mcr.microsoft.com/mssql/server:2019-latest"; - public override string DockerImage => grateTestConfig.DockerImage ?? "mcr.microsoft.com/mssql/server:2022-latest"; - protected override int InternalPort => 1433; + public override string DockerImage => GrateTestConfig.DockerImage ?? "mcr.microsoft.com/mssql/server:2022-latest"; + protected override int InternalPort => MsSqlBuilder.MsSqlPort; + protected override string NetworkAlias => "sqlserver-test-container"; - protected override IContainer InitializeTestContainer(ILogger logger) + protected override IContainer InitializeTestContainer() { return new MsSqlBuilder() .WithImage(DockerImage) @@ -22,17 +26,17 @@ protected override IContainer InitializeTestContainer(ILogger logger) .WithPassword(AdminPassword) .WithPortBinding(InternalPort, true) .WithEnvironment("MSSQL_COLLATION", "Danish_Norwegian_CI_AS") - .WithLogger(logger) + .WithLogger(Logger) .Build(); } public override string AdminConnectionString => - $"Data Source=localhost,{Port};Initial Catalog=master;User Id=sa;Password={AdminPassword};Encrypt=false;Pooling=false"; + $"Data Source={Hostname},{Port};Initial Catalog=master;User Id=sa;Password={AdminPassword};Encrypt=false;Pooling=false"; public override string ConnectionString(string database) => - $"Data Source=localhost,{Port};Initial Catalog={database};User Id=sa;Password={AdminPassword};Encrypt=false;Pooling=false;Connect Timeout=2"; + $"Data Source={Hostname},{Port};Initial Catalog={database};User Id=sa;Password={AdminPassword};Encrypt=false;Pooling=false;Connect Timeout=2"; public override string UserConnectionString(string database) => - $"Data Source=localhost,{Port};Initial Catalog={database};User Id=zorro;Password=batmanZZ4;Encrypt=false;Pooling=false;Connect Timeout=2"; + $"Data Source={Hostname},{Port};Initial Catalog={database};User Id=zorro;Password=batmanZZ4;Encrypt=false;Pooling=false;Connect Timeout=2"; } diff --git a/unittests/SqlServerCaseSensitive/TestInfrastructure/SqlServerGrateTestContext.cs b/unittests/SqlServerCaseSensitive/TestInfrastructure/SqlServerGrateTestContext.cs index d57f6f48..11d7f238 100644 --- a/unittests/SqlServerCaseSensitive/TestInfrastructure/SqlServerGrateTestContext.cs +++ b/unittests/SqlServerCaseSensitive/TestInfrastructure/SqlServerGrateTestContext.cs @@ -12,7 +12,7 @@ namespace SqlServerCaseSensitive.TestInfrastructure; [CollectionDefinition(nameof(SqlServerGrateTestContext))] public class SqlServerTestCollection : ICollectionFixture; -public class SqlServerGrateTestContext( +public record SqlServerGrateTestContext( IGrateMigrator migrator, string serverCollation, ITestDatabase testDatabase) diff --git a/unittests/SqlServerCaseSensitive/TestInfrastructure/SqlServerTestContainerDatabase.cs b/unittests/SqlServerCaseSensitive/TestInfrastructure/SqlServerTestContainerDatabase.cs index 38015ec3..b1d41f6a 100644 --- a/unittests/SqlServerCaseSensitive/TestInfrastructure/SqlServerTestContainerDatabase.cs +++ b/unittests/SqlServerCaseSensitive/TestInfrastructure/SqlServerTestContainerDatabase.cs @@ -1,19 +1,22 @@ using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; using Microsoft.Extensions.Logging; using TestCommon.TestInfrastructure; using Testcontainers.MsSql; namespace SqlServerCaseSensitive.TestInfrastructure; -public class SqlServerTestContainerDatabase( - GrateTestConfig grateTestConfig, - ILogger logger) - : TestContainerDatabase(logger) +public record SqlServerTestContainerDatabase( + GrateTestConfig GrateTestConfig, + ILogger Logger, + INetwork Network) + : TestContainerDatabase(GrateTestConfig) { // Run with linux/amd86 on ARM architectures too, the docker emulation is good enough - public override string DockerImage => grateTestConfig.DockerImage ?? "mcr.microsoft.com/mssql/server:2019-latest"; - protected override int InternalPort => 1433; + public override string DockerImage => GrateTestConfig.DockerImage ?? "mcr.microsoft.com/mssql/server:2019-latest"; + protected override int InternalPort => MsSqlBuilder.MsSqlPort; + protected override string NetworkAlias => "sqlserver-test-container"; - protected override IContainer InitializeTestContainer(ILogger logger) + protected override IContainer InitializeTestContainer() { return new MsSqlBuilder() .WithImage(DockerImage) @@ -21,7 +24,7 @@ protected override IContainer InitializeTestContainer(ILogger logger) .WithPassword(AdminPassword) .WithPortBinding(InternalPort, true) .WithEnvironment("MSSQL_COLLATION", "Danish_Norwegian_CI_AS") - .WithLogger(logger) + .WithLogger(Logger) .Build(); } diff --git a/unittests/Sqlite/TestInfrastructure/SqliteGrateTestContext.cs b/unittests/Sqlite/TestInfrastructure/SqliteGrateTestContext.cs index 12d21a40..94e5e141 100644 --- a/unittests/Sqlite/TestInfrastructure/SqliteGrateTestContext.cs +++ b/unittests/Sqlite/TestInfrastructure/SqliteGrateTestContext.cs @@ -13,7 +13,7 @@ namespace Sqlite.TestInfrastructure; [CollectionDefinition(nameof(SqliteGrateTestContext))] public class SqliteTestCollection : ICollectionFixture; -public class SqliteGrateTestContext( +public record SqliteGrateTestContext( IGrateMigrator migrator, ITestDatabase testDatabase) : GrateTestContext(migrator, testDatabase) { diff --git a/unittests/TestCommon/Generic/Running_MigrationScripts/Anytime_scripts.cs b/unittests/TestCommon/Generic/Running_MigrationScripts/Anytime_scripts.cs index 45ae1268..c837c3bf 100644 --- a/unittests/TestCommon/Generic/Running_MigrationScripts/Anytime_scripts.cs +++ b/unittests/TestCommon/Generic/Running_MigrationScripts/Anytime_scripts.cs @@ -46,7 +46,7 @@ public async Task Are_not_run_more_than_once_when_unchanged() string[] scripts; string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -86,7 +86,7 @@ public async Task Are_run_again_if_changed_between_runs() string[] scripts; string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")} ORDER BY id"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -127,7 +127,7 @@ public async Task Do_not_have_text_logged_if_flag_set() string[] scripts; string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -154,7 +154,7 @@ public async Task Do_have_text_logged_by_default() .WithSqlFilesDirectory(parent) .Build(); - await using (migrator = Context.Migrator.WithConfiguration(config)) + await using (migrator = Context.External.Migrator.WithConfiguration(config)) { await migrator.Migrate(); } @@ -162,7 +162,7 @@ public async Task Do_have_text_logged_by_default() string[] scripts; string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -202,7 +202,7 @@ public async Task Are_run_more_than_once_when_unchanged_but_flag_set() string[] scripts; string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } diff --git a/unittests/TestCommon/Generic/Running_MigrationScripts/DropDatabase.cs b/unittests/TestCommon/Generic/Running_MigrationScripts/DropDatabase.cs index d69275db..94082fc1 100644 --- a/unittests/TestCommon/Generic/Running_MigrationScripts/DropDatabase.cs +++ b/unittests/TestCommon/Generic/Running_MigrationScripts/DropDatabase.cs @@ -45,7 +45,7 @@ public async Task Ensure_database_gets_dropped() string[] scripts; string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } diff --git a/unittests/TestCommon/Generic/Running_MigrationScripts/Environment_scripts.cs b/unittests/TestCommon/Generic/Running_MigrationScripts/Environment_scripts.cs index a1f4c920..f3bc6d9e 100644 --- a/unittests/TestCommon/Generic/Running_MigrationScripts/Environment_scripts.cs +++ b/unittests/TestCommon/Generic/Running_MigrationScripts/Environment_scripts.cs @@ -41,7 +41,7 @@ public async Task Are_not_run_if_not_in_environment() string[] scripts; string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -75,7 +75,7 @@ public async Task Are_not_run_by_default() //Bug #101 string[] scripts; string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -111,7 +111,7 @@ public async Task Are_run_if_in_environment() string[] scripts; string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -148,7 +148,7 @@ public async Task Non_environment_scripts_are_always_run() string[] scripts; string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } diff --git a/unittests/TestCommon/Generic/Running_MigrationScripts/Everytime_scripts.cs b/unittests/TestCommon/Generic/Running_MigrationScripts/Everytime_scripts.cs index dedcb9d2..73d50a70 100644 --- a/unittests/TestCommon/Generic/Running_MigrationScripts/Everytime_scripts.cs +++ b/unittests/TestCommon/Generic/Running_MigrationScripts/Everytime_scripts.cs @@ -39,7 +39,7 @@ public async Task Are_run_every_time_even_when_unchanged() string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using var conn = Context.CreateDbConnection(db); + using var conn = Context.External.CreateDbConnection(db); var scripts = (await conn.QueryAsync(sql)).ToArray(); scripts.Should().HaveCount(3); @@ -70,7 +70,7 @@ public async Task Are_not_run_in_dryrun() string sql = $"SELECT 1 FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")} " + $"WHERE script_name = '1_jalla.sql'"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { try { @@ -116,7 +116,7 @@ public async Task Are_recognized_by_script_name() string[] scripts; string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -151,7 +151,7 @@ public async Task Are_not_run_in_baseline() string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using var conn = Context.CreateDbConnection(db); + using var conn = Context.External.CreateDbConnection(db); var scripts = (await conn.QueryAsync(sql)).ToArray(); scripts.Should().HaveCount(1); //marked as run diff --git a/unittests/TestCommon/Generic/Running_MigrationScripts/Failing_Scripts.cs b/unittests/TestCommon/Generic/Running_MigrationScripts/Failing_Scripts.cs index a8e40323..d3b50b74 100644 --- a/unittests/TestCommon/Generic/Running_MigrationScripts/Failing_Scripts.cs +++ b/unittests/TestCommon/Generic/Running_MigrationScripts/Failing_Scripts.cs @@ -150,7 +150,7 @@ public async Task Inserts_Failed_Scripts_Into_ScriptRunErrors_Table() using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled)) { - using var conn = Context.GetDbConnection(Context.ConnectionString(db)); + using var conn = Context.GetDbConnection(Context.External.ConnectionString(db)); scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -187,7 +187,7 @@ public async Task Inserts_RepositoryPath_Into_ScriptRunErrors_Table() using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled)) { - using var conn = Context.GetDbConnection(Context.ConnectionString(db)); + using var conn = Context.External.GetDbConnection(Context.External.ConnectionString(db)); loggedRepositoryPath = (await conn.QuerySingleOrDefaultAsync(sql)); } @@ -225,7 +225,7 @@ public async Task Inserts_Large_Failed_Scripts_Into_ScriptRunErrors_Table() using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled)) { - using var conn = Context.GetDbConnection(Context.ConnectionString(db)); + using var conn = Context.External.GetDbConnection(Context.External.ConnectionString(db)); scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -362,7 +362,7 @@ public async Task Create_a_version_in_error_if_ran_without_transaction() using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled)) { - using var conn = Context.GetDbConnection(Context.ConnectionString(db)); + using var conn = Context.External.GetDbConnection(Context.External.ConnectionString(db)); versions = (await conn.QueryAsync(sql)).ToArray(); } @@ -398,7 +398,7 @@ private async Task RunMigration(MigrationsFolder folder, string filena string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema(config.SchemaName, config.ScriptsRunTableName)}"; - using (var conn = Context.GetDbConnection(Context.ConnectionString(db))) + using (var conn = Context.External.GetDbConnection(Context.External.ConnectionString(db))) { scripts = (await conn.QueryAsync(sql)).ToArray(); } diff --git a/unittests/TestCommon/Generic/Running_MigrationScripts/One_time_scripts.cs b/unittests/TestCommon/Generic/Running_MigrationScripts/One_time_scripts.cs index b93ffa30..f88b63b3 100644 --- a/unittests/TestCommon/Generic/Running_MigrationScripts/One_time_scripts.cs +++ b/unittests/TestCommon/Generic/Running_MigrationScripts/One_time_scripts.cs @@ -44,7 +44,7 @@ public async Task Are_not_run_more_than_once_when_unchanged() string[] scripts; string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -86,7 +86,7 @@ public virtual async Task Fails_if_changed_between_runs() string[] scripts; string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -129,7 +129,7 @@ public async Task Runs_and_warns_if_changed_between_runs_and_flag_set() string[] scripts; string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")} order by id"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -176,7 +176,7 @@ public async Task Ignores_and_warns_if_changed_between_runs_and_flag_set() string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")} order by id"; - using var conn = Context.CreateDbConnection(db); + using var conn = Context.External.CreateDbConnection(db); var scripts = await conn.QueryAsync(sql); var result = (await conn.QueryAsync("select col from grate")).Single(); diff --git a/unittests/TestCommon/Generic/Running_MigrationScripts/Order_Of_Scripts.cs b/unittests/TestCommon/Generic/Running_MigrationScripts/Order_Of_Scripts.cs index 8bbba675..c4fc0f8c 100644 --- a/unittests/TestCommon/Generic/Running_MigrationScripts/Order_Of_Scripts.cs +++ b/unittests/TestCommon/Generic/Running_MigrationScripts/Order_Of_Scripts.cs @@ -31,7 +31,7 @@ public async Task Is_as_expected() string[] scripts; string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")} ORDER BY id"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } diff --git a/unittests/TestCommon/Generic/Running_MigrationScripts/ScriptsRun_Table.cs b/unittests/TestCommon/Generic/Running_MigrationScripts/ScriptsRun_Table.cs index ef414c9f..62ca9575 100644 --- a/unittests/TestCommon/Generic/Running_MigrationScripts/ScriptsRun_Table.cs +++ b/unittests/TestCommon/Generic/Running_MigrationScripts/ScriptsRun_Table.cs @@ -42,7 +42,7 @@ public async Task Includes_the_folder_name_in_the_script_name_if_subfolders() string[] scripts; string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -79,7 +79,7 @@ public async Task Does_not_include_the_folder_name_in_the_script_name_if_no_subf string[] scripts; string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -124,7 +124,7 @@ public async Task Does_not_overwrite_scripts_from_different_folders_with_last_co Result[] scripts; string sql = $"SELECT script_name, text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } @@ -175,7 +175,7 @@ public async Task Can_handle_large_scripts() string[] scripts; string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; - using (var conn = Context.CreateDbConnection(db)) + using (var conn = Context.External.CreateDbConnection(db)) { scripts = (await conn.QueryAsync(sql)).ToArray(); } diff --git a/unittests/TestCommon/Generic/Running_MigrationScripts/TokenScripts.cs b/unittests/TestCommon/Generic/Running_MigrationScripts/TokenScripts.cs index b9f4557b..c62dc42b 100644 --- a/unittests/TestCommon/Generic/Running_MigrationScripts/TokenScripts.cs +++ b/unittests/TestCommon/Generic/Running_MigrationScripts/TokenScripts.cs @@ -40,7 +40,7 @@ public async Task Tokens_are_replaced() } string sql = $"SELECT dbase FROM grate"; - using var conn = Context.CreateDbConnection(db); + using var conn = Context.External.CreateDbConnection(db); var actual = await conn.QuerySingleAsync(sql); actual.Should().Be(db); @@ -71,7 +71,7 @@ public async Task User_tokens_are_replaced() } string sql = $"SELECT dbase FROM grate"; - using var conn = Context.CreateDbConnection(db); + using var conn = Context.External.CreateDbConnection(db); var actual = await conn.QuerySingleAsync(sql); actual.Should().Be("token1"); diff --git a/unittests/TestCommon/Startup.cs b/unittests/TestCommon/Startup.cs index 003e90b3..77de9c7f 100644 --- a/unittests/TestCommon/Startup.cs +++ b/unittests/TestCommon/Startup.cs @@ -1,3 +1,4 @@ +using DotNet.Testcontainers.Builders; using grate.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -13,7 +14,7 @@ public abstract class Startup { public void ConfigureHost(IHostBuilder hostBuilder) => hostBuilder - .ConfigureHostConfiguration(builder => builder.AddEnvironmentVariables() ) + .ConfigureHostConfiguration(builder => builder.AddEnvironmentVariables()) .ConfigureAppConfiguration((context, builder) => { }); // ReSharper disable once UnusedMember.Global @@ -25,7 +26,11 @@ public void ConfigureServices(IServiceCollection services, HostBuilderContext co .AddConsole() .SetMinimumLevel(TestConfig.GetLogLevel()) ); - + + var network = new NetworkBuilder() + .WithName(Guid.NewGuid().ToString("D")) + .Build(); + services.AddSingleton(network); ConfigureExtraServices(services, context); @@ -37,10 +42,14 @@ public void ConfigureServices(IServiceCollection services, HostBuilderContext co protected abstract Type ExternalTestDatabaseType { get; } protected abstract Type TestContextType { get; } + // ReSharper disable once UnassignedGetOnlyAutoProperty + protected virtual GrateTestConfig ExtraConfiguration(GrateTestConfig config) => config; + protected void RegisterTestDatabase(IServiceCollection services, IConfiguration configuration) { var testConfig = new GrateTestConfig(); configuration.Bind("GrateTestConfig", testConfig); + testConfig = ExtraConfiguration(testConfig); services.TryAddSingleton(testConfig); if (testConfig.AdminConnectionString != null) diff --git a/unittests/TestCommon/TestInfrastructure/GrateTestConfig.cs b/unittests/TestCommon/TestInfrastructure/GrateTestConfig.cs index 4d603d2a..7206d4b5 100644 --- a/unittests/TestCommon/TestInfrastructure/GrateTestConfig.cs +++ b/unittests/TestCommon/TestInfrastructure/GrateTestConfig.cs @@ -1,7 +1,8 @@ namespace TestCommon.TestInfrastructure; -public class GrateTestConfig +public record GrateTestConfig { public string? AdminConnectionString { get; set; } = null!; public string? DockerImage { get; set; } = null!; + public bool ConnectToDockerInternal { get; set; } } diff --git a/unittests/TestCommon/TestInfrastructure/GrateTestContext.cs b/unittests/TestCommon/TestInfrastructure/GrateTestContext.cs index 49b8f971..dceee4d6 100644 --- a/unittests/TestCommon/TestInfrastructure/GrateTestContext.cs +++ b/unittests/TestCommon/TestInfrastructure/GrateTestContext.cs @@ -5,15 +5,16 @@ namespace TestCommon.TestInfrastructure; -public abstract class GrateTestContext(IGrateMigrator migrator, ITestDatabase testDatabase) : IGrateTestContext, IAsyncLifetime +public abstract record GrateTestContext(IGrateMigrator Migrator, ITestDatabase TestDatabase) : IGrateTestContext, IAsyncLifetime { - - public IGrateMigrator Migrator { get; } = migrator; - protected ITestDatabase TestDatabase { get; } = testDatabase; + //public IGrateMigrator Migrator { get; } = Migrator; + //protected ITestDatabase TestDatabase { get; } = TestDatabase; public string AdminConnectionString => TestDatabase.AdminConnectionString; public string ConnectionString(string database) => TestDatabase.ConnectionString(database); public string UserConnectionString(string database) => TestDatabase.UserConnectionString(database); + + public virtual IGrateTestContext External => this with { TestDatabase = TestDatabase.External }; public async Task InitializeAsync() { diff --git a/unittests/TestCommon/TestInfrastructure/IGrateTestContext.cs b/unittests/TestCommon/TestInfrastructure/IGrateTestContext.cs index 555d83e0..36e1fe0e 100644 --- a/unittests/TestCommon/TestInfrastructure/IGrateTestContext.cs +++ b/unittests/TestCommon/TestInfrastructure/IGrateTestContext.cs @@ -1,7 +1,9 @@ using System.Data; +using System.Runtime.CompilerServices; using grate.Configuration; using grate.Infrastructure; using grate.Migration; +using Microsoft.Extensions.Logging; namespace TestCommon.TestInfrastructure; @@ -10,6 +12,8 @@ public interface IGrateTestContext string AdminConnectionString { get; } string ConnectionString(string database); string UserConnectionString(string database); + + IGrateTestContext External => this; IDbConnection CreateAdminDbConnection() => GetDbConnection(AdminConnectionString); IDbConnection CreateDbConnection(string database) => GetDbConnection(ConnectionString(database)); @@ -32,6 +36,7 @@ public interface IGrateTestContext AlterDatabase = true, NonInteractive = true, Transaction = SupportsTransaction, + OutputPath = Directory.CreateTempSubdirectory() }; public Task DropDatabase(string databaseName); diff --git a/unittests/TestCommon/TestInfrastructure/ITestDatabase.cs b/unittests/TestCommon/TestInfrastructure/ITestDatabase.cs index 59bc76b8..ae1f72d7 100644 --- a/unittests/TestCommon/TestInfrastructure/ITestDatabase.cs +++ b/unittests/TestCommon/TestInfrastructure/ITestDatabase.cs @@ -5,4 +5,12 @@ public interface ITestDatabase string AdminConnectionString { get; } string ConnectionString(string database); string UserConnectionString(string database); + + /// + /// An external representation of the database. Returns this instance by default, + /// mostly used to be able to access Docker containers both from the outside and the internal docker network + /// (from the inside when testing grate in a docker image, from the outside when performing assertions on the + /// _database_ container from the unit tests afterwards) + /// + public ITestDatabase External => this; } diff --git a/unittests/TestCommon/TestInfrastructure/TestContainerDatabase.cs b/unittests/TestCommon/TestInfrastructure/TestContainerDatabase.cs index 230efd65..93d037e6 100644 --- a/unittests/TestCommon/TestInfrastructure/TestContainerDatabase.cs +++ b/unittests/TestCommon/TestInfrastructure/TestContainerDatabase.cs @@ -1,47 +1,60 @@ -using DotNet.Testcontainers.Configurations; -using DotNet.Testcontainers.Containers; -using Microsoft.Extensions.Logging; +using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; namespace TestCommon.TestInfrastructure; -public abstract class TestContainerDatabase : ITestDatabase, IAsyncLifetime +public abstract record TestContainerDatabase : ITestDatabase, IAsyncLifetime { public abstract string DockerImage { get; } private readonly Random _random = Random.Shared; - public string AdminPassword { get; } - public int Port => TestContainer.GetMappedPublicPort(InternalPort); + protected string AdminPassword { get; } protected abstract int InternalPort { get; } + + public ITestDatabase External => this with { IsInternal = false }; + private bool IsInternal { get; init; } + + public abstract INetwork Network { get; init; } + protected abstract string NetworkAlias { get; } + + protected string Hostname => IsInternal ? NetworkAlias : TestContainer.Hostname; + protected int Port => IsInternal ? InternalPort : TestContainer.GetMappedPublicPort(InternalPort); + private const string LowerCase = "abcdefghijklmnopqrstuvwxyz"; private const string UpperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; private const string Digits = "1234567890"; - protected TestContainerDatabase(ILogger logger) + protected TestContainerDatabase(GrateTestConfig grateTestConfig) { AdminPassword = _random.GetString(10, UpperCase) + - _random.GetString(10, LowerCase) + - _random.GetString(10, Digits); + _random.GetString(10, LowerCase) + + _random.GetString(10, Digits); + + IsInternal = grateTestConfig.ConnectToDockerInternal; // ReSharper disable once VirtualMemberCallInConstructor - TestContainer = InitializeTestContainer(logger); + TestContainer = InitializeTestContainer(); } public IContainer TestContainer { get; } public abstract string AdminConnectionString { get; } - public async Task DisposeAsync() + public virtual async Task InitializeAsync() { - await TestContainer.DisposeAsync(); + await Network.CreateAsync(); + await TestContainer.StartAsync(); } - - public async Task InitializeAsync() + + public virtual async Task DisposeAsync() { - await TestContainer.StartAsync(); + //await Network.DeleteAsync(); + await Network.DisposeAsync(); + await TestContainer.DisposeAsync(); } - protected abstract IContainer InitializeTestContainer(ILogger logger); + protected abstract IContainer InitializeTestContainer(); public abstract string ConnectionString(string database); public abstract string UserConnectionString(string database); }