From 78a64ea2c75667c7b9ca27cec8d9010a0ae03c10 Mon Sep 17 00:00:00 2001 From: Tobias Metzke-Bernstein Date: Thu, 5 Aug 2021 09:18:28 +0200 Subject: [PATCH] chore(sql-script): add equality test for Liquibase --- distro/sql-script/pom.xml | 123 +++----- .../camunda/bpm/sql/test/SqlScriptTest.java | 282 ++++++++++++++++++ .../resources/properties-from-pom.properties | 6 + 3 files changed, 322 insertions(+), 89 deletions(-) create mode 100644 distro/sql-script/src/test/java/org/camunda/bpm/sql/test/SqlScriptTest.java create mode 100644 distro/sql-script/src/test/resources/properties-from-pom.properties diff --git a/distro/sql-script/pom.xml b/distro/sql-script/pom.xml index 19cb7bbeec..e5b8dcbdc4 100644 --- a/distro/sql-script/pom.xml +++ b/distro/sql-script/pom.xml @@ -13,6 +13,10 @@ jar Camunda Platform - SQL scripts + + + true + @@ -30,7 +34,6 @@ org.camunda.bpm camunda-engine - ${project.version} provided @@ -40,6 +43,25 @@ 7.15.0 provided + + + + junit + junit + test + + + org.assertj + assertj-core + test + + + org.liquibase + liquibase-core + 4.4.2 + test + + @@ -79,6 +101,13 @@ *.sql + + src/test/resources + true + + **/properties-from-pom.properties + + @@ -479,100 +508,16 @@ - + check-sql - - - - - org.codehaus.mojo - sql-maven-plugin - - - create-engine - generate-test-sources - - execute - - - - ${project.build.directory}/sql/create/${database.type}_engine_${project.version}.sql - - true - - - - create-identity - generate-test-sources - - execute - - - - ${project.build.directory}/sql/create/${database.type}_identity_${project.version}.sql - - true - - - - drop-engine - generate-test-sources - - execute - - - - ${project.build.directory}/sql/drop/${database.type}_engine_${project.version}.sql - - true - - - - drop-identity - generate-test-sources - - execute - - - - ${project.build.directory}/sql/drop/${database.type}_identity_${project.version}.sql - - true - - - - - - org.liquibase - liquibase-maven-plugin - - - create-engine-liquibase - generate-test-sources - - update - - - changelog.xml - ${project.build.directory}/sql/liquibase/ - - - - drop-engine-liquibase - generate-test-sources - - dropAll - - - - - - + + false + diff --git a/distro/sql-script/src/test/java/org/camunda/bpm/sql/test/SqlScriptTest.java b/distro/sql-script/src/test/java/org/camunda/bpm/sql/test/SqlScriptTest.java new file mode 100644 index 0000000000..90e2d890e0 --- /dev/null +++ b/distro/sql-script/src/test/java/org/camunda/bpm/sql/test/SqlScriptTest.java @@ -0,0 +1,282 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.camunda.bpm.sql.test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.file.Paths; +import java.util.Properties; +import java.util.SortedSet; + +import org.camunda.commons.utils.IoUtil; +import org.camunda.commons.utils.StringUtil; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import liquibase.Contexts; +import liquibase.Liquibase; +import liquibase.change.core.SQLFileChange; +import liquibase.database.Database; +import liquibase.database.DatabaseFactory; +import liquibase.database.OfflineConnection; +import liquibase.diff.DiffResult; +import liquibase.diff.ObjectDifferences; +import liquibase.diff.compare.CompareControl; +import liquibase.diff.output.DiffOutputControl; +import liquibase.diff.output.ObjectChangeFilter; +import liquibase.diff.output.changelog.DiffToChangeLog; +import liquibase.exception.DatabaseException; +import liquibase.exception.LiquibaseParseException; +import liquibase.parser.SnapshotParser; +import liquibase.parser.SnapshotParserFactory; +import liquibase.resource.ClassLoaderResourceAccessor; +import liquibase.resource.FileSystemResourceAccessor; +import liquibase.resource.InputStreamList; +import liquibase.resource.ResourceAccessor; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.SnapshotControl; +import liquibase.snapshot.SnapshotGeneratorFactory; +import liquibase.structure.DatabaseObject; + +public class SqlScriptTest { + + protected Properties properties; + protected Database database; + protected String databaseType; + protected String projectVersion; + + @Before + public void setup() throws Exception { + InputStream is = getClass().getClassLoader().getResourceAsStream("properties-from-pom.properties"); + properties = new Properties(); + properties.load(is); + + databaseType = properties.getProperty("database.type"); + projectVersion = properties.getProperty("project.version"); + + database = getDatabase(properties); + } + + @After + public void tearDown() throws Exception { + if (database == null) { + database = getDatabase(properties); + } + if (database != null) { + // Liquibase will take care of closing the database as well + try (Liquibase liquibase = new Liquibase((String) null, null, database)) { + liquibase.dropAll(); + } + } + } + + @Test + public void shouldRunCreateScripts() { + // when + executeSqlScript("create", "engine"); + executeSqlScript("create", "identity"); + + // then there is no failure + } + + @Test + public void shouldRunDropScriptsAfterCreateScripts() { + // given + executeSqlScript("create", "engine"); + executeSqlScript("create", "identity"); + + // when + executeSqlScript("drop", "engine"); + executeSqlScript("drop", "identity"); + + // then there is no failure + } + + @Test + public void shouldRunLiquibaseChangelog() throws Exception { + // given + File changelogDirectory = getChangelogDirectory(); + try (Liquibase liquibase = new Liquibase("changelog.xml", new FileSystemResourceAccessor(changelogDirectory), database)) { + // when + liquibase.update(new Contexts()); + // then there is no failure + } catch (Exception ex) { + fail(String.format("Failed to execute Liquibase changelog with: %s%n%s", + ex.getClass().getName(), StringUtil.getStackTrace(ex))); + } finally { + // database has been closed already, tearDown should create a new connection if necessary + database = null; + } + } + + @Test + public void shouldEqualLiquibaseChangelogAndCreateScripts() throws Exception { + // given + SnapshotParserFactory.getInstance().register(new DirectAccessSnapshotParser()); + // database set up with create scripts + executeSqlScript("create", "engine"); + executeSqlScript("create", "identity"); + + File changelogDirectory = getChangelogDirectory(); + try (Liquibase liquibase = new Liquibase("changelog.xml", new FileSystemResourceAccessor(changelogDirectory), database)) { + // snapshot created of the database for manual scripts + DatabaseSnapshot snapshotManualScripts = createCurrentDatabaseSnapshot(); + // database cleared and set up with Liquibase changelog + liquibase.dropAll(); + liquibase.update(new Contexts()); + DatabaseSnapshot snapshotLiquibaseChangelog = createCurrentDatabaseSnapshot(); + // when + DiffResult diffResult = liquibase.diff(getDatabaseForSnapshot(snapshotManualScripts), + getDatabaseForSnapshot(snapshotLiquibaseChangelog), new CompareControl()); + + // TODO try if that helps defining what differences to ignore + new DiffToChangeLog(diffResult, new DiffOutputControl().setObjectChangeFilter(new ObjectChangeFilter() { + + @Override + public boolean includeUnexpected(DatabaseObject object, Database referenceDatabase, Database comparisionDatabase) { + return checkNoOtherElementsIgnoreName(object, referenceDatabase); + } + + @Override + public boolean includeMissing(DatabaseObject object, Database referenceDatabase, Database comparisionDatabase) { + return checkNoOtherElementsIgnoreName(object, comparisionDatabase); + } + + @Override + public boolean includeChanged(DatabaseObject object, ObjectDifferences differences, Database referenceDatabase, Database comparisionDatabase) { + // always include + return true; + } + + @Override + public boolean include(DatabaseObject object) { + return true; + } + + private boolean checkNoOtherElementsIgnoreName(DatabaseObject object, Database referenceDatabase) { + // TODO check if no other object of same type with same attributes (except the name) exists in comparison database + return true; + } + })).generateChangeSets(); + // then + assertThat(diffResult.getChangedObjects()).isEmpty(); + assertThat(diffResult.getMissingObjects()).isEmpty();// TODO constraints can have different names on h2 (might be even more on other systems) + assertThat(diffResult.getUnexpectedObjects()).hasSize(2);// TODO there should be 2 additional tables (Liquibase tables) and columns related to them, nothing else + } finally { + // database has been closed already, tearDown should create a new connection if necessary + database = null; + } + } + + protected Database getDatabase(Properties properties) throws DatabaseException { + String databaseUrl = properties.getProperty("database.url"); + String databaseUser = properties.getProperty("database.username"); + String databasePassword = properties.getProperty("database.password"); + String databaseClass = properties.getProperty("database.driver"); + return DatabaseFactory.getInstance().openDatabase(databaseUrl, databaseUser, databasePassword, databaseClass, + null, null, null, new ClassLoaderResourceAccessor()); + } + + protected void executeSqlScript(String sqlFolder, String sqlScript) { + try { + String statements = IoUtil.inputStreamAsString(getClass().getClassLoader().getResourceAsStream( + String.format("sql/%s/%s_%s_%s.sql", sqlFolder, databaseType, sqlScript, projectVersion))); + SQLFileChange sqlFileChange = new SQLFileChange(); + sqlFileChange.setSql(statements); + database.execute(sqlFileChange.generateStatements(database), null); + } catch (Exception ex) { + fail(String.format("Failed to execute SQL script with: %s%n%s", ex.getClass().getName(), StringUtil.getStackTrace(ex))); + } + } + + protected Liquibase getLiquibase() throws URISyntaxException { + File changelogDirectory = getChangelogDirectory(); + return new Liquibase("changelog.xml", new FileSystemResourceAccessor(changelogDirectory), database); + } + + protected File getChangelogDirectory() throws URISyntaxException { + return Paths.get(getClass().getClassLoader().getResource("sql/liquibase").toURI()).toAbsolutePath().toFile(); + } + + protected DatabaseSnapshot createCurrentDatabaseSnapshot() throws Exception { + return SnapshotGeneratorFactory.getInstance().createSnapshot(database.getDefaultSchema(), database, new SnapshotControl(database)); + } + + protected Database getDatabaseForSnapshot(DatabaseSnapshot snapshot) throws Exception { + String offlineDatabaseUrl = "offline:" + databaseType + "?snapshot=foo"; + OfflineConnection offlineDatabaseConnection = new OfflineConnection(offlineDatabaseUrl, new SnapshotResourceAccessor(snapshot)); + return DatabaseFactory.getInstance().findCorrectDatabaseImplementation(offlineDatabaseConnection); + } + + protected static class DirectAccessSnapshotParser implements SnapshotParser { + + @Override + public int getPriority() { + return 0; + } + + @Override + public DatabaseSnapshot parse(String path, ResourceAccessor resourceAccessor) throws LiquibaseParseException { + return ((SnapshotResourceAccessor) resourceAccessor).getSnapshot(); + } + + @Override + public boolean supports(String path, ResourceAccessor resourceAccessor) { + return resourceAccessor instanceof SnapshotResourceAccessor; + } + } + + protected static class SnapshotResourceAccessor implements ResourceAccessor { + + private DatabaseSnapshot snapshot; + + public SnapshotResourceAccessor(DatabaseSnapshot snapshot) { + this.snapshot = snapshot; + } + + @Override + public InputStreamList openStreams(String relativeTo, String streamPath) throws IOException { + return null; + } + + @Override + public InputStream openStream(String relativeTo, String streamPath) throws IOException { + return null; + } + + @Override + public SortedSet list(String relativeTo, String path, boolean recursive, boolean includeFiles, + boolean includeDirectories) throws IOException { + return null; + } + + @Override + public SortedSet describeLocations() { + return null; + } + + public DatabaseSnapshot getSnapshot() { + return snapshot; + } + } + +} diff --git a/distro/sql-script/src/test/resources/properties-from-pom.properties b/distro/sql-script/src/test/resources/properties-from-pom.properties new file mode 100644 index 0000000000..6b833b3506 --- /dev/null +++ b/distro/sql-script/src/test/resources/properties-from-pom.properties @@ -0,0 +1,6 @@ +database.url=${database.url} +database.username=${database.username} +database.password=${database.password} +database.driver=${database.driver} +database.type=${database.type} +project.version=${project.version} \ No newline at end of file -- 2.25.0.windows.1