diff --git a/src/SQLCE4Umbraco/SqlCEHelper.cs b/src/SQLCE4Umbraco/SqlCEHelper.cs index ab5d5065ad..a7f1abd88c 100644 --- a/src/SQLCE4Umbraco/SqlCEHelper.cs +++ b/src/SQLCE4Umbraco/SqlCEHelper.cs @@ -45,117 +45,6 @@ namespace SqlCE4Umbraco } } - //SD: Commented this out, it was used to remove all data in the database but not the database schema.... - // hoping for faster unit test performance, unfortunately I don't think it is currently possible to - // install the base umbraco data without dropping constraints. For some reason I don't think the performance - // will be much faster dropping and re-creating constraints as opposed to just re-creating the schema. -// internal void ClearUmbracoDatabaseData() -// { -// var tables = new List(); -// using (var reader = ExecuteReader("select table_name from information_schema.tables where table_type <> 'VIEW' order by table_name")) -// { -// while (reader.Read()) tables.Add(reader.GetString("table_name").Trim()); -// } - -// var pKeys = new List>(); -// //first get the primary key columsn for each table: -// using (var reader = ExecuteReader(@" -//select information_schema.table_constraints.table_name, column_name from information_schema.table_constraints -//inner join information_schema.key_column_usage -//on information_schema.table_constraints.constraint_name = information_schema.key_column_usage.constraint_name -//where constraint_type = 'PRIMARY KEY'")) -// { -// while (reader.Read()) -// { -// pKeys.Add(new Tuple(reader.GetString("table_name").Trim(), reader.GetString("column_name").Trim())); -// } -// } - -// //exclude the umbracoNode table, it is special and has a recursive constraing so we'll deal with that last -// tables = tables.Where(x => !x.InvariantEquals("umbracoNode")).ToList(); - -// var retries = -1; - -// //Clear all data in all tables except for umbracoNode... this will always be last -// while (tables.Any()) -// { -// retries++; - -// //avoid an infinite loop if there's something seriously wrong -// if (retries > 100) -// { -// throw new ApplicationException("Could not clear out all of the data in the database :("); -// } - -// var currTables = tables.ToArray(); -// for (int index = 0; index < currTables.Count(); index++) -// { -// var table = currTables[index]; -// try -// { - -// //get all foreign key constraint columns for the table -// var fkCols = new List(); -// using (var reader = ExecuteReader(@" -//select column_name -// from information_schema.key_column_usage -// inner join information_schema.table_constraints on -// information_schema.key_column_usage.constraint_name = -// information_schema.table_constraints.constraint_name -//where constraint_type = 'FOREIGN KEY' AND information_schema.key_column_usage.table_name = '" + table + "'")) -// { -// while (reader.Read()) fkCols.Add(reader.GetString("column_name").Trim()); -// } - -// //set the value to null for everything in these columns (we do a try catch in case null is not allowed) -// foreach (var fk in fkCols) -// { -// try -// { -// ExecuteNonQuery("UPDATE " + table + " SET " + fk + " = NULL"); -// } -// catch (Exception) -// { -// //swallow this exception and continue, apparently null is not allowed here -// } -// } - -// //SINCE SqlCe doesn't support "truncate table" we need to do this and reset the identity seed -// ExecuteNonQuery("delete from [" + table + "]"); -// foreach (var key in pKeys.Where(x => x.Item1.InvariantEquals(table))) -// { -// try -// { -// ExecuteNonQuery("ALTER TABLE [" + table + "] ALTER COLUMN " + key.Item2 + " IDENTITY (1,1)"); -// } -// catch (Exception) -// { -// //swallow... SD: we're swallowing this in case the key doesn't have an identity, there might be a better way to -// // look this up but I can't find it. -// } -// } -// tables.Remove(table); //table is complete -// } -// catch (Exception) -// { -// //swallow... SD: I know this isn't nice but it's just as fast and trying to identify the upmost 'parent' table -// // and then follow its chain down to it's bottommost 'child' table based on constraints/relations is something -// // I don't feel like writing.... could figure it out manually but don't have time atm. -// } -// } -// } - -// //Now, we deal with umbracoNode -// //Set all parentId's = the first item found -// var firstId = ExecuteScalar("Select TOP 1 id from umbracoNode"); -// ExecuteNonQuery("UPDATE umbracoNode SET parentId = " + firstId); -// //now clear out the data, aparet from the foreign key we've updated too -// ExecuteNonQuery("delete from umbracoNode where id <> " + firstId); -// //finally clear out the last one -// ExecuteNonQuery("delete from umbracoNode"); - -// } - /// /// Most likely only will be used for unit tests but will remove all tables from the database /// diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index 1397a04246..549ec87bff 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Events; +using Umbraco.Core.Logging; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.SqlSyntax; @@ -65,15 +66,28 @@ namespace Umbraco.Core.Persistence.Migrations.Initial }; #endregion - //SD: Commented out for now, was used to insert the data into a database that already has a schema - ///// - ///// Returns the tables in order of creation cycle - ///// - ///// - //internal IEnumerable GetOrderedTableDefinitions() - //{ - // return OrderedTables.OrderBy(x => x.Key).Select(x => DefinitionFactory.GetTableDefinition(x.Value)); - //} + /// + /// Drops all Umbraco tables in the db + /// + internal void UninstallDatabaseSchema() + { + foreach (var item in OrderedTables.OrderByDescending(x => x.Key)) + { + var tableNameAttribute = item.Value.FirstAttribute(); + string tableName = tableNameAttribute == null ? item.Value.Name : tableNameAttribute.Value; + + try + { + _database.DropTable(tableName); + } + catch (Exception ex) + { + //swallow this for now, not sure how best to handle this with diff databases... though this is internal + // and only used for unit tests. If this fails its because the table doesn't exist... generally! + LogHelper.Error("Could not drop table " + tableName, ex); + } + } + } public DatabaseSchemaCreation(Database database) { diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index df89639c0d..333c94455c 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -139,6 +139,12 @@ namespace Umbraco.Core.Persistence CreateDatabaseSchema(db, true); } + internal static void UninstallDatabaseSchema(this Database db) + { + var creation = new DatabaseSchemaCreation(db); + creation.UninstallDatabaseSchema(); + } + internal static void CreateDatabaseSchema(this Database db, bool guardConfiguration) { if (guardConfiguration && ApplicationContext.Current.IsConfigured) @@ -156,28 +162,6 @@ namespace Umbraco.Core.Persistence NewTable -= PetaPocoExtensions_NewTable; } - //NOTE: SD: This doesn't work because in order to create the initial data the constraints cannot exist, it is not enough - // to just allow identity insertion. - //internal static void CreateInitialData(this Database db) - //{ - // var creation = new DatabaseSchemaCreation(db); - // foreach (var tableDefinition in creation.GetOrderedTableDefinitions()) - // { - // var baseDataCreation = new BaseDataCreation(db); - // var tableName = tableDefinition.Name; - // //Turn on identity insert if db provider is not mysql - // if (SqlSyntaxContext.SqlSyntaxProvider.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - // db.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} ON ", SqlSyntaxContext.SqlSyntaxProvider.GetQuotedTableName(tableName)))); - - // baseDataCreation.InitializeBaseData(tableName); - - // //Turn off identity insert if db provider is not mysql - // if (SqlSyntaxContext.SqlSyntaxProvider.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - // db.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} OFF;", SqlSyntaxContext.SqlSyntaxProvider.GetQuotedTableName(tableName)))); - // } - - //} - public static DatabaseProviders GetDatabaseProvider(this Database db) { return ApplicationContext.Current.DatabaseContext.DatabaseProvider; diff --git a/src/Umbraco.Tests/PublishedContent/DynamicNodeTests.cs b/src/Umbraco.Tests/PublishedContent/DynamicNodeTests.cs index d7a1340d87..902cd30544 100644 --- a/src/Umbraco.Tests/PublishedContent/DynamicNodeTests.cs +++ b/src/Umbraco.Tests/PublishedContent/DynamicNodeTests.cs @@ -20,6 +20,14 @@ namespace Umbraco.Tests.PublishedContent get { return true; } } + /// + /// We only need a new schema per fixture... speeds up testing + /// + protected override DbInitBehavior DatabaseTestBehavior + { + get { return DbInitBehavior.NewSchemaPerFixture; } + } + public override void Initialize() { base.Initialize(); diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index 77d2f6faef..cbbfc23bb3 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -26,7 +26,6 @@ using umbraco.BusinessLogic; namespace Umbraco.Tests.TestHelpers { - /// /// Use this abstract class for tests that requires a Sql Ce database populated with the umbraco db schema. /// The PetaPoco Database class should be used through the . @@ -34,12 +33,22 @@ namespace Umbraco.Tests.TestHelpers [TestFixture, RequiresSTA] public abstract class BaseDatabaseFactoryTest : BaseUmbracoApplicationTest { - private static volatile bool _firstRun = true; + //This is used to indicate that this is the first test to run in the test session, if so, we always + //ensure a new database file is used. + private static volatile bool _firstRunInTestSession = true; private static readonly object Locker = new object(); + private bool _firstTestInFixture = true; + + //Used to flag if its the first test in the current session + private bool _isFirstRunInTestSession = false; + //Used to flag if its the first test in the current fixture + private bool _isFirstTestInFixture = false; [SetUp] public override void Initialize() { + InitializeFirstRunFlags(); + base.Initialize(); var path = TestHelper.CurrentAssemblyDirectory; @@ -51,19 +60,22 @@ namespace Umbraco.Tests.TestHelpers //assign the service context new ServiceContext(new PetaPocoUnitOfWorkProvider(), new FileUnitOfWorkProvider(), new PublishingStrategy())) { IsReady = true }; - CreateDatabase(); - DatabaseContext.Initialize(); + CreateDatabase(); + InitializeDatabase(); //ensure the configuration matches the current version for tests SettingsForTests.ConfigurationStatus = UmbracoVersion.Current.ToString(3); } - - protected virtual bool EnsureBrandNewDatabase + + /// + /// The database behavior to use for the test/fixture + /// + protected virtual DbInitBehavior DatabaseTestBehavior { - get { return false; } + get { return DbInitBehavior.NewSchemaPerTest; } } /// @@ -71,60 +83,48 @@ namespace Umbraco.Tests.TestHelpers /// protected virtual void CreateDatabase() { - //this needs to be thread-safe - var isFirstRun = false; - if (_firstRun) - { - lock (Locker) - { - if (_firstRun) - { - isFirstRun = true; //set the flag - _firstRun = false; - } - } - } - var path = TestHelper.CurrentAssemblyDirectory; - - //Ensure that any database connections from a previous test is disposed. - //This is really just double safety as its also done in the TearDown. - if (ApplicationContext != null && DatabaseContext != null) - DatabaseContext.Database.Dispose(); - SqlCeContextGuardian.CloseBackgroundConnection(); - + //Get the connectionstring settings from config var settings = ConfigurationManager.ConnectionStrings[Core.Configuration.GlobalSettings.UmbracoConnectionName]; ConfigurationManager.AppSettings.Set(Core.Configuration.GlobalSettings.UmbracoConnectionName, @"datalayer=SQLCE4Umbraco.SqlCEHelper,SQLCE4Umbraco;data source=|DataDirectory|\UmbracoPetaPocoTests.sdf"); string dbFilePath = string.Concat(path, "\\UmbracoPetaPocoTests.sdf"); - if (isFirstRun || EnsureBrandNewDatabase || !File.Exists(dbFilePath)) + //create a new database file if + // - is the first test in the session + // - the database file doesn't exist + // - NewDbFileAndSchemaPerTest + // - _isFirstTestInFixture + DbInitBehavior.NewDbFileAndSchemaPerFixture + + //if this is the first test in the session, always ensure a new db file is created + if (_isFirstRunInTestSession || !File.Exists(dbFilePath) + || DatabaseTestBehavior == DbInitBehavior.NewDbFileAndSchemaPerTest + || (_isFirstTestInFixture && DatabaseTestBehavior == DbInitBehavior.NewDbFileAndSchemaPerFixture)) { - try - { - //Delete database file before continueing - if (File.Exists(dbFilePath)) + + RemoveDatabaseFile(ex => { - File.Delete(dbFilePath); - } - } - catch (Exception) - { - //if this doesn't work we have to make sure everything is reset! otherwise - // well run into issues because we've already set some things up - TearDown(); - throw; - } + //if this doesn't work we have to make sure everything is reset! otherwise + // well run into issues because we've already set some things up + TearDown(); + throw ex; + }); //Create the Sql CE database var engine = new SqlCeEngine(settings.ConnectionString); engine.CreateDatabase(); } - else + + //clear the database if + // - NewSchemaPerTest + // - _isFirstTestInFixture + DbInitBehavior.NewSchemaPerFixture + + else if (DatabaseTestBehavior == DbInitBehavior.NewSchemaPerTest + || (_isFirstTestInFixture && DatabaseTestBehavior == DbInitBehavior.NewSchemaPerFixture)) { - TestHelper.ClearDatabase(); - } + DatabaseContext.Database.UninstallDatabaseSchema(); + } } /// @@ -152,46 +152,112 @@ namespace Umbraco.Tests.TestHelpers /// protected virtual void InitializeDatabase() { - //Create the umbraco database and its base data - DatabaseContext.Database.CreateDatabaseSchema(false); + //create the schema and load default data if: + // - is the first test in the session + // - NewSchemaPerTest + // - NewDbFileAndSchemaPerTest + // - _isFirstTestInFixture + DbInitBehavior.NewSchemaPerFixture + // - _isFirstTestInFixture + DbInitBehavior.NewDbFileAndSchemaPerFixture + + if (_isFirstRunInTestSession + || DatabaseTestBehavior == DbInitBehavior.NewSchemaPerTest + || (_isFirstTestInFixture && DatabaseTestBehavior == DbInitBehavior.NewSchemaPerFixture)) + { + //Create the umbraco database and its base data + DatabaseContext.Database.CreateDatabaseSchema(false); + } + } + + [TestFixtureTearDown] + public void FixtureTearDown() + { + if (DatabaseTestBehavior == DbInitBehavior.NewDbFileAndSchemaPerFixture) + { + RemoveDatabaseFile(); + } } [TearDown] public override void TearDown() { - base.TearDown(); + _isFirstTestInFixture = false; //ensure this is false before anything! - string path = TestHelper.CurrentAssemblyDirectory; - - SqlSyntaxContext.SqlSyntaxProvider = null; - - //legacy API database connection close - because a unit test using PetaPoco db-layer can trigger the usage of SqlHelper we need to ensure that a possible connection is closed. - SqlCeContextGuardian.CloseBackgroundConnection(); - - if (EnsureBrandNewDatabase) + if (DatabaseTestBehavior == DbInitBehavior.NewDbFileAndSchemaPerTest) { - try - { - string filePath = string.Concat(path, "\\UmbracoPetaPocoTests.sdf"); - if (File.Exists(filePath)) - { - File.Delete(filePath); - } - } - catch (Exception ex) - { - LogHelper.Error("Could not remove the old database file", ex); - - //We will swallow this exception! That's because a sub class might require further teardown logic. - } + RemoveDatabaseFile(); } - else + else if (DatabaseTestBehavior == DbInitBehavior.NewSchemaPerTest) { - TestHelper.ClearDatabase(); + DatabaseContext.Database.UninstallDatabaseSchema(); + //TestHelper.ClearDatabase(); } AppDomain.CurrentDomain.SetData("DataDirectory", null); - + + SqlSyntaxContext.SqlSyntaxProvider = null; + + base.TearDown(); + } + + private void CloseDbConnections() + { + //Ensure that any database connections from a previous test is disposed. + //This is really just double safety as its also done in the TearDown. + if (ApplicationContext != null && DatabaseContext != null && DatabaseContext.Database != null) + DatabaseContext.Database.Dispose(); + SqlCeContextGuardian.CloseBackgroundConnection(); + } + + private void InitializeFirstRunFlags() + { + //this needs to be thread-safe + _isFirstRunInTestSession = false; + if (_firstRunInTestSession) + { + lock (Locker) + { + if (_firstRunInTestSession) + { + _isFirstRunInTestSession = true; //set the flag + _firstRunInTestSession = false; + } + } + } + if (_firstTestInFixture) + { + lock (Locker) + { + if (_firstTestInFixture) + { + _isFirstTestInFixture = true; //set the flag + _firstTestInFixture = false; + } + } + } + } + + private void RemoveDatabaseFile(Action onFail = null) + { + CloseDbConnections(); + string path = TestHelper.CurrentAssemblyDirectory; + try + { + string filePath = string.Concat(path, "\\UmbracoPetaPocoTests.sdf"); + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + } + catch (Exception ex) + { + LogHelper.Error("Could not remove the old database file", ex); + + //We will swallow this exception! That's because a sub class might require further teardown logic. + if (onFail != null) + { + onFail(ex); + } + } } protected ServiceContext ServiceContext diff --git a/src/Umbraco.Tests/TestHelpers/DbInitBehavior.cs b/src/Umbraco.Tests/TestHelpers/DbInitBehavior.cs new file mode 100644 index 0000000000..ec9f2419f2 --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/DbInitBehavior.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Tests.TestHelpers +{ + /// + /// The behavior used to control how the database is handled for test fixtures inheriting from BaseDatabaseFactoryTest + /// + public enum DbInitBehavior + { + /// + /// For each test a new database file and schema will be created + /// + NewDbFileAndSchemaPerTest, + + /// + /// For each test a new schema will be created on the existing database file + /// + NewSchemaPerTest, + + /// + /// Creates a new database file and schema for the whole fixture, each test will use the pre-existing one + /// + NewDbFileAndSchemaPerFixture, + + /// + /// Creates a new schema based on the existing database file for the whole fixture, each test will use the pre-existing one + /// + NewSchemaPerFixture, + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs index 788cced28a..6ab161a3b0 100644 --- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs @@ -31,17 +31,6 @@ namespace Umbraco.Tests.TestHelpers dataHelper.ClearDatabase(); } - //public static void ClearUmbracoDatabaseData() - //{ - // var databaseSettings = ConfigurationManager.ConnectionStrings[Core.Configuration.GlobalSettings.UmbracoConnectionName]; - // var dataHelper = DataLayerHelper.CreateSqlHelper(databaseSettings.ConnectionString, false) as SqlCEHelper; - - // if (dataHelper == null) - // throw new InvalidOperationException("The sql helper for unit tests must be of type SqlCEHelper, check the ensure the connection string used for this test is set to use SQLCE"); - - // dataHelper.ClearUmbracoDatabaseData(); - //} - public static void DropForeignKeys(string table) { var databaseSettings = ConfigurationManager.ConnectionStrings[Core.Configuration.GlobalSettings.UmbracoConnectionName]; diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 1fa8603804..2373fc3f4d 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -302,6 +302,7 @@ +