diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e1529f6..e653b6c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -111,9 +111,8 @@ jobs: matrix: arch: [ "win-x64", "win-x86", "win-arm64", "linux-musl-x64", "linux-musl-arm64", "linux-x64", "linux-arm64", - "osx-x64", "osx-arm64" + "osx-x64" ] - steps: - uses: actions/checkout@v4 - name: Setup .NET 8 @@ -126,24 +125,49 @@ jobs: env: VERSION: ${{ needs.set-version-number.outputs.nuGetVersion }} - - name: Publish .NET 6/7/8 dependent ${{ matrix.arch }} - run: dotnet publish ./src/grate/grate.csproj -r ${{ matrix.arch }} -c release --no-self-contained -o ./publish/${{ matrix.arch }}/dependent - env: - VERSION: ${{ needs.set-version-number.outputs.nuGetVersion }} - name: Upload self-contained ${{ matrix.arch }} - if: ${{ needs.set-version-number.outputs.is-release == 'true' }} + #if: ${{ needs.set-version-number.outputs.is-release == 'true' }} uses: actions/upload-artifact@v3 with: name: grate-${{ matrix.arch }}-self-contained-${{ needs.set-version-number.outputs.nuGetVersion }} path: ./publish/${{ matrix.arch }}/self-contained/* - - name: Upload .net dependent ${{ matrix.arch }} - if: ${{ needs.set-version-number.outputs.is-release == 'true' }} + + build-standalone-mac-arm64: + name: Build cli + needs: set-version-number + + # Use macos-14 to build osx-arm64, it runs on M1, see + # https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source/ + # + # I've earlier had problems with that the trimmed, self-contained binary for osx-arm64 that was built on Linux + # did not work when opened on an actual mac with arm64. + + runs-on: macos-14 + strategy: + matrix: + arch: [ "osx-arm64" ] + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Publish self-contained ${{ matrix.arch }} + run: dotnet publish ./src/grate/grate.csproj -f net8.0 -r ${{ matrix.arch }} -c release --self-contained -p:SelfContained=true -o ./publish/${{ matrix.arch }}/self-contained + env: + VERSION: ${{ needs.set-version-number.outputs.nuGetVersion }} + + - name: Upload self-contained ${{ matrix.arch }} + #if: ${{ needs.set-version-number.outputs.is-release == 'true' }} uses: actions/upload-artifact@v3 with: - name: grate-${{ matrix.arch }}-framework-dependent-${{ needs.set-version-number.outputs.nuGetVersion }} - path: ./publish/${{ matrix.arch }}/dependent/* + name: grate-${{ matrix.arch }}-self-contained-${{ needs.set-version-number.outputs.nuGetVersion }} + path: ./publish/${{ matrix.arch }}/self-contained/* + build-msi: name: Build MSI @@ -248,7 +272,7 @@ jobs: arch=$(echo ${{ matrix.arch }} | cut -d- -f2 | sed 's/x64/amd64/') echo "::set-output name=arch::$arch" - - name: Create dpkg # Linux with powershell script? really? + - name: Create dpkg # Linux with powershell script? really? YES! :D if: ${{ needs.set-version-number.outputs.is-release == 'true' }} run: ./installers/deb/Create-Package.ps1 -grateExe ./${{ matrix.arch }}/grate -Version "${{ needs.set-version-number.outputs.nuGetVersion }}" -arch ${{ steps.get-arch.outputs.arch}} env: @@ -260,33 +284,6 @@ jobs: name: grate_${{ needs.set-version-number.outputs.nuGetVersion }}-1_${{ steps.get-arch.outputs.arch}}.deb path: ./installers/deb/grate_${{ needs.set-version-number.outputs.nuGetVersion }}-1_${{ steps.get-arch.outputs.arch }}.deb - # build-winget: - # name: Winget - Update package manifest in the OWC - # needs: - # - set-version-number - # - build-msi - # runs-on: windows-latest - # if: ${{ needs.set-version-number.outputs.is-release == 'true' }} - - # steps: - # - name: Winget-Create - # run: | - - # $version = "$($env:version)" - - # # Download wingetcreate - # iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe - - # $packageUrl="https://github.com/erikbra/grate/releases/download/$version/grate-$version.msi" - - # echo "Running ./wingetcreate.exe update erikbra.grate -u $packageUrl -v $version -t `"$env:WINGET_GH_PAT`" --submit" - # ./wingetcreate.exe update erikbra.grate -u $packageUrl -v $version -t "$env:WINGET_GH_PAT" --submit - # env: - # WINGET_GH_PAT: ${{ secrets.WINGET_GH_PAT }} - # #version: "1.4.0" - # version: "${{ needs.set-version-number.outputs.nuGetVersion }}" - - test: name: Run tests @@ -302,12 +299,87 @@ jobs: with: dotnet-version: 8.0.x - name: Test - run: | - dotnet test \ - unittests/${{ matrix.category }} \ - --logger:"xunit;LogFilePath=/tmp/test-results/${{ matrix.category }}.xml" -- \ - -MaxCpuCount 2 + run: > + dotnet test + unittests/${{ matrix.category }} + --logger:"xunit;LogFilePath=/tmp/test-results/${{ matrix.category }}.xml" -- + -MaxCpuCount 2 + env: + LogLevel: Warning + TZ: UTC + + integration-test: + name: Tests cli + needs: + - set-version-number + - build-standalone + + strategy: + # We could really like to: + # - Start each database (on another host, using Docker, or other means, e.g. Azure SQL, etc + # - On all architectures available (windows-latest, linux-latest, macos-latest, and macos-14), + # - Run command-line tests against each database. + # + # This way, we can test against all variations of each database too, e.g. Azure SQL, SQL Server "on prem" (in docker), + # hosted Oracle databases, aws databases, etc. But we need to find a way to provision these databases both very fast, + # and cheap, for each one. + matrix: + category: + - CommandLine.SqlServer + - CommandLine.PostgreSQL + - CommandLine.MariaDB + - CommandLine.Sqlite + - CommandLine.Oracle + os: + # We can only run tests on Linux for now, until we start the database separately somewhere (azure, something) + # and run against an external database. Because the commandline tests are also for now _dependent_ on + # TestContainers, and running the database in a container using Docker. And Docker is only available on Linux + # on Github actions. + # - os: windows-latest + # executable: grate.exe + # - name: windows-latest + # arch: win-x64 + # executable: grate.exe + # - os: linux-latest + # executable: grate + - name: linux-latest + arch: linux-x64 + executable: grate + # - os: macos-latest + # executable: grate + # - name: macos-latest + # arch: osx-x64 + # executable: grate + # macos-14 is M1 (arm64) + # - name: macos-14 + # arch: osx-x64 + # executable: grate + + runs-on: ${{ matrix.os.name }} + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v3 + with: + name: grate-${{ matrix.os.arch }}-self-contained-${{ needs.set-version-number.outputs.nuGetVersion }} + path: executables/${{ matrix.os.arch }} + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: chmod u+x + run: chmod u+x $GrateExecutablePath + if: ${{ matrix.os.arch != 'win-x64' }} + env: + GrateExecutablePath: ${{ github.workspace }}/executables/${{ matrix.os.arch }}/${{ matrix.os.executable }} + - name: Test + run: > + dotnet test + unittests/CommandLine/${{ matrix.category }} + --logger:"xunit;LogFilePath=/tmp/test-results/${{ matrix.os.arch }}/${{ matrix.category }}.xml" -- + -MaxCpuCount 2 env: LogLevel: Warning + GrateExecutablePath: ${{ github.workspace }}/executables/${{ matrix.os.arch }}/${{ matrix.os.executable }} TZ: UTC diff --git a/.gitignore b/.gitignore index 303dae0f..e6c0f944 100644 --- a/.gitignore +++ b/.gitignore @@ -355,3 +355,5 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ /grate/Properties/launchSettings.json + +.DS_Store diff --git a/Directory.Packages.props b/Directory.Packages.props index 1cfb7df4..acec9dbf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,6 +20,7 @@ + diff --git a/grate.sln b/grate.sln index b1c98c88..d00c30ff 100644 --- a/grate.sln +++ b/grate.sln @@ -43,6 +43,23 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "grate.core", "src\grate.cor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "grate", "src\grate\grate.csproj", "{9D3377C2-1E3C-40C9-A5F1-DF41DFDFDD9D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Command line tests", "Command line tests", "{04731BDA-05F0-4275-AC70-3E60E87F9461}" + ProjectSection(SolutionItems) = preProject + unittests\CommandLine\IncludeTests.targets = unittests\CommandLine\IncludeTests.targets + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine.Common", "unittests\CommandLine\CommandLine.Common\CommandLine.Common.csproj", "{DB220C5D-7B52-4389-B568-793FCD4286AA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine.MariaDB", "unittests\CommandLine\CommandLine.MariaDB\CommandLine.MariaDB.csproj", "{088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine.Sqlite", "unittests\CommandLine\CommandLine.Sqlite\CommandLine.Sqlite.csproj", "{ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine.Oracle", "unittests\CommandLine\CommandLine.Oracle\CommandLine.Oracle.csproj", "{BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine.PostgreSQL", "unittests\CommandLine\CommandLine.PostgreSQL\CommandLine.PostgreSQL.csproj", "{89CBE842-BD34-4833-96B4-9E410C5C6AD7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine.SqlServer", "unittests\CommandLine\CommandLine.SqlServer\CommandLine.SqlServer.csproj", "{3CD81B8C-9348-4F45-9E50-12713D47B1FB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -233,6 +250,78 @@ Global {9D3377C2-1E3C-40C9-A5F1-DF41DFDFDD9D}.Release|x64.Build.0 = Release|Any CPU {9D3377C2-1E3C-40C9-A5F1-DF41DFDFDD9D}.Release|x86.ActiveCfg = Release|Any CPU {9D3377C2-1E3C-40C9-A5F1-DF41DFDFDD9D}.Release|x86.Build.0 = Release|Any CPU + {DB220C5D-7B52-4389-B568-793FCD4286AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB220C5D-7B52-4389-B568-793FCD4286AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB220C5D-7B52-4389-B568-793FCD4286AA}.Debug|x64.ActiveCfg = Debug|Any CPU + {DB220C5D-7B52-4389-B568-793FCD4286AA}.Debug|x64.Build.0 = Debug|Any CPU + {DB220C5D-7B52-4389-B568-793FCD4286AA}.Debug|x86.ActiveCfg = Debug|Any CPU + {DB220C5D-7B52-4389-B568-793FCD4286AA}.Debug|x86.Build.0 = Debug|Any CPU + {DB220C5D-7B52-4389-B568-793FCD4286AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB220C5D-7B52-4389-B568-793FCD4286AA}.Release|Any CPU.Build.0 = Release|Any CPU + {DB220C5D-7B52-4389-B568-793FCD4286AA}.Release|x64.ActiveCfg = Release|Any CPU + {DB220C5D-7B52-4389-B568-793FCD4286AA}.Release|x64.Build.0 = Release|Any CPU + {DB220C5D-7B52-4389-B568-793FCD4286AA}.Release|x86.ActiveCfg = Release|Any CPU + {DB220C5D-7B52-4389-B568-793FCD4286AA}.Release|x86.Build.0 = Release|Any CPU + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}.Debug|x64.ActiveCfg = Debug|Any CPU + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}.Debug|x64.Build.0 = Debug|Any CPU + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}.Debug|x86.ActiveCfg = Debug|Any CPU + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}.Debug|x86.Build.0 = Debug|Any CPU + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}.Release|Any CPU.Build.0 = Release|Any CPU + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}.Release|x64.ActiveCfg = Release|Any CPU + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}.Release|x64.Build.0 = Release|Any CPU + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}.Release|x86.ActiveCfg = Release|Any CPU + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE}.Release|x86.Build.0 = Release|Any CPU + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}.Debug|x64.ActiveCfg = Debug|Any CPU + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}.Debug|x64.Build.0 = Debug|Any CPU + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}.Debug|x86.ActiveCfg = Debug|Any CPU + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}.Debug|x86.Build.0 = Debug|Any CPU + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}.Release|Any CPU.Build.0 = Release|Any CPU + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}.Release|x64.ActiveCfg = Release|Any CPU + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}.Release|x64.Build.0 = Release|Any CPU + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}.Release|x86.ActiveCfg = Release|Any CPU + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B}.Release|x86.Build.0 = Release|Any CPU + {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}.Debug|x64.ActiveCfg = Debug|Any CPU + {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}.Debug|x64.Build.0 = Debug|Any CPU + {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}.Debug|x86.ActiveCfg = Debug|Any CPU + {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}.Debug|x86.Build.0 = Debug|Any CPU + {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}.Release|Any CPU.Build.0 = Release|Any CPU + {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}.Release|x64.ActiveCfg = Release|Any CPU + {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}.Release|x64.Build.0 = Release|Any CPU + {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}.Release|x86.ActiveCfg = Release|Any CPU + {BD31AD42-D1A0-4131-9DD5-7DA83F3F34A8}.Release|x86.Build.0 = Release|Any CPU + {89CBE842-BD34-4833-96B4-9E410C5C6AD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89CBE842-BD34-4833-96B4-9E410C5C6AD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89CBE842-BD34-4833-96B4-9E410C5C6AD7}.Debug|x64.ActiveCfg = Debug|Any CPU + {89CBE842-BD34-4833-96B4-9E410C5C6AD7}.Debug|x64.Build.0 = Debug|Any CPU + {89CBE842-BD34-4833-96B4-9E410C5C6AD7}.Debug|x86.ActiveCfg = Debug|Any CPU + {89CBE842-BD34-4833-96B4-9E410C5C6AD7}.Debug|x86.Build.0 = Debug|Any CPU + {89CBE842-BD34-4833-96B4-9E410C5C6AD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89CBE842-BD34-4833-96B4-9E410C5C6AD7}.Release|Any CPU.Build.0 = Release|Any CPU + {89CBE842-BD34-4833-96B4-9E410C5C6AD7}.Release|x64.ActiveCfg = Release|Any CPU + {89CBE842-BD34-4833-96B4-9E410C5C6AD7}.Release|x64.Build.0 = Release|Any CPU + {89CBE842-BD34-4833-96B4-9E410C5C6AD7}.Release|x86.ActiveCfg = Release|Any CPU + {89CBE842-BD34-4833-96B4-9E410C5C6AD7}.Release|x86.Build.0 = Release|Any CPU + {3CD81B8C-9348-4F45-9E50-12713D47B1FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CD81B8C-9348-4F45-9E50-12713D47B1FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CD81B8C-9348-4F45-9E50-12713D47B1FB}.Debug|x64.ActiveCfg = Debug|Any CPU + {3CD81B8C-9348-4F45-9E50-12713D47B1FB}.Debug|x64.Build.0 = Debug|Any CPU + {3CD81B8C-9348-4F45-9E50-12713D47B1FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {3CD81B8C-9348-4F45-9E50-12713D47B1FB}.Debug|x86.Build.0 = Debug|Any CPU + {3CD81B8C-9348-4F45-9E50-12713D47B1FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CD81B8C-9348-4F45-9E50-12713D47B1FB}.Release|Any CPU.Build.0 = Release|Any CPU + {3CD81B8C-9348-4F45-9E50-12713D47B1FB}.Release|x64.ActiveCfg = Release|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -256,5 +345,12 @@ Global {69DE6B9D-F937-4AF4-A75E-40F5507B4C83} = {34C2458F-1244-4AF5-B5B5-B349CAEB17A5} {3BAF5373-6009-405B-AC9D-C8FFFB263E6B} = {34C2458F-1244-4AF5-B5B5-B349CAEB17A5} {9D3377C2-1E3C-40C9-A5F1-DF41DFDFDD9D} = {34C2458F-1244-4AF5-B5B5-B349CAEB17A5} + {04731BDA-05F0-4275-AC70-3E60E87F9461} = {CE4BF96D-E795-49EC-BB36-EDCC7E542685} + {DB220C5D-7B52-4389-B568-793FCD4286AA} = {04731BDA-05F0-4275-AC70-3E60E87F9461} + {088D2F44-26AE-4DCE-9668-8B8A38EDD5EE} = {04731BDA-05F0-4275-AC70-3E60E87F9461} + {ABFC5823-ACCF-4CFC-9EDA-90F7DD8A930B} = {04731BDA-05F0-4275-AC70-3E60E87F9461} + {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} EndGlobalSection EndGlobal diff --git a/src/grate.core/Configuration/FoldersConfiguration.cs b/src/grate.core/Configuration/FoldersConfiguration.cs index c7be5f58..598427a8 100644 --- a/src/grate.core/Configuration/FoldersConfiguration.cs +++ b/src/grate.core/Configuration/FoldersConfiguration.cs @@ -26,6 +26,8 @@ public FoldersConfiguration() public MigrationsFolder? DropDatabase { get; set; } public static FoldersConfiguration Empty => new(); + + public override string ToString() => string.Join(';', Values); public static IFoldersConfiguration Default(IKnownFolderNames? folderNames = null) { diff --git a/src/grate.core/Configuration/MigrationsFolder.cs b/src/grate.core/Configuration/MigrationsFolder.cs index a1e071df..1821377e 100644 --- a/src/grate.core/Configuration/MigrationsFolder.cs +++ b/src/grate.core/Configuration/MigrationsFolder.cs @@ -27,4 +27,6 @@ public MigrationsFolder( : this(name, name, type, connectionType, transactionHandling) { } + public override string ToString() => + $"{Name}=path:{Path},type:{Type},connectionType:{ConnectionType},transactionHandling:{TransactionHandling}"; } diff --git a/src/grate.core/Infrastructure/GrateEnvironment.cs b/src/grate.core/Infrastructure/GrateEnvironment.cs index d28cb796..acb16f51 100644 --- a/src/grate.core/Infrastructure/GrateEnvironment.cs +++ b/src/grate.core/Infrastructure/GrateEnvironment.cs @@ -20,4 +20,5 @@ private bool IsForCurrentEnvironment(string path) => public static bool IsEnvironmentFile(string fileName) => fileName.Contains(EnvironmentMarker, InvariantCultureIgnoreCase); private static string FileName(string path) => new FileInfo(path).Name; + public override string ToString() => Current; } diff --git a/src/grate.core/grate.core.csproj b/src/grate.core/grate.core.csproj index 29ce0d8b..270d1000 100644 --- a/src/grate.core/grate.core.csproj +++ b/src/grate.core/grate.core.csproj @@ -17,6 +17,11 @@ + + + + + diff --git a/src/grate/grate.csproj b/src/grate/grate.csproj index f60b09de..ca42d5c9 100644 --- a/src/grate/grate.csproj +++ b/src/grate/grate.csproj @@ -9,6 +9,12 @@ + + + + + + diff --git a/unittests/CommandLine/CommandLine.Common/CommandLine.Common.csproj b/unittests/CommandLine/CommandLine.Common/CommandLine.Common.csproj new file mode 100644 index 00000000..9347f583 --- /dev/null +++ b/unittests/CommandLine/CommandLine.Common/CommandLine.Common.csproj @@ -0,0 +1,40 @@ + + + + net8.0 + enable + enable + + false + false + + CommandLine.Common.Startup + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + <_Parameter1>$(MSBuildProjectDirectory)/$(OutputPath)/grate + + + + diff --git a/unittests/CommandLine/CommandLine.Common/GrateExecutablePathAttribute.cs b/unittests/CommandLine/CommandLine.Common/GrateExecutablePathAttribute.cs new file mode 100644 index 00000000..761db8c9 --- /dev/null +++ b/unittests/CommandLine/CommandLine.Common/GrateExecutablePathAttribute.cs @@ -0,0 +1,8 @@ +namespace CommandLine.Common; + +[AttributeUsage(AttributeTargets.Assembly)] +public class GrateExecutablePathAttribute(string path) : Attribute +{ + public string Path { get; set; } = path; +} + diff --git a/unittests/CommandLine/CommandLine.Common/Startup.cs b/unittests/CommandLine/CommandLine.Common/Startup.cs new file mode 100644 index 00000000..89369daf --- /dev/null +++ b/unittests/CommandLine/CommandLine.Common/Startup.cs @@ -0,0 +1,40 @@ +using CommandLine.Common.TestInfrastructure; +using grate.Configuration; +using grate.Migration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using TestCommon.TestInfrastructure; + +namespace CommandLine.Common; + +// ReSharper disable once UnusedType.Global +public abstract class Startup +{ + // ReSharper disable once UnusedMember.Global + public void ConfigureServices(IServiceCollection services, HostBuilderContext context) + { + services.AddLogging( + lb => lb + .AddXUnit() + .AddConsole() + .SetMinimumLevel(TestConfig.GetLogLevel()) + ); + + services.AddSingleton(new CommandLineGrateMigrator(DatabaseType)); + + services.TryAddSingleton(TestContextType); + services.TryAddSingleton(TestContainerType); + + services.TryAddTransient(provider => + new CommandLineGrateTestContext( + provider.GetRequiredService(), + (IGrateTestContext) provider.GetRequiredService(TestContextType))); + } + + protected abstract DatabaseType DatabaseType { get; } + protected abstract Type TestContainerType { get; } + protected abstract Type TestContextType { get; } + +} diff --git a/unittests/CommandLine/CommandLine.Common/TestInfrastructure/CommandLineGrateMigrator.cs b/unittests/CommandLine/CommandLine.Common/TestInfrastructure/CommandLineGrateMigrator.cs new file mode 100644 index 00000000..4ed7c097 --- /dev/null +++ b/unittests/CommandLine/CommandLine.Common/TestInfrastructure/CommandLineGrateMigrator.cs @@ -0,0 +1,136 @@ +using System.Diagnostics; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using grate.Commands; +using grate.Configuration; +using grate.Exceptions; +using grate.Migration; + +namespace CommandLine.Common.TestInfrastructure; + +public record CommandLineGrateMigrator(DatabaseType DatabaseType) : IGrateMigrator +{ + public DatabaseType DatabaseType { get; set; } = DatabaseType; + + public async Task Migrate() + { + // Convert configuration to command-line arguments + var convertToCommandLineArguments = ConvertToCommandLineArguments(Configuration); + var commandLineArguments = convertToCommandLineArguments.ToList(); + + // Add the database type + commandLineArguments.Add("--databasetype=" + DatabaseType.ToString().ToLowerInvariant()); + + // Run the command-line tool with the arguments + var processInfo = new ProcessStartInfo(GrateExecutable, commandLineArguments) + { + RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, + }; + + using var process = Process.Start(processInfo); + await process!.WaitForExitAsync(); + + var output = await process.StandardOutput.ReadToEndAsync(); + var error = await process.StandardError.ReadToEndAsync(); + + var exitCode = process.ExitCode; + if (exitCode != 0) + { + try + { + throw new Exception($"grate failed with exit code {exitCode}. Output: {output}. Error: {error}"); + }catch (Exception e) + { + throw new MigrationFailed(e); + } + } + + } + + public GrateConfiguration Configuration { get; private init; } = null!; + + public IGrateMigrator WithConfiguration(GrateConfiguration configuration) + { + return this with { Configuration = configuration }; + } + + public IGrateMigrator WithConfiguration(Action builder) + { + var b = GrateConfigurationBuilder.Create(Configuration); + builder.Invoke(b); + return this with { Configuration = b.Build() }; + } + + public IGrateMigrator WithDatabase(IDatabase database) => this with { Database = database }; + public IDatabase? Database { get; set; } + + + private List ConvertToCommandLineArguments(GrateConfiguration configuration) + { + List result = new(); + + var properties = configuration.GetType().GetProperties(); + + var cmd = new MigrateCommand(this); + + 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 + }; + + private static string GrateExecutable => + Environment.GetEnvironmentVariable("GrateExecutablePath") ?? + typeof(CommandLineGrateMigrator).Assembly.GetCustomAttribute()?.Path + ?? throw new Exception("Grate executable path not found"); + + + public ValueTask DisposeAsync() + { + return new ValueTask(Task.CompletedTask); + } + +} diff --git a/unittests/CommandLine/CommandLine.Common/TestInfrastructure/CommandLineTestFramework.cs b/unittests/CommandLine/CommandLine.Common/TestInfrastructure/CommandLineTestFramework.cs new file mode 100644 index 00000000..b900c537 --- /dev/null +++ b/unittests/CommandLine/CommandLine.Common/TestInfrastructure/CommandLineTestFramework.cs @@ -0,0 +1,48 @@ +// using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +// using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +// using Xunit.Abstractions; +// using Xunit.Sdk; +// +// namespace CommandLine_tests.TestInfrastructure; +// +// public class CommandLineTestFramework : XunitTestFramework +// { +// public CommandLineTestFramework(IMessageSink messageSink) +// : base(messageSink) +// { +// } +// +// protected override ITestFrameworkDiscoverer CreateDiscoverer( +// IAssemblyInfo assemblyInfo) +// => new CommandLineTestFrameworkDiscoverer( +// assemblyInfo, +// SourceInformationProvider, +// DiagnosticMessageSink); +// } +// public class CommandLineTestFrameworkDiscoverer : XunitTestFrameworkDiscoverer +// { +// public CommandLineTestFrameworkDiscoverer( +// IAssemblyInfo assemblyInfo, +// ISourceInformationProvider sourceProvider, +// IMessageSink diagnosticMessageSink, +// IXunitTestCollectionFactory? collectionFactory = null) +// : base( +// assemblyInfo, +// sourceProvider, +// diagnosticMessageSink, +// collectionFactory) +// { +// } +// +// protected override bool IsValidTestClass(ITypeInfo type) +// => base.IsValidTestClass(type) && +// FilterType(type); +// +// protected virtual bool FilterType(ITypeInfo type) +// { +// // Insert your custom filter conditions here. +// return true; +// } +// } +// +// diff --git a/unittests/CommandLine/CommandLine.Common/TestInfrastructure/CommandlineGrateTestContext.cs b/unittests/CommandLine/CommandLine.Common/TestInfrastructure/CommandlineGrateTestContext.cs new file mode 100644 index 00000000..88755037 --- /dev/null +++ b/unittests/CommandLine/CommandLine.Common/TestInfrastructure/CommandlineGrateTestContext.cs @@ -0,0 +1,39 @@ +using System.Data; +using grate.Infrastructure; +using grate.Migration; +using TestCommon.TestInfrastructure; + +namespace CommandLine.Common.TestInfrastructure; + +public class CommandLineGrateTestContext : IGrateTestContext +{ + private readonly IGrateTestContext _testContext; + + public CommandLineGrateTestContext( + IGrateMigrator migrator, + IGrateTestContext testContext) + { + _testContext = testContext; + Migrator = migrator; + } + + public IGrateMigrator Migrator { get; } + + public string AdminConnectionString => _testContext.AdminConnectionString; + public string ConnectionString(string database) => _testContext.ConnectionString(database); + public string UserConnectionString(string database) => _testContext.UserConnectionString(database); + + public IDbConnection GetDbConnection(string connectionString) => _testContext.GetDbConnection(connectionString); + + public ISyntax Syntax => _testContext.Syntax; + public Type DbExceptionType => _testContext.DbExceptionType; + + public Type DatabaseType => _testContext.DbExceptionType; + public bool SupportsTransaction => _testContext.SupportsTransaction; + + public SqlStatements Sql => _testContext.Sql; + + public string ExpectedVersionPrefix => _testContext.ExpectedVersionPrefix; + public bool SupportsCreateDatabase => _testContext.SupportsCreateDatabase; + public bool SupportsSchemas => _testContext.SupportsSchemas; +} diff --git a/unittests/CommandLine/CommandLine.MariaDB/CommandLine.MariaDB.csproj b/unittests/CommandLine/CommandLine.MariaDB/CommandLine.MariaDB.csproj new file mode 100644 index 00000000..92070270 --- /dev/null +++ b/unittests/CommandLine/CommandLine.MariaDB/CommandLine.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/CommandLine/CommandLine.MariaDB/Running_MigrationScripts/Failing_Scripts.cs b/unittests/CommandLine/CommandLine.MariaDB/Running_MigrationScripts/Failing_Scripts.cs new file mode 100644 index 00000000..97400975 --- /dev/null +++ b/unittests/CommandLine/CommandLine.MariaDB/Running_MigrationScripts/Failing_Scripts.cs @@ -0,0 +1,24 @@ +using MariaDB.TestInfrastructure; +using TestCommon.TestInfrastructure; + +namespace CommandLine.MariaDB.Running_MigrationScripts; + +[Collection(nameof(MariaDbTestContainer))] +// ReSharper disable once InconsistentNaming +public class Failing_Scripts(IGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.Failing_Scripts(testContext, testOutput) +{ + protected override string ExpectedErrorMessageForInvalidSql => "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/CommandLine/CommandLine.MariaDB/Running_MigrationScripts/One_time_scripts.cs b/unittests/CommandLine/CommandLine.MariaDB/Running_MigrationScripts/One_time_scripts.cs new file mode 100644 index 00000000..7da55a14 --- /dev/null +++ b/unittests/CommandLine/CommandLine.MariaDB/Running_MigrationScripts/One_time_scripts.cs @@ -0,0 +1,58 @@ +using Dapper; +using FluentAssertions; +using grate.Configuration; +using grate.Exceptions; +using MariaDB.TestInfrastructure; +using TestCommon.TestInfrastructure; +using static grate.Configuration.KnownFolderKeys; + +namespace CommandLine.MariaDB.Running_MigrationScripts; + +[Collection(nameof(MariaDbTestContainer))] +// ReSharper disable once InconsistentNaming +public class One_time_scripts(IGrateTestContext 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 = FoldersConfiguration.Default(null); + 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); + } + +} + + diff --git a/unittests/CommandLine/CommandLine.MariaDB/Startup.cs b/unittests/CommandLine/CommandLine.MariaDB/Startup.cs new file mode 100644 index 00000000..197353f7 --- /dev/null +++ b/unittests/CommandLine/CommandLine.MariaDB/Startup.cs @@ -0,0 +1,12 @@ +using grate.Configuration; +using MariaDB.TestInfrastructure; + +namespace CommandLine.MariaDB; + +// ReSharper disable once UnusedType.Global +public class Startup: CommandLine.Common.Startup +{ + protected override DatabaseType DatabaseType => DatabaseType.MariaDB; + protected override Type TestContainerType => typeof(MariaDbTestContainer); + protected override Type TestContextType => typeof(MariaDbGrateTestContext); +} diff --git a/unittests/CommandLine/CommandLine.Oracle/CommandLine.Oracle.csproj b/unittests/CommandLine/CommandLine.Oracle/CommandLine.Oracle.csproj new file mode 100644 index 00000000..b4e5ecb9 --- /dev/null +++ b/unittests/CommandLine/CommandLine.Oracle/CommandLine.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/CommandLine/CommandLine.Oracle/Running_MigrationScripts/Failing_Scripts.cs b/unittests/CommandLine/CommandLine.Oracle/Running_MigrationScripts/Failing_Scripts.cs new file mode 100644 index 00000000..0f593728 --- /dev/null +++ b/unittests/CommandLine/CommandLine.Oracle/Running_MigrationScripts/Failing_Scripts.cs @@ -0,0 +1,23 @@ +using TestCommon.TestInfrastructure; + +namespace CommandLine.Oracle.Running_MigrationScripts; + +[Collection(nameof(OracleTestContainer))] +// ReSharper disable once InconsistentNaming +public class Failing_Scripts(IGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.Failing_Scripts(testContext, testOutput) +{ + protected override string ExpectedErrorMessageForInvalidSql => "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/CommandLine/CommandLine.Oracle/Running_MigrationScripts/One_time_scripts.cs b/unittests/CommandLine/CommandLine.Oracle/Running_MigrationScripts/One_time_scripts.cs new file mode 100644 index 00000000..899cbbdf --- /dev/null +++ b/unittests/CommandLine/CommandLine.Oracle/Running_MigrationScripts/One_time_scripts.cs @@ -0,0 +1,59 @@ +using Dapper; +using FluentAssertions; +using grate.Configuration; +using grate.Exceptions; +using TestCommon.TestInfrastructure; +using static grate.Configuration.KnownFolderKeys; + +namespace CommandLine.Oracle.Running_MigrationScripts; + +[Collection(nameof(OracleTestContainer))] +// ReSharper disable once InconsistentNaming +public class One_time_scripts(IGrateTestContext 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 = FoldersConfiguration.Default(null); + 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); + } + +} + + diff --git a/unittests/CommandLine/CommandLine.Oracle/Startup.cs b/unittests/CommandLine/CommandLine.Oracle/Startup.cs new file mode 100644 index 00000000..249b3c37 --- /dev/null +++ b/unittests/CommandLine/CommandLine.Oracle/Startup.cs @@ -0,0 +1,13 @@ +using grate.Configuration; +using Oracle.TestInfrastructure; +using TestCommon.TestInfrastructure; + +namespace CommandLine.Oracle; + +// ReSharper disable once UnusedType.Global +public class Startup: CommandLine.Common.Startup +{ + protected override DatabaseType DatabaseType => DatabaseType.Oracle; + protected override Type TestContainerType => typeof(OracleTestContainer); + protected override Type TestContextType => typeof(OracleGrateTestContext); +} diff --git a/unittests/CommandLine/CommandLine.PostgreSQL/CommandLine.PostgreSQL.csproj b/unittests/CommandLine/CommandLine.PostgreSQL/CommandLine.PostgreSQL.csproj new file mode 100644 index 00000000..e524c1a5 --- /dev/null +++ b/unittests/CommandLine/CommandLine.PostgreSQL/CommandLine.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/CommandLine/CommandLine.PostgreSQL/Running_MigrationScripts/Failing_Scripts.cs b/unittests/CommandLine/CommandLine.PostgreSQL/Running_MigrationScripts/Failing_Scripts.cs new file mode 100644 index 00000000..e06cdcde --- /dev/null +++ b/unittests/CommandLine/CommandLine.PostgreSQL/Running_MigrationScripts/Failing_Scripts.cs @@ -0,0 +1,23 @@ +using TestCommon.TestInfrastructure; + +namespace CommandLine.PostgreSQL.Running_MigrationScripts; + +[Collection(nameof(PostgreSqlTestContainer))] +// ReSharper disable once InconsistentNaming +public class Failing_Scripts(IGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.Failing_Scripts(testContext, testOutput) +{ + protected override string ExpectedErrorMessageForInvalidSql => "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/CommandLine/CommandLine.PostgreSQL/Running_MigrationScripts/One_time_scripts.cs b/unittests/CommandLine/CommandLine.PostgreSQL/Running_MigrationScripts/One_time_scripts.cs new file mode 100644 index 00000000..9e7f9f8a --- /dev/null +++ b/unittests/CommandLine/CommandLine.PostgreSQL/Running_MigrationScripts/One_time_scripts.cs @@ -0,0 +1,56 @@ +using Dapper; +using FluentAssertions; +using grate.Configuration; +using grate.Exceptions; +using TestCommon.TestInfrastructure; +using static grate.Configuration.KnownFolderKeys; + +namespace CommandLine.PostgreSQL.Running_MigrationScripts; + +[Collection(nameof(PostgreSqlTestContainer))] +// ReSharper disable once InconsistentNaming +public class One_time_scripts(IGrateTestContext 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 = FoldersConfiguration.Default(null); + 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); + } + +} + + diff --git a/unittests/CommandLine/CommandLine.PostgreSQL/Startup.cs b/unittests/CommandLine/CommandLine.PostgreSQL/Startup.cs new file mode 100644 index 00000000..bd2731ab --- /dev/null +++ b/unittests/CommandLine/CommandLine.PostgreSQL/Startup.cs @@ -0,0 +1,13 @@ +using grate.Configuration; +using PostgreSQL.TestInfrastructure; +using TestCommon.TestInfrastructure; + +namespace CommandLine.PostgreSQL; + +// ReSharper disable once UnusedType.Global +public class Startup: CommandLine.Common.Startup +{ + protected override DatabaseType DatabaseType => DatabaseType.PostgreSQL; + protected override Type TestContainerType => typeof(PostgreSqlTestContainer); + protected override Type TestContextType => typeof(PostgreSqlGrateTestContext); +} diff --git a/unittests/CommandLine/CommandLine.SqlServer/CommandLine.SqlServer.csproj b/unittests/CommandLine/CommandLine.SqlServer/CommandLine.SqlServer.csproj new file mode 100644 index 00000000..fe1ef6bf --- /dev/null +++ b/unittests/CommandLine/CommandLine.SqlServer/CommandLine.SqlServer.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 + + + + + + + + + + + ../../SqlServer + + + + diff --git a/unittests/CommandLine/CommandLine.SqlServer/Running_MigrationScripts/Failing_Scripts.cs b/unittests/CommandLine/CommandLine.SqlServer/Running_MigrationScripts/Failing_Scripts.cs new file mode 100644 index 00000000..23b10507 --- /dev/null +++ b/unittests/CommandLine/CommandLine.SqlServer/Running_MigrationScripts/Failing_Scripts.cs @@ -0,0 +1,23 @@ +using TestCommon.TestInfrastructure; + +namespace CommandLine.SqlServer.Running_MigrationScripts; + +[Collection(nameof(SqlServerTestContainer))] +// ReSharper disable once InconsistentNaming +public class Failing_Scripts(IGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.Failing_Scripts(testContext, testOutput) +{ + protected override string ExpectedErrorMessageForInvalidSql => "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/CommandLine/CommandLine.SqlServer/Running_MigrationScripts/One_time_scripts.cs b/unittests/CommandLine/CommandLine.SqlServer/Running_MigrationScripts/One_time_scripts.cs new file mode 100644 index 00000000..0dbdb2f9 --- /dev/null +++ b/unittests/CommandLine/CommandLine.SqlServer/Running_MigrationScripts/One_time_scripts.cs @@ -0,0 +1,56 @@ +using Dapper; +using FluentAssertions; +using grate.Configuration; +using grate.Exceptions; +using TestCommon.TestInfrastructure; +using static grate.Configuration.KnownFolderKeys; + +namespace CommandLine.SqlServer.Running_MigrationScripts; + +[Collection(nameof(SqlServerTestContainer))] +// ReSharper disable once InconsistentNaming +public class One_time_scripts(IGrateTestContext 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 = FoldersConfiguration.Default(null); + 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); + } + +} + + diff --git a/unittests/CommandLine/CommandLine.SqlServer/Startup.cs b/unittests/CommandLine/CommandLine.SqlServer/Startup.cs new file mode 100644 index 00000000..c6e7d870 --- /dev/null +++ b/unittests/CommandLine/CommandLine.SqlServer/Startup.cs @@ -0,0 +1,13 @@ +using grate.Configuration; +using SqlServer.TestInfrastructure; +using TestCommon.TestInfrastructure; + +namespace CommandLine.SqlServer; + +// ReSharper disable once UnusedType.Global +public class Startup: CommandLine.Common.Startup +{ + protected override DatabaseType DatabaseType => DatabaseType.SQLServer; + protected override Type TestContainerType => typeof(SqlServerTestContainer); + protected override Type TestContextType => typeof(SqlServerGrateTestContext); +} diff --git a/unittests/CommandLine/CommandLine.Sqlite/CommandLine.Sqlite.csproj b/unittests/CommandLine/CommandLine.Sqlite/CommandLine.Sqlite.csproj new file mode 100644 index 00000000..3550dc0d --- /dev/null +++ b/unittests/CommandLine/CommandLine.Sqlite/CommandLine.Sqlite.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 + + + + + + + + + + + ../../Sqlite + + + + diff --git a/unittests/CommandLine/CommandLine.Sqlite/Running_MigrationScripts/Failing_Scripts.cs b/unittests/CommandLine/CommandLine.Sqlite/Running_MigrationScripts/Failing_Scripts.cs new file mode 100644 index 00000000..536d9bc0 --- /dev/null +++ b/unittests/CommandLine/CommandLine.Sqlite/Running_MigrationScripts/Failing_Scripts.cs @@ -0,0 +1,23 @@ +using TestCommon.TestInfrastructure; + +namespace CommandLine.Sqlite.Running_MigrationScripts; + +[Collection(nameof(SqliteTestContainer))] +// ReSharper disable once InconsistentNaming +public class Failing_Scripts(IGrateTestContext testContext, ITestOutputHelper testOutput) + : TestCommon.Generic.Running_MigrationScripts.Failing_Scripts(testContext, testOutput) +{ + protected override string ExpectedErrorMessageForInvalidSql => "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/CommandLine/CommandLine.Sqlite/Running_MigrationScripts/One_time_scripts.cs b/unittests/CommandLine/CommandLine.Sqlite/Running_MigrationScripts/One_time_scripts.cs new file mode 100644 index 00000000..408f4553 --- /dev/null +++ b/unittests/CommandLine/CommandLine.Sqlite/Running_MigrationScripts/One_time_scripts.cs @@ -0,0 +1,56 @@ +using Dapper; +using FluentAssertions; +using grate.Configuration; +using grate.Exceptions; +using TestCommon.TestInfrastructure; +using static grate.Configuration.KnownFolderKeys; + +namespace CommandLine.Sqlite.Running_MigrationScripts; + +[Collection(nameof(SqliteTestContainer))] +// ReSharper disable once InconsistentNaming +public class One_time_scripts(IGrateTestContext 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 = FoldersConfiguration.Default(null); + 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); + } + +} + + diff --git a/unittests/CommandLine/CommandLine.Sqlite/Startup.cs b/unittests/CommandLine/CommandLine.Sqlite/Startup.cs new file mode 100644 index 00000000..cc2d7a7f --- /dev/null +++ b/unittests/CommandLine/CommandLine.Sqlite/Startup.cs @@ -0,0 +1,13 @@ +using grate.Configuration; +using Sqlite.TestInfrastructure; +using TestCommon.TestInfrastructure; + +namespace CommandLine.Sqlite; + +// ReSharper disable once UnusedType.Global +public class Startup: CommandLine.Common.Startup +{ + protected override DatabaseType DatabaseType => DatabaseType.SQLite; + protected override Type TestContainerType => typeof(SqliteTestContainer); + protected override Type TestContextType => typeof(SqliteGrateTestContext); +} diff --git a/unittests/CommandLine/IncludeTests.targets b/unittests/CommandLine/IncludeTests.targets new file mode 100644 index 00000000..4d217022 --- /dev/null +++ b/unittests/CommandLine/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 a89ba5c2..66817db5 100644 --- a/unittests/MariaDB/TestInfrastructure/MariaDbGrateTestContext.cs +++ b/unittests/MariaDB/TestInfrastructure/MariaDbGrateTestContext.cs @@ -55,4 +55,5 @@ public MariaDbGrateTestContext( public string ExpectedVersionPrefix => "10.10.7-MariaDB"; public bool SupportsCreateDatabase => true; + public bool SupportsSchemas => false; } diff --git a/unittests/Oracle/TestInfrastructure/OracleGrateTestContext.cs b/unittests/Oracle/TestInfrastructure/OracleGrateTestContext.cs index 100d5f74..2ac7bc91 100644 --- a/unittests/Oracle/TestInfrastructure/OracleGrateTestContext.cs +++ b/unittests/Oracle/TestInfrastructure/OracleGrateTestContext.cs @@ -50,4 +50,5 @@ public OracleGrateTestContext( public string ExpectedVersionPrefix => "Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production"; public bool SupportsCreateDatabase => true; + public bool SupportsSchemas => false; } diff --git a/unittests/PostgreSQL/TestInfrastructure/PostgreSqlGrateTestContext.cs b/unittests/PostgreSQL/TestInfrastructure/PostgreSqlGrateTestContext.cs index 8405ff20..298e7bcb 100644 --- a/unittests/PostgreSQL/TestInfrastructure/PostgreSqlGrateTestContext.cs +++ b/unittests/PostgreSQL/TestInfrastructure/PostgreSqlGrateTestContext.cs @@ -53,5 +53,6 @@ public string UserConnectionString(string database) => public string ExpectedVersionPrefix => "PostgreSQL 16."; public bool SupportsCreateDatabase => true; + public bool SupportsSchemas => true; } diff --git a/unittests/SqlServer/TestInfrastructure/SqlServerGrateTestContext.cs b/unittests/SqlServer/TestInfrastructure/SqlServerGrateTestContext.cs index a2179be0..f490084f 100644 --- a/unittests/SqlServer/TestInfrastructure/SqlServerGrateTestContext.cs +++ b/unittests/SqlServer/TestInfrastructure/SqlServerGrateTestContext.cs @@ -71,6 +71,7 @@ public string UserConnectionString(string database) => public string ExpectedVersionPrefix => "Microsoft SQL Server 2019"; public bool SupportsCreateDatabase => true; + public bool SupportsSchemas => true; public string ServerCollation { get; } } diff --git a/unittests/SqlServerCaseSensitive/TestInfrastructure/SqlServerGrateTestContext.cs b/unittests/SqlServerCaseSensitive/TestInfrastructure/SqlServerGrateTestContext.cs index 4631007d..c0891cde 100644 --- a/unittests/SqlServerCaseSensitive/TestInfrastructure/SqlServerGrateTestContext.cs +++ b/unittests/SqlServerCaseSensitive/TestInfrastructure/SqlServerGrateTestContext.cs @@ -53,6 +53,7 @@ public SqlServerGrateTestContext( public string ExpectedVersionPrefix => "Microsoft SQL Server 2019"; public bool SupportsCreateDatabase => true; + public bool SupportsSchemas => true; public string ServerCollation { get; } } diff --git a/unittests/Sqlite/TestInfrastructure/SqliteGrateTestContext.cs b/unittests/Sqlite/TestInfrastructure/SqliteGrateTestContext.cs index b4b644b5..4287fa73 100644 --- a/unittests/Sqlite/TestInfrastructure/SqliteGrateTestContext.cs +++ b/unittests/Sqlite/TestInfrastructure/SqliteGrateTestContext.cs @@ -44,6 +44,7 @@ public SqliteGrateTestContext( public string ExpectedVersionPrefix => throw new NotSupportedException("Sqlite does not support versioning"); public bool SupportsCreateDatabase => false; + public bool SupportsSchemas => false; public IGrateMigrator Migrator { get; } } diff --git a/unittests/TestCommon/Generic/Running_MigrationScripts/Failing_Scripts.cs b/unittests/TestCommon/Generic/Running_MigrationScripts/Failing_Scripts.cs index d4501dc4..5115c3a3 100644 --- a/unittests/TestCommon/Generic/Running_MigrationScripts/Failing_Scripts.cs +++ b/unittests/TestCommon/Generic/Running_MigrationScripts/Failing_Scripts.cs @@ -26,7 +26,7 @@ protected Failing_Scripts(): this(null!, null!) protected abstract string ExpectedErrorMessageForInvalidSql { get; } [Fact] - public async Task Aborts_the_run_giving_an_error_message() + public virtual async Task Aborts_the_run_giving_an_error_message() { var db = TestConfig.RandomDatabase(); @@ -47,7 +47,7 @@ public async Task Aborts_the_run_giving_an_error_message() [Fact] - public async Task Exception_includes_details_on_the_failed_script() + public virtual async Task Exception_includes_details_on_the_failed_script() { var db = TestConfig.RandomDatabase(); @@ -84,7 +84,7 @@ public async Task Exception_includes_details_on_the_failed_script() // TODO: Improve this test to throw both transient and non-transient exceptions, and check the result [Fact] - public async Task Exception_is_set_to_transient_based_on_inner_exceptions() + public virtual async Task Exception_is_set_to_transient_based_on_inner_exceptions() { var db = TestConfig.RandomDatabase(); diff --git a/unittests/TestCommon/Generic/Running_MigrationScripts/One_time_scripts.cs b/unittests/TestCommon/Generic/Running_MigrationScripts/One_time_scripts.cs index db72ebfb..aca0111a 100644 --- a/unittests/TestCommon/Generic/Running_MigrationScripts/One_time_scripts.cs +++ b/unittests/TestCommon/Generic/Running_MigrationScripts/One_time_scripts.cs @@ -53,7 +53,7 @@ public async Task Are_not_run_more_than_once_when_unchanged() } [Fact] - public async Task Fails_if_changed_between_runs() + public virtual async Task Fails_if_changed_between_runs() { var db = TestConfig.RandomDatabase(); diff --git a/unittests/TestCommon/TestInfrastructure/GrateMigratorExtensions.cs b/unittests/TestCommon/TestInfrastructure/GrateMigratorExtensions.cs index b9ce274e..cb35dc85 100644 --- a/unittests/TestCommon/TestInfrastructure/GrateMigratorExtensions.cs +++ b/unittests/TestCommon/TestInfrastructure/GrateMigratorExtensions.cs @@ -8,9 +8,4 @@ public static IDbMigrator GetDbMigrator(this IGrateMigrator migrator) { return (migrator as GrateMigrator)!.DbMigrator; } - - public static bool SupportsSchemas(this IGrateMigrator migrator) - { - return (migrator as GrateMigrator)!.Database!.SupportsSchemas; - } } diff --git a/unittests/TestCommon/TestInfrastructure/IGrateTestContext.cs b/unittests/TestCommon/TestInfrastructure/IGrateTestContext.cs index 7a12e373..f1e06e30 100644 --- a/unittests/TestCommon/TestInfrastructure/IGrateTestContext.cs +++ b/unittests/TestCommon/TestInfrastructure/IGrateTestContext.cs @@ -36,9 +36,11 @@ public interface IGrateTestContext public IGrateMigrator Migrator { get; } - public bool SupportsSchemas => Migrator.SupportsSchemas(); + + //public bool SupportsSchemas => Migrator.SupportsSchemas(); bool SupportsCreateDatabase { get; } + bool SupportsSchemas { get; } IDbConnection GetDbConnection(string connectionString); } diff --git a/unittests/TestCommon/TestInfrastructure/TestConfig.cs b/unittests/TestCommon/TestInfrastructure/TestConfig.cs index a6eca76b..e25fec80 100644 --- a/unittests/TestCommon/TestInfrastructure/TestConfig.cs +++ b/unittests/TestCommon/TestInfrastructure/TestConfig.cs @@ -15,6 +15,11 @@ public static DirectoryInfo CreateRandomTempDirectory() var dummyFile = Path.GetTempFileName(); File.Delete(dummyFile); + if (Directory.Exists(dummyFile)) + { + Directory.Delete(dummyFile, true); + } + var scriptsDir = Directory.CreateDirectory(dummyFile); return scriptsDir; }