diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2019-08-28 17:33:17 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-09-02 20:21:04 +0200 |
commit | b789401479f17bc69eac0c5676d24c704f37b707 (patch) | |
tree | d669d121c480cae721e64679944335c8ba929e51 /server/sonar-db-core/src/testFixtures | |
parent | f0f55945b8c05cfb3feda3d06ceea4173467c31a (diff) | |
download | sonarqube-b789401479f17bc69eac0c5676d24c704f37b707.tar.gz sonarqube-b789401479f17bc69eac0c5676d24c704f37b707.zip |
use testFixtures instead of test configuration of db-core
Diffstat (limited to 'server/sonar-db-core/src/testFixtures')
6 files changed, 976 insertions, 0 deletions
diff --git a/server/sonar-db-core/src/testFixtures/java/org/sonar/db/AbstractDbTester.java b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/AbstractDbTester.java new file mode 100644 index 00000000000..0b9bb16bc6e --- /dev/null +++ b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/AbstractDbTester.java @@ -0,0 +1,512 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Ordering; +import java.math.BigDecimal; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.dbutils.QueryRunner; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.junit.rules.ExternalResource; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.util.stream.MoreCollectors; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Lists.asList; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Maps.newHashMap; +import static java.sql.ResultSetMetaData.columnNoNulls; +import static java.sql.ResultSetMetaData.columnNullable; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +public class AbstractDbTester<T extends TestDb> extends ExternalResource { + private static final Joiner COMMA_JOINER = Joiner.on(", "); + protected final T db; + + public AbstractDbTester(T db) { + this.db = db; + } + + public T getDb() { + return db; + } + + public void executeUpdateSql(String sql, Object... params) { + try (Connection connection = getConnection()) { + new QueryRunner().update(connection, sql, params); + if (!connection.getAutoCommit()) { + connection.commit(); + } + } catch (SQLException e) { + SQLException nextException = e.getNextException(); + if (nextException != null) { + throw new IllegalStateException("Fail to execute sql: " + sql, + new SQLException(e.getMessage(), nextException.getSQLState(), nextException.getErrorCode(), nextException)); + } + throw new IllegalStateException("Fail to execute sql: " + sql, e); + } catch (Exception e) { + throw new IllegalStateException("Fail to execute sql: " + sql, e); + } + } + + public void executeDdl(String ddl) { + try (Connection connection = getConnection(); + Statement stmt = connection.createStatement()) { + stmt.execute(ddl); + } catch (SQLException e) { + throw new IllegalStateException("Failed to execute DDL: " + ddl, e); + } + } + + /** + * Very simple helper method to insert some data into a table. + * It's the responsibility of the caller to convert column values to string. + */ + public void executeInsert(String table, String firstColumn, Object... others) { + executeInsert(table, mapOf(firstColumn, others)); + } + + private static Map<String, Object> mapOf(String firstColumn, Object... values) { + ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder(); + List<Object> args = asList(firstColumn, values); + for (int i = 0; i < args.size(); i++) { + String key = args.get(i).toString(); + Object value = args.get(i + 1); + if (value != null) { + builder.put(key, value); + } + i++; + } + return builder.build(); + } + + /** + * Very simple helper method to insert some data into a table. + * It's the responsibility of the caller to convert column values to string. + */ + public void executeInsert(String table, Map<String, Object> valuesByColumn) { + if (valuesByColumn.isEmpty()) { + throw new IllegalArgumentException("Values cannot be empty"); + } + + String sql = "insert into " + table.toLowerCase(Locale.ENGLISH) + " (" + + COMMA_JOINER.join(valuesByColumn.keySet().stream().map(t -> t.toLowerCase(Locale.ENGLISH)).toArray(String[]::new)) + + ") values (" + + COMMA_JOINER.join(Collections.nCopies(valuesByColumn.size(), '?')) + + ")"; + executeUpdateSql(sql, valuesByColumn.values().toArray(new Object[valuesByColumn.size()])); + } + + /** + * Returns the number of rows in the table. Example: + * <pre>int issues = countRowsOfTable("issues")</pre> + */ + public int countRowsOfTable(String tableName) { + return countRowsOfTable(tableName, new NewConnectionSupplier()); + } + + protected int countRowsOfTable(String tableName, ConnectionSupplier connectionSupplier) { + checkArgument(StringUtils.containsNone(tableName, " "), "Parameter must be the name of a table. Got " + tableName); + return countSql("select count(1) from " + tableName.toLowerCase(Locale.ENGLISH), connectionSupplier); + } + + /** + * Executes a SQL request starting with "SELECT COUNT(something) FROM", for example: + * <pre>int OpenIssues = countSql("select count('id') from issues where status is not null")</pre> + */ + public int countSql(String sql) { + return countSql(sql, new NewConnectionSupplier()); + } + + protected int countSql(String sql, ConnectionSupplier connectionSupplier) { + checkArgument(StringUtils.contains(sql, "count("), + "Parameter must be a SQL request containing 'count(x)' function. Got " + sql); + try ( + ConnectionSupplier supplier = connectionSupplier; + PreparedStatement stmt = supplier.get().prepareStatement(sql); + ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return rs.getInt(1); + } + throw new IllegalStateException("No results for " + sql); + + } catch (Exception e) { + throw new IllegalStateException("Fail to execute sql: " + sql, e); + } + } + + public List<Map<String, Object>> select(String selectSql) { + return select(selectSql, new NewConnectionSupplier()); + } + + protected List<Map<String, Object>> select(String selectSql, ConnectionSupplier connectionSupplier) { + try ( + ConnectionSupplier supplier = connectionSupplier; + PreparedStatement stmt = supplier.get().prepareStatement(selectSql); + ResultSet rs = stmt.executeQuery()) { + return getHashMap(rs); + } catch (Exception e) { + throw new IllegalStateException("Fail to execute sql: " + selectSql, e); + } + } + + public Map<String, Object> selectFirst(String selectSql) { + return selectFirst(selectSql, new NewConnectionSupplier()); + } + + protected Map<String, Object> selectFirst(String selectSql, ConnectionSupplier connectionSupplier) { + List<Map<String, Object>> rows = select(selectSql, connectionSupplier); + if (rows.isEmpty()) { + throw new IllegalStateException("No results for " + selectSql); + } else if (rows.size() > 1) { + throw new IllegalStateException("Too many results for " + selectSql); + } + return rows.get(0); + } + + private static List<Map<String, Object>> getHashMap(ResultSet resultSet) throws Exception { + ResultSetMetaData metaData = resultSet.getMetaData(); + int colCount = metaData.getColumnCount(); + List<Map<String, Object>> rows = newArrayList(); + while (resultSet.next()) { + Map<String, Object> columns = newHashMap(); + for (int i = 1; i <= colCount; i++) { + Object value = resultSet.getObject(i); + if (value instanceof Clob) { + Clob clob = (Clob) value; + value = IOUtils.toString((clob.getAsciiStream())); + doClobFree(clob); + } else if (value instanceof BigDecimal) { + // In Oracle, INTEGER types are mapped as BigDecimal + BigDecimal bgValue = ((BigDecimal) value); + if (bgValue.scale() == 0) { + value = bgValue.longValue(); + } else { + value = bgValue.doubleValue(); + } + } else if (value instanceof Integer) { + // To be consistent, all INTEGER types are mapped as Long + value = ((Integer) value).longValue(); + } else if (value instanceof Byte) { + Byte byteValue = (Byte) value; + value = byteValue.intValue(); + } else if (value instanceof Timestamp) { + value = new Date(((Timestamp) value).getTime()); + } + columns.put(metaData.getColumnLabel(i), value); + } + rows.add(columns); + } + return rows; + } + + public void assertColumnDefinition(String table, String column, int expectedType, @Nullable Integer expectedSize, @Nullable Boolean isNullable) { + try (Connection connection = getConnection(); + PreparedStatement stmt = connection.prepareStatement("select * from " + table); + ResultSet res = stmt.executeQuery()) { + Integer columnIndex = getColumnIndex(res, column); + if (columnIndex == null) { + fail("The column '" + column + "' does not exist"); + } + + assertThat(res.getMetaData().getColumnType(columnIndex)).isEqualTo(expectedType); + if (expectedSize != null) { + assertThat(res.getMetaData().getColumnDisplaySize(columnIndex)).isEqualTo(expectedSize); + } + if (isNullable != null) { + assertThat(res.getMetaData().isNullable(columnIndex)).isEqualTo(isNullable ? columnNullable : columnNoNulls); + } + } catch (Exception e) { + throw new IllegalStateException("Fail to check column", e); + } + } + + public void assertColumnDoesNotExist(String table, String column) throws SQLException { + try (Connection connection = getConnection(); + PreparedStatement stmt = connection.prepareStatement("select * from " + table); + ResultSet res = stmt.executeQuery()) { + assertThat(getColumnNames(res)).doesNotContain(column); + } + } + + public void assertTableDoesNotExist(String table) { + assertTableExists(table, false); + } + + public void assertTableExists(String table) { + assertTableExists(table, true); + } + + private void assertTableExists(String table, boolean expected) { + try (Connection connection = getConnection()) { + boolean tableExists = DatabaseUtils.tableExists(table, connection); + assertThat(tableExists).isEqualTo(expected); + } catch (Exception e) { + throw new IllegalStateException("Fail to check if table exists", e); + } + } + + /** + * Verify that non-unique index exists on columns + */ + public void assertIndex(String tableName, String indexName, String expectedColumn, String... expectedSecondaryColumns) { + assertIndexImpl(tableName, indexName, false, expectedColumn, expectedSecondaryColumns); + } + + /** + * Verify that unique index exists on columns + */ + public void assertUniqueIndex(String tableName, String indexName, String expectedColumn, String... expectedSecondaryColumns) { + assertIndexImpl(tableName, indexName, true, expectedColumn, expectedSecondaryColumns); + } + + private void assertIndexImpl(String tableName, String indexName, boolean expectedUnique, String expectedColumn, String... expectedSecondaryColumns) { + try (Connection connection = getConnection(); + ResultSet rs = connection.getMetaData().getIndexInfo(null, null, tableName.toUpperCase(Locale.ENGLISH), false, false)) { + List<String> onColumns = new ArrayList<>(); + while (rs.next()) { + if (indexName.equalsIgnoreCase(rs.getString("INDEX_NAME"))) { + assertThat(rs.getBoolean("NON_UNIQUE")).isEqualTo(!expectedUnique); + int position = rs.getInt("ORDINAL_POSITION"); + onColumns.add(position - 1, rs.getString("COLUMN_NAME").toLowerCase(Locale.ENGLISH)); + } + } + assertThat(asList(expectedColumn, expectedSecondaryColumns)).isEqualTo(onColumns); + } catch (SQLException e) { + throw new IllegalStateException("Fail to check index", e); + } + } + + /** + * Verify that index with name {@code indexName} does not exist on the table {@code tableName} + */ + public void assertIndexDoesNotExist(String tableName, String indexName) { + try (Connection connection = getConnection(); + ResultSet rs = connection.getMetaData().getIndexInfo(null, null, tableName.toUpperCase(Locale.ENGLISH), false, false)) { + List<String> indices = new ArrayList<>(); + while (rs.next()) { + indices.add(rs.getString("INDEX_NAME").toLowerCase(Locale.ENGLISH)); + } + assertThat(indices).doesNotContain(indexName); + } catch (SQLException e) { + throw new IllegalStateException("Fail to check existence of index", e); + } + } + + public void assertPrimaryKey(String tableName, @Nullable String expectedPkName, String columnName, String... otherColumnNames) { + try (Connection connection = getConnection()) { + PK pk = pkOf(connection, tableName.toUpperCase(Locale.ENGLISH)); + if (pk == null) { + pkOf(connection, tableName.toLowerCase(Locale.ENGLISH)); + } + assertThat(pk).as("No primary key is defined on table %s", tableName).isNotNull(); + if (expectedPkName != null) { + assertThat(pk.getName()).isEqualToIgnoringCase(expectedPkName); + } + List<String> expectedColumns = ImmutableList.copyOf(Iterables.concat(Collections.singletonList(columnName), Arrays.asList(otherColumnNames))); + assertThat(pk.getColumns()).as("Primary key does not have the '%s' expected columns", expectedColumns.size()).hasSize(expectedColumns.size()); + + Iterator<String> expectedColumnsIt = expectedColumns.iterator(); + Iterator<String> actualColumnsIt = pk.getColumns().iterator(); + while (expectedColumnsIt.hasNext() && actualColumnsIt.hasNext()) { + assertThat(actualColumnsIt.next()).isEqualToIgnoringCase(expectedColumnsIt.next()); + } + } catch (SQLException e) { + throw new IllegalStateException("Fail to check primary key", e); + } + } + + @CheckForNull + private PK pkOf(Connection connection, String tableName) throws SQLException { + try (ResultSet resultSet = connection.getMetaData().getPrimaryKeys(null, null, tableName)) { + String pkName = null; + List<PkColumn> columnNames = null; + while (resultSet.next()) { + if (columnNames == null) { + pkName = resultSet.getString("PK_NAME"); + columnNames = new ArrayList<>(1); + } else { + assertThat(pkName).as("Multiple primary keys found").isEqualTo(resultSet.getString("PK_NAME")); + } + columnNames.add(new PkColumn(resultSet.getInt("KEY_SEQ") - 1, resultSet.getString("COLUMN_NAME"))); + } + if (columnNames == null) { + return null; + } + return new PK( + pkName, + columnNames.stream() + .sorted(PkColumn.ORDERING_BY_INDEX) + .map(PkColumn::getName) + .collect(MoreCollectors.toList())); + } + } + + private static final class PkColumn { + private static final Ordering<PkColumn> ORDERING_BY_INDEX = Ordering.natural().onResultOf(PkColumn::getIndex); + + /** 0-based */ + private final int index; + private final String name; + + private PkColumn(int index, String name) { + this.index = index; + this.name = name; + } + + public int getIndex() { + return index; + } + + public String getName() { + return name; + } + } + + @CheckForNull + private Integer getColumnIndex(ResultSet res, String column) { + try { + ResultSetMetaData meta = res.getMetaData(); + int numCol = meta.getColumnCount(); + for (int i = 1; i < numCol + 1; i++) { + if (meta.getColumnLabel(i).toLowerCase().equals(column.toLowerCase())) { + return i; + } + } + return null; + + } catch (Exception e) { + throw new IllegalStateException("Fail to get column index"); + } + } + + private Set<String> getColumnNames(ResultSet res) { + try { + Set<String> columnNames = new HashSet<>(); + ResultSetMetaData meta = res.getMetaData(); + int numCol = meta.getColumnCount(); + for (int i = 1; i < numCol + 1; i++) { + columnNames.add(meta.getColumnLabel(i).toLowerCase()); + } + return columnNames; + } catch (Exception e) { + throw new IllegalStateException("Fail to get column names"); + } + } + + private static void doClobFree(Clob clob) throws SQLException { + try { + clob.free(); + } catch (AbstractMethodError e) { + // JTS driver do not implement free() as it's using JDBC 3.0 + } + } + + public Connection openConnection() throws SQLException { + return getConnection(); + } + + private Connection getConnection() throws SQLException { + return db.getDatabase().getDataSource().getConnection(); + } + + public Database database() { + return db.getDatabase(); + } + + /** + * An {@link AutoCloseable} supplier of {@link Connection}. + */ + protected interface ConnectionSupplier extends AutoCloseable { + Connection get() throws SQLException; + + @Override + void close(); + } + + private static class PK { + @CheckForNull + private final String name; + private final List<String> columns; + + private PK(@Nullable String name, List<String> columns) { + this.name = name; + this.columns = ImmutableList.copyOf(columns); + } + + @CheckForNull + public String getName() { + return name; + } + + public List<String> getColumns() { + return columns; + } + } + + private class NewConnectionSupplier implements ConnectionSupplier { + private Connection connection; + + @Override + public Connection get() throws SQLException { + if (this.connection == null) { + this.connection = getConnection(); + } + return this.connection; + } + + @Override + public void close() { + if (this.connection != null) { + try { + this.connection.close(); + } catch (SQLException e) { + Loggers.get(CoreDbTester.class).warn("Fail to close connection", e); + // do not re-throw the exception + } + } + } + } +} diff --git a/server/sonar-db-core/src/testFixtures/java/org/sonar/db/CoreDbTester.java b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/CoreDbTester.java new file mode 100644 index 00000000000..ed85f851228 --- /dev/null +++ b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/CoreDbTester.java @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db; + +import org.apache.commons.lang.StringUtils; + +/** + * This class should be called using @Rule. + * Data is truncated between each tests. The schema is created between each test. + */ +public class CoreDbTester extends AbstractDbTester<CoreTestDb> { + private final DefaultOrganizationTesting defaultOrganizationTesting; + + private CoreDbTester(CoreTestDb testDb) { + super(testDb); + this.defaultOrganizationTesting = new DefaultOrganizationTesting(this); + } + + public static CoreDbTester createForSchema(Class testClass, String filename) { + String path = StringUtils.replaceChars(testClass.getCanonicalName(), '.', '/'); + String schemaPath = path + "/" + filename; + return new CoreDbTester(CoreTestDb.create(schemaPath)); + } + + public static CoreDbTester createEmpty() { + return new CoreDbTester(CoreTestDb.createEmpty()); + } + + @Override + protected void before() { + db.start(); + db.truncateTables(); + } + + @Override + protected void after() { + db.stop(); + } + + public DefaultOrganizationTesting defaultOrganization() { + return defaultOrganizationTesting; + } +} diff --git a/server/sonar-db-core/src/testFixtures/java/org/sonar/db/CoreH2Database.java b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/CoreH2Database.java new file mode 100644 index 00000000000..f4a78eed82d --- /dev/null +++ b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/CoreH2Database.java @@ -0,0 +1,119 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; +import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.io.output.NullWriter; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.jdbc.ScriptRunner; +import org.sonar.db.dialect.Dialect; +import org.sonar.db.dialect.H2; + +import static java.lang.String.format; + +/** + * H2 in-memory database, used for unit tests only against an empty DB or a provided H2 SQL script. + */ +public class CoreH2Database implements Database { + private final String name; + private BasicDataSource datasource; + + /** + * IMPORTANT: change DB name in order to not conflict with {@link DefaultDatabaseTest} + */ + public CoreH2Database(String name) { + this.name = name; + } + + @Override + public void start() { + startDatabase(); + } + + private void startDatabase() { + try { + datasource = new BasicDataSource(); + datasource.setDriverClassName("org.h2.Driver"); + datasource.setUsername("sonar"); + datasource.setPassword("sonar"); + datasource.setUrl("jdbc:h2:mem:" + name); + } catch (Exception e) { + throw new IllegalStateException("Fail to start H2", e); + } + } + + public void executeScript(String classloaderPath) { + try (Connection connection = datasource.getConnection()) { + executeScript(connection, classloaderPath); + } catch (SQLException e) { + throw new IllegalStateException("Fail to execute script: " + classloaderPath, e); + } + } + + private static void executeScript(Connection connection, String path) { + ScriptRunner scriptRunner = newScriptRunner(connection); + try { + scriptRunner.runScript(Resources.getResourceAsReader(path)); + connection.commit(); + + } catch (Exception e) { + throw new IllegalStateException("Fail to restore: " + path, e); + } + } + + private static ScriptRunner newScriptRunner(Connection connection) { + ScriptRunner scriptRunner = new ScriptRunner(connection); + scriptRunner.setDelimiter(";"); + scriptRunner.setStopOnError(true); + scriptRunner.setLogWriter(new PrintWriter(new NullWriter())); + return scriptRunner; + } + + @Override + public void stop() { + try { + datasource.close(); + } catch (SQLException e) { + // Ignore error + } + } + + public DataSource getDataSource() { + return datasource; + } + + public Dialect getDialect() { + return new H2(); + } + + @Override + public void enableSqlLogging(boolean enable) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + return format("H2 Database[%s]", name); + } +} diff --git a/server/sonar-db-core/src/testFixtures/java/org/sonar/db/CoreTestDb.java b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/CoreTestDb.java new file mode 100644 index 00000000000..4479031f8c3 --- /dev/null +++ b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/CoreTestDb.java @@ -0,0 +1,190 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import javax.annotation.Nullable; +import javax.sql.DataSource; +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.AssumptionViolatedException; +import org.sonar.api.config.Settings; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.db.version.SqTables; + +import static java.util.Objects.requireNonNull; +import static org.sonar.process.ProcessProperties.Property.JDBC_USERNAME; + +/** + * This class should be call using @ClassRule in order to create the schema once (if @Rule is used + * the schema will be recreated before each test). + * <p> + * <strong>Tests which rely on this class can only be run on H2</strong> because: + * <ul> + * <li>resetting the schema for each test on non-H2 database is assumed to expensive and slow</li> + * <li>when a specific schema is provided, this schema can't provide a syntax supported by all SGBDs and therefor only + * H2 is targeted</li> + * </ul> + */ +class CoreTestDb implements TestDb { + + private Database db; + + protected CoreTestDb() { + // use static factory method + } + + protected CoreTestDb(Database db) { + this.db = db; + } + + static CoreTestDb create(String schemaPath) { + requireNonNull(schemaPath, "schemaPath can't be null"); + + return new CoreTestDb().init(schemaPath); + } + + static CoreTestDb createEmpty() { + return new CoreTestDb().init(null); + } + + private CoreTestDb init(@Nullable String schemaPath) { + Consumer<Settings> noExtraSettingsLoaded = settings -> { + }; + Function<Settings, Database> databaseCreator = settings -> { + String dialect = settings.getString("sonar.jdbc.dialect"); + + // test relying on CoreTestDb can only run on H2 + if (dialect != null && !"h2".equals(dialect)) { + throw new AssumptionViolatedException("This test is intended to be run on H2 only"); + } + + return new CoreH2Database("h2Tests-" + (schemaPath == null ? "empty" : DigestUtils.md5Hex(schemaPath))); + }; + Consumer<Database> databaseInitializer = database -> { + if (schemaPath == null) { + return; + } + + ((CoreH2Database) database).executeScript(schemaPath); + }; + BiConsumer<Database, Boolean> noPostStartAction = (db, created) -> { + }; + + init(noExtraSettingsLoaded, databaseCreator, databaseInitializer, noPostStartAction); + return this; + } + + protected void init(Consumer<Settings> settingsLoader, + Function<Settings, Database> databaseCreator, + Consumer<Database> databaseInitializer, + BiConsumer<Database, Boolean> extendedStart) { + if (db == null) { + Settings settings = new MapSettings().addProperties(System.getProperties()); + settingsLoader.accept(settings); + logJdbcSettings(settings); + db = databaseCreator.apply(settings); + db.start(); + + databaseInitializer.accept(db); + Loggers.get(getClass()).debug("Test Database: " + db); + + String login = settings.getString(JDBC_USERNAME.getKey()); + + extendedStart.accept(db, true); + } else { + extendedStart.accept(db, false); + } + } + + public void truncateTables() { + try { + truncateDatabase(getDatabase().getDataSource()); + } catch (SQLException e) { + throw new IllegalStateException("Fail to truncate db tables", e); + } + } + + private void truncateDatabase(DataSource dataSource) throws SQLException { + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(false); + try (Statement statement = connection.createStatement()) { + for (String table : SqTables.TABLES) { + try { + if (shouldTruncate(connection, table)) { + statement.executeUpdate(truncateSql(table)); + connection.commit(); + } + } catch (Exception e) { + connection.rollback(); + throw new IllegalStateException("Fail to truncate table " + table, e); + } + } + } + } + } + + private static boolean shouldTruncate(Connection connection, String table) { + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("select count(1) from " + table)) { + if (rs.next()) { + return rs.getInt(1) > 0; + } + + } catch (SQLException ignored) { + // probably because table does not exist. That's the case with H2 tests. + } + return false; + } + + private static String truncateSql(String table) { + return "TRUNCATE TABLE " + table; + } + + @Override + public Database getDatabase() { + return db; + } + + @Override + public void start() { + // everything is done in init + } + + @Override + public void stop() { + db.stop(); + } + + private void logJdbcSettings(Settings settings) { + Logger logger = Loggers.get(getClass()); + for (String key : settings.getKeysStartingWith("sonar.jdbc")) { + logger.info(key + ": " + settings.getString(key)); + } + } + +} diff --git a/server/sonar-db-core/src/testFixtures/java/org/sonar/db/DefaultOrganizationTesting.java b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/DefaultOrganizationTesting.java new file mode 100644 index 00000000000..f2bb100e27c --- /dev/null +++ b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/DefaultOrganizationTesting.java @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db; + +public class DefaultOrganizationTesting { + private static final String TABLE_ORGANIZATIONS = "organizations"; + private static final String DEFAULT_ORGANIZATION_UUID = "def-org"; + + private final CoreDbTester db; + + public DefaultOrganizationTesting(CoreDbTester db) { + this.db = db; + } + + public String setupDefaultOrganization() { + insertInternalProperty(DEFAULT_ORGANIZATION_UUID); + insertOrganization(DEFAULT_ORGANIZATION_UUID); + return DEFAULT_ORGANIZATION_UUID; + } + + public String insertOrganization() { + insertOrganization(DEFAULT_ORGANIZATION_UUID); + return DEFAULT_ORGANIZATION_UUID; + } + + public void insertOrganization(String uuid) { + db.executeInsert( + TABLE_ORGANIZATIONS, + "UUID", uuid, + "KEE", uuid, + "NAME", uuid, + "CREATED_AT", "1000", + "UPDATED_AT", "1000"); + } + + public String insertInternalProperty() { + insertInternalProperty(DEFAULT_ORGANIZATION_UUID); + return DEFAULT_ORGANIZATION_UUID; + } + + public void insertInternalProperty(String defaultOrganizationUuid) { + db.executeInsert( + "INTERNAL_PROPERTIES", + "KEE", "organization.default", + "IS_EMPTY", "false", + "TEXT_VALUE", defaultOrganizationUuid); + } + +} diff --git a/server/sonar-db-core/src/testFixtures/java/org/sonar/db/TestDb.java b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/TestDb.java new file mode 100644 index 00000000000..c87e3715c12 --- /dev/null +++ b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/TestDb.java @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db; + +public interface TestDb { + void start(); + + void stop(); + + Database getDatabase(); + +} |