compile project(':sonar-core')
compile project(':server:sonar-ce-task')
- compile project(':server:sonar-db-core')
compile project(':server:sonar-db-dao')
compile project(':server:sonar-db-migration')
compile project(':server:sonar-process')
compile project(':server:sonar-ce-task')
compile project(':server:sonar-ce-task-projectanalysis')
compile project(':server:sonar-db-dao')
- compile project(':server:sonar-db-core')
compile project(':server:sonar-process')
compile project(':server:sonar-ce-task')
compile project(':server:sonar-server-common')
testCompile 'com.microsoft.sqlserver:mssql-jdbc'
testCompile 'com.oracle.jdbc:ojdbc8'
testCompile 'com.tngtech.java:junit-dataprovider'
- testCompile 'junit:junit'
- testCompile 'org.assertj:assertj-core'
testCompile 'org.mockito:mockito-core'
testCompile 'org.postgresql:postgresql'
testCompile project(':sonar-testing-harness')
testRuntime 'com.microsoft.sqlserver:mssql-jdbc'
testRuntime 'com.oracle.jdbc:ojdbc8'
testRuntime 'org.postgresql:postgresql'
+
+ testFixturesApi 'junit:junit'
+ testFixturesApi 'org.assertj:assertj-core'
+
+ testFixturesCompileOnly 'com.google.code.findbugs:jsr305'
}
test {
systemProperty 'orchestrator.configUrl', System.getProperty('orchestrator.configUrl')
}
-task testJar(type: Jar) {
- classifier = 'tests'
- from sourceSets.test.output
-}
-
-configurations { tests { extendsFrom testRuntime } }
-
-artifacts {
- tests testJar
-}
-
artifactoryPublish.skip = false
// Used by core plugins
if (release) {
artifact sourcesJar
artifact javadocJar
- artifact testJar
}
}
}
+++ /dev/null
-/*
- * 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
- }
- }
- }
- }
-}
+++ /dev/null
-/*
- * 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;
- }
-}
+++ /dev/null
-/*
- * 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);
- }
-}
+++ /dev/null
-/*
- * 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));
- }
- }
-
-}
+++ /dev/null
-/*
- * 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);
- }
-
-}
+++ /dev/null
-/*
- * 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();
-
-}
--- /dev/null
+/*
+ * 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
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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));
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+
+}
testCompile 'org.mockito:mockito-core'
testCompile 'org.sonarsource.orchestrator:sonar-orchestrator'
testCompile project(':sonar-testing-harness')
- testCompile project(':server:sonar-db-core').sourceSets.test.output
+ testCompile testFixtures(project(':server:sonar-db-core'))
testCompile project(':sonar-plugin-api-impl')
testCompileOnly 'com.google.code.findbugs:jsr305'
testCompile 'org.mockito:mockito-core'
testCompile project(':sonar-scanner-protocol')
testCompile project(':sonar-testing-harness')
- testCompile project(':server:sonar-db-core').sourceSets.test.output
+ testCompile testFixtures(project(':server:sonar-db-core'))
testRuntime 'com.h2database:h2'
testRuntime 'com.microsoft.sqlserver:mssql-jdbc'
// please keep the list grouped by configuration and ordered by name
compile 'com.h2database:h2'
- compile project(':server:sonar-db-core').sourceSets.test.output
+ compile testFixtures(project(':server:sonar-db-core'))
compile project(':server:sonar-db-dao').sourceSets.test.output
}