aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-db-core/src/testFixtures
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2019-08-28 17:33:17 +0200
committerSonarTech <sonartech@sonarsource.com>2019-09-02 20:21:04 +0200
commitb789401479f17bc69eac0c5676d24c704f37b707 (patch)
treed669d121c480cae721e64679944335c8ba929e51 /server/sonar-db-core/src/testFixtures
parentf0f55945b8c05cfb3feda3d06ceea4173467c31a (diff)
downloadsonarqube-b789401479f17bc69eac0c5676d24c704f37b707.tar.gz
sonarqube-b789401479f17bc69eac0c5676d24c704f37b707.zip
use testFixtures instead of test configuration of db-core
Diffstat (limited to 'server/sonar-db-core/src/testFixtures')
-rw-r--r--server/sonar-db-core/src/testFixtures/java/org/sonar/db/AbstractDbTester.java512
-rw-r--r--server/sonar-db-core/src/testFixtures/java/org/sonar/db/CoreDbTester.java60
-rw-r--r--server/sonar-db-core/src/testFixtures/java/org/sonar/db/CoreH2Database.java119
-rw-r--r--server/sonar-db-core/src/testFixtures/java/org/sonar/db/CoreTestDb.java190
-rw-r--r--server/sonar-db-core/src/testFixtures/java/org/sonar/db/DefaultOrganizationTesting.java66
-rw-r--r--server/sonar-db-core/src/testFixtures/java/org/sonar/db/TestDb.java29
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();
+
+}