diff --git a/grate.unittests/Basic/Infrastructure/FileSystem_.cs b/grate.unittests/Basic/Infrastructure/FileSystem_.cs new file mode 100644 index 00000000..df1e87bf --- /dev/null +++ b/grate.unittests/Basic/Infrastructure/FileSystem_.cs @@ -0,0 +1,57 @@ +using System.IO; +using System.Linq; +using FluentAssertions; +using grate.Configuration; +using grate.Migration; +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.Basic.Infrastructure; + +[TestFixture] +[Category("Basic")] +public class FileSystem_ +{ + [Test] + public void Sorts_enumerated_files_on_filename_when_no_subfolders() + { + var knownFolders = KnownFolders.In(TestConfig.CreateRandomTempDirectory()); + + var path = knownFolders.Up!.Path; + + var folder1 = new DirectoryInfo(path.ToString()); + + string filename1 = "01_any_filename.sql"; + string filename2 = "02_any_filename.sql"; + + TestConfig.WriteContent(folder1, filename1, "Whatever"); + TestConfig.WriteContent(folder1, filename2, "Whatever"); + + var files = FileSystem.GetFiles(path, "*.sql").ToList(); + + files.First().FullName.Should().Be(Path.Combine(folder1.ToString(), filename1)); + files.Last().FullName.Should().Be(Path.Combine(folder1.ToString(), filename2)); + } + + [Test] + public void Sorts_enumerated_files_on_sub_path_when_subfolders_are_used() + { + var knownFolders = KnownFolders.In(TestConfig.CreateRandomTempDirectory()); + + var path = knownFolders.Up!.Path; + + var folder1 = new DirectoryInfo(Path.Combine(path.ToString(), "01_sub", "folder", "long", "way")); + var folder2 = new DirectoryInfo(Path.Combine(path.ToString(), "02_sub", "folder", "long", "way")); + + string filename1 = "01_any_filename.sql"; + string filename2 = "02_any_filename.sql"; + + TestConfig.WriteContent(folder1, filename2, "Whatever"); + TestConfig.WriteContent(folder2, filename1, "Whatever"); + + var files = FileSystem.GetFiles(path, "*.sql").ToList(); + + files.First().FullName.Should().Be(Path.Combine(folder1.ToString(), filename2)); + files.Last().FullName.Should().Be(Path.Combine(folder2.ToString(), filename1)); + } +} diff --git a/grate.unittests/Generic/GenericMigrationTables.cs b/grate.unittests/Generic/GenericMigrationTables.cs index 5139e769..780cb07f 100644 --- a/grate.unittests/Generic/GenericMigrationTables.cs +++ b/grate.unittests/Generic/GenericMigrationTables.cs @@ -121,7 +121,7 @@ public async Task Inserts_version_in_version_table() versions.Should().HaveCount(1); versions.FirstOrDefault().Should().Be("a.b.c.d"); } - + private static void CreateInvalidSql(MigrationsFolder? folder) { var dummySql = "SELECT TOP"; @@ -135,15 +135,17 @@ private static void WriteSql(DirectoryInfo path, string filename, string? sql) } private static DirectoryInfo MakeSurePathExists(MigrationsFolder? folder) + => MakeSurePathExists(folder?.Path); + + protected static DirectoryInfo MakeSurePathExists(DirectoryInfo? path) { - var path = folder?.Path ?? throw new ArgumentException(nameof(folder.Path)); - + ArgumentNullException.ThrowIfNull(path); if (!path.Exists) { path.Create(); } - return path; } + } diff --git a/grate.unittests/Generic/Running_MigrationScripts/MigrationsScriptsBase.cs b/grate.unittests/Generic/Running_MigrationScripts/MigrationsScriptsBase.cs index edc75fff..91f31638 100644 --- a/grate.unittests/Generic/Running_MigrationScripts/MigrationsScriptsBase.cs +++ b/grate.unittests/Generic/Running_MigrationScripts/MigrationsScriptsBase.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using grate.Configuration; using grate.unittests.TestInfrastructure; @@ -7,53 +6,30 @@ namespace grate.unittests.Generic.Running_MigrationScripts; public abstract class MigrationsScriptsBase { - protected static DirectoryInfo CreateRandomTempDirectory() - { - - // I keep seeing the same temp di name in repeat test runs, and the dir has leftover scripts sitting in it. - // Trying to get a clean folder each time we ask for it - - var dummyFile = Path.GetRandomFileName(); - File.Delete(dummyFile); - - var scriptsDir = Directory.CreateDirectory(dummyFile); - return scriptsDir; - } + protected static DirectoryInfo CreateRandomTempDirectory() => TestConfig.CreateRandomTempDirectory(); protected void CreateDummySql(MigrationsFolder? folder, string filename = "1_jalla.sql") + => CreateDummySql(folder?.Path, filename); + + protected void WriteSomeOtherSql(MigrationsFolder? folder, string filename = "1_jalla.sql") + => WriteSomeOtherSql(folder?.Path, filename); + + protected void CreateDummySql(DirectoryInfo? path, string filename = "1_jalla.sql") { var dummySql = Context.Sql.SelectVersion; - var path = MakeSurePathExists(folder); WriteSql(path, filename, dummySql); } - protected void WriteSomeOtherSql(MigrationsFolder? folder, string filename = "1_jalla.sql") + protected void WriteSomeOtherSql(DirectoryInfo? path, string filename = "1_jalla.sql") { var dummySql = Context.Syntax.CurrentDatabase; - var path = MakeSurePathExists(folder); WriteSql(path, filename, dummySql); } - protected static void WriteSql(DirectoryInfo path, string filename, string? sql) - { - if (!path.Exists) - { - path.Create(); - } - File.WriteAllText(Path.Combine(path.ToString(), filename), sql); - } + protected static void WriteSql(DirectoryInfo? path, string filename, string? sql) => + TestConfig.WriteContent(path, filename, sql); - protected static DirectoryInfo MakeSurePathExists(MigrationsFolder? folder) - { - var path = folder?.Path ?? throw new ArgumentException(nameof(folder.Path)); - - if (!path.Exists) - { - path.Create(); - } - - return path; - } + protected static DirectoryInfo MakeSurePathExists(MigrationsFolder? folder) => TestConfig.MakeSurePathExists(folder?.Path); protected abstract IGrateTestContext Context { get; } -} \ No newline at end of file +} diff --git a/grate.unittests/Generic/Running_MigrationScripts/One_time_scripts.cs b/grate.unittests/Generic/Running_MigrationScripts/One_time_scripts.cs index be898aba..46ee25d8 100644 --- a/grate.unittests/Generic/Running_MigrationScripts/One_time_scripts.cs +++ b/grate.unittests/Generic/Running_MigrationScripts/One_time_scripts.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using System.Threading.Tasks; using Dapper; diff --git a/grate.unittests/Generic/Running_MigrationScripts/ScriptsRun_Table.cs b/grate.unittests/Generic/Running_MigrationScripts/ScriptsRun_Table.cs new file mode 100644 index 00000000..b73008d9 --- /dev/null +++ b/grate.unittests/Generic/Running_MigrationScripts/ScriptsRun_Table.cs @@ -0,0 +1,126 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Dapper; +using FluentAssertions; +using grate.Configuration; +using grate.Migration; +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.Generic.Running_MigrationScripts; + +[TestFixture] +public abstract class ScriptsRun_Table : MigrationsScriptsBase +{ + [Test()] + public async Task Includes_the_folder_name_in_the_script_name_if_subfolders() + { + var db = TestConfig.RandomDatabase(); + + var knownFolders = KnownFolders.In(TestConfig.CreateRandomTempDirectory()); + GrateMigrator? migrator; + + var folder = new DirectoryInfo(Path.Combine(knownFolders.Up!.Path.ToString(), "sub", "folder", "long", "way")); + + string filename = "any_filename.sql"; + + CreateDummySql(folder, filename); + await using (migrator = Context.GetMigrator(db, knownFolders)) + { + await migrator.Migrate(); + } + + string[] scripts; + string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + + await using (var conn = Context.CreateDbConnection(db)) + { + scripts = (await conn.QueryAsync(sql)).ToArray(); + } + + var expectedName = $"sub/folder/long/way/{filename}"; + scripts.First().Should().Be(expectedName); + } + + [Test()] + public async Task Does_not_include_the_folder_name_in_the_script_name_if_no_subfolders() + { + var db = TestConfig.RandomDatabase(); + + var knownFolders = KnownFolders.In(TestConfig.CreateRandomTempDirectory()); + GrateMigrator? migrator; + + string filename = "any_filename.sql"; + + CreateDummySql(knownFolders.Up, filename); + + await using (migrator = Context.GetMigrator(db, knownFolders)) + { + await migrator.Migrate(); + } + + string[] scripts; + string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + + await using (var conn = Context.CreateDbConnection(db)) + { + scripts = (await conn.QueryAsync(sql)).ToArray(); + } + + scripts.First().Should().Be(filename); + } + + // ReSharper disable InconsistentNaming + // ReSharper disable once ClassNeverInstantiated.Local + record Result(string script_name, string text_of_script); + // ReSharper restore InconsistentNaming + + [Test()] + public async Task Does_not_overwrite_scripts_from_different_folders_with_last_content() + { + var db = TestConfig.RandomDatabase(); + + var knownFolders = KnownFolders.In(TestConfig.CreateRandomTempDirectory()); + GrateMigrator? migrator; + + string filename = "any_filename.sql"; + var folder1 = new DirectoryInfo(Path.Combine(knownFolders.Up!.Path.ToString(), "dub", "folder", "long", "way")); + var folder2 = new DirectoryInfo(Path.Combine(knownFolders.Up!.Path.ToString(), "sub", "dolder", "gong", "way")); + + CreateDummySql(folder1, filename); + WriteSomeOtherSql(folder2, filename); + + await using (migrator = Context.GetMigrator(db, knownFolders)) + { + await migrator.Migrate(); + } + + + Result[] scripts; + string sql = $"SELECT script_name, text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + + await using (var conn = Context.CreateDbConnection(db)) + { + scripts = (await conn.QueryAsync(sql)).ToArray(); + } + + Assert.Multiple(() => + { + scripts.Should().HaveCount(2); + var first = scripts.First(); + var second = scripts.Last(); + + first.script_name.Should().Be($"dub/folder/long/way/{filename}"); + first.text_of_script.Should().Be(Context.Sql.SelectVersion); + + second.script_name.Should().Be($"sub/dolder/gong/way/{filename}"); + second.text_of_script.Should().Be(Context.Syntax.CurrentDatabase); + }); + + + + } + + +} diff --git a/grate.unittests/MariaDB/Running_MigrationScripts/ScriptsRun_Table.cs b/grate.unittests/MariaDB/Running_MigrationScripts/ScriptsRun_Table.cs new file mode 100644 index 00000000..36f1f297 --- /dev/null +++ b/grate.unittests/MariaDB/Running_MigrationScripts/ScriptsRun_Table.cs @@ -0,0 +1,8 @@ +using grate.unittests.TestInfrastructure; + +namespace grate.unittests.MariaDB.Running_MigrationScripts; + +public class ScriptsRun_Table: Generic.Running_MigrationScripts.ScriptsRun_Table +{ + protected override IGrateTestContext Context => GrateTestContext.MariaDB; +} diff --git a/grate.unittests/Oracle/Running_MigrationScripts/ScriptsRun_Table.cs b/grate.unittests/Oracle/Running_MigrationScripts/ScriptsRun_Table.cs new file mode 100644 index 00000000..37f82317 --- /dev/null +++ b/grate.unittests/Oracle/Running_MigrationScripts/ScriptsRun_Table.cs @@ -0,0 +1,8 @@ +using grate.unittests.TestInfrastructure; + +namespace grate.unittests.Oracle.Running_MigrationScripts; + +public class ScriptsRun_Table: Generic.Running_MigrationScripts.ScriptsRun_Table +{ + protected override IGrateTestContext Context => GrateTestContext.Oracle; +} diff --git a/grate.unittests/PostgreSQL/Running_MigrationScripts/ScriptsRun_Table.cs b/grate.unittests/PostgreSQL/Running_MigrationScripts/ScriptsRun_Table.cs new file mode 100644 index 00000000..7b07d114 --- /dev/null +++ b/grate.unittests/PostgreSQL/Running_MigrationScripts/ScriptsRun_Table.cs @@ -0,0 +1,8 @@ +using grate.unittests.TestInfrastructure; + +namespace grate.unittests.PostgreSQL.Running_MigrationScripts; + +public class ScriptsRun_Table: Generic.Running_MigrationScripts.ScriptsRun_Table +{ + protected override IGrateTestContext Context => GrateTestContext.PostgreSql; +} diff --git a/grate.unittests/SqLite/Running_MigrationScripts/ScriptsRun_Table.cs b/grate.unittests/SqLite/Running_MigrationScripts/ScriptsRun_Table.cs new file mode 100644 index 00000000..b53c921f --- /dev/null +++ b/grate.unittests/SqLite/Running_MigrationScripts/ScriptsRun_Table.cs @@ -0,0 +1,8 @@ +using grate.unittests.TestInfrastructure; + +namespace grate.unittests.SqLite.Running_MigrationScripts; + +public class ScriptsRun_Table: Generic.Running_MigrationScripts.ScriptsRun_Table +{ + protected override IGrateTestContext Context => GrateTestContext.Sqlite; +} diff --git a/grate.unittests/SqLite/SetupTestEnvironment.cs b/grate.unittests/SqLite/SetupTestEnvironment.cs index 80f078bd..3739919e 100644 --- a/grate.unittests/SqLite/SetupTestEnvironment.cs +++ b/grate.unittests/SqLite/SetupTestEnvironment.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Threading; +using System.Threading.Tasks; using grate.unittests.TestInfrastructure; using Microsoft.Data.Sqlite; using NUnit.Framework; @@ -22,8 +24,27 @@ public void RunBeforeAnyTests() Logger.LogDebug($"Before tests. Deleting old DB files."); foreach (var dbFile in dbFiles) { - Logger.LogDebug("File: {DbFile}", dbFile); - File.Delete(dbFile); + TryDeletingFile(dbFile); + } + } + + private static void TryDeletingFile(string dbFile) + { + var i = 0; + var sleepTime = 300; + const int maxTries = 5; + while (i++ < maxTries) + { + try + { + Logger.LogDebug("File: {DbFile}", dbFile); + File.Delete(dbFile); + return; + } + catch (IOException) when (i <= maxTries) + { + Thread.Sleep(sleepTime); + } } } diff --git a/grate.unittests/SqlServer/Running_MigrationScripts/ScriptsRun_Table.cs b/grate.unittests/SqlServer/Running_MigrationScripts/ScriptsRun_Table.cs new file mode 100644 index 00000000..10373dd2 --- /dev/null +++ b/grate.unittests/SqlServer/Running_MigrationScripts/ScriptsRun_Table.cs @@ -0,0 +1,8 @@ +using grate.unittests.TestInfrastructure; + +namespace grate.unittests.SqlServer.Running_MigrationScripts; + +public class ScriptsRun_Table: Generic.Running_MigrationScripts.ScriptsRun_Table +{ + protected override IGrateTestContext Context => GrateTestContext.SqlServer; +} diff --git a/grate.unittests/TestInfrastructure/TestConfig.cs b/grate.unittests/TestInfrastructure/TestConfig.cs index 427be128..69fd4ca1 100644 --- a/grate.unittests/TestInfrastructure/TestConfig.cs +++ b/grate.unittests/TestInfrastructure/TestConfig.cs @@ -40,4 +40,27 @@ private static LogLevel GetLogLevel() return logLevel; } + public static void WriteContent(DirectoryInfo? path, string filename, string? content) + { + ArgumentNullException.ThrowIfNull(path); + if (!path.Exists) + { + path.Create(); + } + + File.WriteAllText(Path.Combine(path.ToString(), filename), content); + } + + public static DirectoryInfo MakeSurePathExists(DirectoryInfo? path) + { + ArgumentNullException.ThrowIfNull(path); + + if (!path.Exists) + { + path.Create(); + } + + return path; + } + } \ No newline at end of file diff --git a/grate/Migration/DbMigrator.cs b/grate/Migration/DbMigrator.cs index fd3b4e60..62a21eba 100644 --- a/grate/Migration/DbMigrator.cs +++ b/grate/Migration/DbMigrator.cs @@ -295,4 +295,4 @@ public async ValueTask DisposeAsync() await Database.DisposeAsync(); GC.SuppressFinalize(this); } -} +} \ No newline at end of file diff --git a/grate/Migration/FileSystem.cs b/grate/Migration/FileSystem.cs new file mode 100644 index 00000000..eb16d9c5 --- /dev/null +++ b/grate/Migration/FileSystem.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace grate.Migration; + +public static class FileSystem +{ + public static IEnumerable GetFiles(DirectoryInfo folderPath, string pattern) + { + return folderPath + .EnumerateFileSystemInfos(pattern, SearchOption.AllDirectories).ToList() + .OrderBy(f => Path.GetRelativePath(folderPath.ToString(), f.FullName), StringComparer.CurrentCultureIgnoreCase); + } +} diff --git a/grate/Migration/GrateMigrator.cs b/grate/Migration/GrateMigrator.cs index e47b4b66..58b4a56f 100644 --- a/grate/Migration/GrateMigrator.cs +++ b/grate/Migration/GrateMigrator.cs @@ -241,20 +241,24 @@ private async Task Process(MigrationsFolder folder, string changeDropFolder, lon } var pattern = "*.sql"; - var files = GetFiles(folder.Path, pattern); + var files = FileSystem.GetFiles(folder.Path, pattern); foreach (var file in files) { var sql = await File.ReadAllTextAsync(file.FullName); - bool theSqlRan = await _migrator.RunSql(sql, file.Name, folder.Type, versionId, _migrator.Configuration.Environment, + // Normalize file names to log, so that results won't vary if you run on *nix VS Windows + var fileNameToLog = string.Join('/', + Path.GetRelativePath(folder.Path.ToString(), file.FullName).Split(Path.DirectorySeparatorChar)); + + bool theSqlRan = await _migrator.RunSql(sql, fileNameToLog, folder.Type, versionId, _migrator.Configuration.Environment, connectionType); if (theSqlRan) { try { - CopyToChangeDropFolder(file, changeDropFolder); + CopyToChangeDropFolder(folder.Path.Parent!, file, changeDropFolder); } catch (Exception ex) { @@ -265,11 +269,9 @@ private async Task Process(MigrationsFolder folder, string changeDropFolder, lon } - private void CopyToChangeDropFolder(FileSystemInfo file, string changeDropFolder) + private void CopyToChangeDropFolder(DirectoryInfo migrationRoot, FileSystemInfo file, string changeDropFolder) { - var cfg = _migrator.Configuration; - - var relativePath = Path.GetRelativePath(cfg.SqlFilesDirectory.ToString(), file.FullName); + var relativePath = Path.GetRelativePath(migrationRoot.ToString(), file.FullName); string destinationFile = Path.Combine(changeDropFolder, "itemsRan", relativePath); @@ -281,13 +283,6 @@ private void CopyToChangeDropFolder(FileSystemInfo file, string changeDropFolder File.Copy(file.FullName, destinationFile); } - - private static IEnumerable GetFiles(DirectoryInfo folderPath, string pattern) - { - return folderPath - .EnumerateFileSystemInfos(pattern, SearchOption.AllDirectories).ToList() - .OrderBy(f => f.Name, StringComparer.CurrentCultureIgnoreCase); - } // ReSharper disable once TemplateIsNotCompileTimeConstantProblem #pragma warning disable CA2254 // Template should be a static expression. @@ -310,7 +305,7 @@ public static string ChangeDropFolder(GrateConfiguration config, string? server, "migrations", RemoveInvalidPathChars(database), RemoveInvalidPathChars(server), - RemoveInvalidPathChars(DateTime.Now.ToString("s")) + RemoveInvalidPathChars(DateTime.Now.ToString("O")) ); return folder; } @@ -318,6 +313,7 @@ public static string ChangeDropFolder(GrateConfiguration config, string? server, private static readonly char[] InvalidPathCharacters = Path.GetInvalidPathChars() .Append(':') .Append(',') + .Append('+') .ToArray(); private static string RemoveInvalidPathChars(string? path) diff --git a/grate/grate.csproj b/grate/grate.csproj index ae25e2b0..38748959 100644 --- a/grate/grate.csproj +++ b/grate/grate.csproj @@ -41,7 +41,7 @@ up using modern .NET 6. - +