From 52a508dc5fb51957159f0e5bf2a5c80e2a77f9b5 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Tue, 24 Sep 2013 20:04:22 +0200 Subject: [PATCH] SONAR-4690 SONAR-4691 speed-up db migration of violations --- .../sonar/core/persistence/TestDatabase.java | 25 +- .../migrations/ConvertViolationsToIssues.java | 408 ------------------ .../{ => migrations}/DatabaseMigration.java | 8 +- .../db/migrations/DatabaseMigrations.java} | 20 +- .../db/{ => migrations}/DatabaseMigrator.java | 20 +- .../db/migrations/violation/Progress.java | 72 ++++ .../db/migrations/violation/Referentials.java | 135 ++++++ .../db/migrations/violation/SqlUtil.java | 63 +++ .../violation/ViolationConverter.java | 309 +++++++++++++ .../violation/ViolationConverters.java | 77 ++++ .../violation/ViolationMigration.java | 75 ++++ .../db/migrations/violation/package-info.java | 24 ++ .../org/sonar/server/platform/Platform.java | 7 +- .../java/org/sonar/server/ui/JRubyFacade.java | 2 +- .../main/webapp/WEB-INF/config/environment.rb | 5 + .../401_migrate_violations_to_issues.rb | 4 +- .../DatabaseMigratorTest.java | 26 +- .../db/migrations/violation/ProgressTest.java | 64 +++ .../db/migrations/violation/SqlUtilTest.java | 43 ++ .../violation/ViolationConvertersTest.java | 82 ++++ .../violation/ViolationMigrationTest.java | 66 +++ .../convert_violations_result.xml | 21 - .../migrate_violations.xml} | 8 +- .../migrate_violations_result.xml | 64 +++ .../no_violations_to_migrate.xml | 14 + .../no_violations_to_migrate_result.xml | 14 + .../ViolationMigrationTest}/schema.sql | 0 27 files changed, 1177 insertions(+), 479 deletions(-) delete mode 100644 sonar-server/src/main/java/org/sonar/server/db/migrations/ConvertViolationsToIssues.java rename sonar-server/src/main/java/org/sonar/server/db/{ => migrations}/DatabaseMigration.java (83%) rename sonar-server/src/{test/java/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest.java => main/java/org/sonar/server/db/migrations/DatabaseMigrations.java} (64%) rename sonar-server/src/main/java/org/sonar/server/db/{ => migrations}/DatabaseMigrator.java (82%) create mode 100644 sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Progress.java create mode 100644 sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Referentials.java create mode 100644 sonar-server/src/main/java/org/sonar/server/db/migrations/violation/SqlUtil.java create mode 100644 sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverter.java create mode 100644 sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverters.java create mode 100644 sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationMigration.java create mode 100644 sonar-server/src/main/java/org/sonar/server/db/migrations/violation/package-info.java rename sonar-server/src/test/java/org/sonar/server/db/{ => migrations}/DatabaseMigratorTest.java (84%) create mode 100644 sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ProgressTest.java create mode 100644 sonar-server/src/test/java/org/sonar/server/db/migrations/violation/SqlUtilTest.java create mode 100644 sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationConvertersTest.java create mode 100644 sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationMigrationTest.java delete mode 100644 sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations_result.xml rename sonar-server/src/test/resources/org/sonar/server/db/migrations/{ConvertViolationsToIssuesTest/convert_violations.xml => violation/ViolationMigrationTest/migrate_violations.xml} (90%) create mode 100644 sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations_result.xml create mode 100644 sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate.xml create mode 100644 sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate_result.xml rename sonar-server/src/test/resources/org/sonar/server/db/migrations/{ConvertViolationsToIssuesTest => violation/ViolationMigrationTest}/schema.sql (100%) diff --git a/sonar-core/src/test/java/org/sonar/core/persistence/TestDatabase.java b/sonar-core/src/test/java/org/sonar/core/persistence/TestDatabase.java index 796625ca1be..e750985fd74 100644 --- a/sonar-core/src/test/java/org/sonar/core/persistence/TestDatabase.java +++ b/sonar-core/src/test/java/org/sonar/core/persistence/TestDatabase.java @@ -55,6 +55,8 @@ import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import java.util.Map; @@ -143,6 +145,28 @@ public class TestDatabase extends ExternalResource { } } + public int count(String sql) { + Connection connection = null; + PreparedStatement stmt = null; + ResultSet rs = null; + try { + connection = openConnection(); + stmt = connection.prepareStatement(sql); + 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); + + } finally { + DbUtils.closeQuietly(connection, stmt, rs); + } + } + + public void prepareDbUnit(Class testClass, String... testNames) { InputStream[] streams = new InputStream[testNames.length]; try { @@ -278,5 +302,4 @@ public class TestDatabase extends ExternalResource { IOUtils.closeQuietly(input); } } - } diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/ConvertViolationsToIssues.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/ConvertViolationsToIssues.java deleted file mode 100644 index be1d2c4ce5e..00000000000 --- a/sonar-server/src/main/java/org/sonar/server/db/migrations/ConvertViolationsToIssues.java +++ /dev/null @@ -1,408 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.server.db.migrations; - -import com.google.common.base.Objects; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import org.apache.commons.dbutils.DbUtils; -import org.apache.commons.dbutils.QueryRunner; -import org.apache.commons.dbutils.ResultSetHandler; -import org.apache.commons.dbutils.handlers.AbstractListHandler; -import org.slf4j.LoggerFactory; -import org.sonar.api.rule.Severity; -import org.sonar.core.persistence.Database; -import org.sonar.core.persistence.dialect.Oracle; -import org.sonar.server.db.DatabaseMigration; - -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; - -import java.sql.Connection; -import java.sql.Date; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -/** - * Used in the Active Record Migration 401 - */ -public class ConvertViolationsToIssues implements DatabaseMigration { - - private static final int GROUP_SIZE = 500; - private static final String PROJECT_ID = "projectId"; - private static final String CREATED_AT = "createdAt"; - private static final String REVIEW_ID = "reviewId"; - private static final String SEVERITY = "severity"; - private static final String REVIEW_STATUS = "reviewStatus"; - private static final String REVIEW_MANUAL_SEVERITY = "reviewManualSeverity"; - private static final String REVIEW_SEVERITY = "reviewSeverity"; - private static final String REVIEW_UPDATED_AT = "reviewUpdatedAt"; - private static final String ROOT_PROJECT_ID = "rootProjectId"; - private static final String RULE_ID = "ruleId"; - private static final String MESSAGE = "message"; - private static final String LINE = "line"; - private static final String COST = "cost"; - private static final String CHECKSUM = "checksum"; - private static final String REVIEW_RESOLUTION = "reviewResolution"; - private static final String REVIEW_REPORTER_ID = "reviewReporterId"; - private static final String REVIEW_ASSIGNEE_ID = "reviewAssigneeId"; - private static final String REVIEW_DATA = "reviewData"; - private static final String REVIEW_MANUAL_VIOLATION = "reviewManualViolation"; - private static final String PLAN_ID = "planId"; - private static final String ISSUE_KEY = "issueKey"; - - private static final String STATUS_OPEN = "OPEN"; - private static final String STATUS_CONFIRMED = "CONFIRMED"; - private static final String UPDATED_AT = "updatedAt"; - private static final String REVIEW_TEXT = "reviewText"; - private static final String USER_ID = "userId"; - private static final String SEVERITY_MAJOR = "MAJOR"; - - private QueryRunner runner = new QueryRunner(); - - @Override - public void execute(Database db) { - Connection readConnection = null, writeConnection = null; - try { - readConnection = db.getDataSource().getConnection(); - writeConnection = db.getDataSource().getConnection(); - writeConnection.setAutoCommit(false); - truncateIssueTables(writeConnection); - convertViolations(readConnection, new Converter(db, runner, readConnection, writeConnection)); - } catch (Exception e) { - throw new IllegalStateException("Fail to convert violations to issues", e); - } finally { - DbUtils.closeQuietly(readConnection); - DbUtils.closeQuietly(writeConnection); - } - } - - private void truncateIssueTables(Connection writeConnection) throws SQLException { - // lower-case table names for SQLServer.... - runner.update(writeConnection, "TRUNCATE TABLE issues"); - runner.update(writeConnection, "TRUNCATE TABLE issue_changes"); - writeConnection.commit(); - } - - private void convertViolations(Connection readConnection, Converter converter) throws SQLException { - runner.query(readConnection, "select id from rule_failures", new ViolationIdHandler(converter)); - } - - - /** - * Browse violation ids and process them by groups of {@link #GROUP_SIZE}. - */ - private static class ViolationIdHandler implements ResultSetHandler { - private Converter converter; - private Object[] violationIds = new Object[GROUP_SIZE]; - private int cursor = 0; - - private ViolationIdHandler(Converter converter) { - this.converter = converter; - } - - @Override - public Object handle(ResultSet rs) throws SQLException { - int total = 0; - while (rs.next()) { - long violationId = rs.getLong(1); - violationIds[cursor++] = violationId; - if (cursor == GROUP_SIZE) { - convert(); - Arrays.fill(violationIds, -1L); - cursor = 0; - } - total++; - } - if (cursor > 0) { - convert(); - } - LoggerFactory.getLogger(getClass()).info(String.format("%d violations migrated to issues", total)); - return null; - } - - private void convert() throws SQLException { - if (cursor > 0) { - converter.convert(violationIds); - } - } - } - - private static class Converter { - private static final long ONE_YEAR = 365L * 24 * 60 * 60 * 1000; - private String insertSql; - private String insertChangeSql; - private Date oneYearAgo = new Date(System.currentTimeMillis() - ONE_YEAR); - private QueryRunner runner; - private Connection readConnection, writeConnection; - private Map loginsByUserId; - private Map plansById; - - private Converter(Database database, QueryRunner runner, Connection readConnection, Connection writeConnection) throws SQLException { - this.runner = runner; - this.readConnection = readConnection; - this.writeConnection = writeConnection; - initInsertSql(database); - initUsers(); - initPlans(); - } - - private void initInsertSql(Database database) { - String issuesColumnsWithoutId = "kee, component_id, root_component_id, rule_id, severity, manual_severity, message, line, effort_to_fix, status, resolution, " + - "checksum, reporter, assignee, action_plan_key, issue_attributes, issue_creation_date, issue_update_date, created_at, updated_at"; - - if (Oracle.ID.equals(database.getDialect().getId())) { - insertSql = "INSERT INTO issues(id, "+ issuesColumnsWithoutId + ") " + - " VALUES (issues_seq.nextval, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - insertChangeSql = "INSERT INTO issue_changes(id, kee, issue_key, user_login, change_type, change_data, created_at, updated_at) " + - " VALUES (issue_changes_seq.nextval, ?, ?, ?, 'comment', ?, ?, ?)"; - } else { - insertSql = "INSERT INTO issues("+ issuesColumnsWithoutId + ") " + - " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - insertChangeSql = "INSERT INTO issue_changes(kee, issue_key, user_login, change_type, change_data, created_at, updated_at) VALUES (?, ?, ?, 'comment', ?, ?, ?)"; - } - } - - - private void initUsers() throws SQLException { - loginsByUserId = selectLongString("select id,login from users"); - } - - private void initPlans() throws SQLException { - plansById = selectLongString("select id,kee from action_plans"); - } - - private Map selectLongString(String sql) throws SQLException { - return runner.query(readConnection, sql, new ResultSetHandler>() { - @Override - public Map handle(ResultSet rs) throws SQLException { - Map map = Maps.newHashMap(); - while (rs.next()) { - map.put(rs.getLong(1), rs.getString(2)); - } - return map; - } - }); - } - - /** - * Convert a group of maximum {@link #GROUP_SIZE} violations to issues - */ - void convert(Object[] violationIds) throws SQLException { - List> rows = runner.query(readConnection, ViolationHandler.SQL, new ViolationHandler(), violationIds); - List allParams = Lists.newArrayList(); - List> allComments = Lists.newArrayList(); - - for (Map row : rows) { - Long componentId = (Long) row.get(PROJECT_ID); - if (componentId == null) { - continue; - } - String issueKey = UUID.randomUUID().toString(); - String status, severity, reporter = null; - boolean manualSeverity; - Object createdAt = Objects.firstNonNull(row.get(CREATED_AT), oneYearAgo); - Object updatedAt; - Long reviewId = (Long) row.get(REVIEW_ID); - if (reviewId == null) { - // violation without review - status = STATUS_OPEN; - manualSeverity = false; - severity = (String) row.get(SEVERITY); - updatedAt = createdAt; - } else { - // violation + review - String reviewStatus = (String) row.get(REVIEW_STATUS); - status = (STATUS_OPEN.equals(reviewStatus) ? STATUS_CONFIRMED : reviewStatus); - manualSeverity = Objects.firstNonNull((Boolean) row.get(REVIEW_MANUAL_SEVERITY), false); - severity = (String) row.get(REVIEW_SEVERITY); - updatedAt = Objects.firstNonNull(row.get(REVIEW_UPDATED_AT), oneYearAgo); - if ((Boolean) row.get(REVIEW_MANUAL_VIOLATION)) { - reporter = login((Long) row.get(REVIEW_REPORTER_ID)); - } - - List> comments = runner.query(readConnection, ReviewCommentsHandler.SQL + reviewId, new ReviewCommentsHandler()); - for (Map comment : comments) { - comment.put(ISSUE_KEY, issueKey); - allComments.add(comment); - } - } - - Object[] params = new Object[20]; - params[0] = issueKey; - params[1] = componentId; - params[2] = row.get(ROOT_PROJECT_ID); - params[3] = row.get(RULE_ID); - params[4] = severity; - params[5] = manualSeverity; - params[6] = row.get(MESSAGE); - params[7] = row.get(LINE); - params[8] = row.get(COST); - params[9] = status; - params[10] = row.get(REVIEW_RESOLUTION); - params[11] = row.get(CHECKSUM); - params[12] = reporter; - params[13] = login((Long) row.get(REVIEW_ASSIGNEE_ID)); - params[14] = plan((Long) row.get(PLAN_ID)); - params[15] = row.get(REVIEW_DATA); - params[16] = createdAt; - params[17] = updatedAt; - params[18] = createdAt; - params[19] = updatedAt; - - allParams.add(params); - } - runner.batch(writeConnection, insertSql, allParams.toArray(new Object[allParams.size()][])); - writeConnection.commit(); - - insertComments(writeConnection, allComments); - } - - private void insertComments(Connection writeConnection, List> comments) throws SQLException { - List allParams = Lists.newArrayList(); - - for (Map comment : comments) { - String login = login((Long) comment.get(USER_ID)); - if (login != null) { - Object[] params = new Object[6]; - params[0] = UUID.randomUUID().toString(); - params[1] = comment.get(ISSUE_KEY); - params[2] = login; - params[3] = comment.get(REVIEW_TEXT); - params[4] = comment.get(CREATED_AT); - params[5] = comment.get(UPDATED_AT); - allParams.add(params); - } - } - if (!allParams.isEmpty()) { - runner.batch(writeConnection, insertChangeSql, allParams.toArray(new Object[allParams.size()][])); - writeConnection.commit(); - } - } - - @CheckForNull - private String login(@Nullable Long userId) { - if (userId != null) { - return loginsByUserId.get(userId); - } - return null; - } - - @CheckForNull - private String plan(@Nullable Long planId) { - if (planId != null) { - return plansById.get(planId); - } - return null; - } - } - - - private static class ViolationHandler extends AbstractListHandler> { - static final String SQL; - static { - StringBuilder sb = new StringBuilder("select rev.id as reviewId, s.project_id as projectId, rf.rule_id as ruleId, " + - " rf.failure_level as failureLevel, rf.message as message, rf.line as line, " + - " rf.cost as cost, rf.created_at as createdAt, rf.checksum as checksum, rev.user_id as reviewReporterId, " + - " rev.assignee_id as reviewAssigneeId, rev.status as reviewStatus, " + - " rev.severity as reviewSeverity, rev.resolution as reviewResolution, rev.manual_severity as reviewManualSeverity, " + - " rev.data as reviewData, rev.updated_at as reviewUpdatedAt, " + - " s.root_project_id as rootProjectId, rev.manual_violation as reviewManualViolation, planreviews.action_plan_id as planId " + - " from rule_failures rf " + - " inner join snapshots s on s.id=rf.snapshot_id " + - " left join reviews rev on rev.rule_failure_permanent_id=rf.permanent_id " + - " left join action_plans_reviews planreviews on planreviews.review_id=rev.id " + - " where "); - for (int i = 0; i < GROUP_SIZE; i++) { - if (i > 0) { - sb.append(" or "); - } - sb.append("rf.id=?"); - } - SQL = sb.toString(); - } - - private static final Map SEVERITIES = ImmutableMap.of(1, Severity.INFO, 2, Severity.MINOR, 3, Severity.MAJOR, 4, Severity.CRITICAL, 5, Severity.BLOCKER); - - @Override - protected Map handleRow(ResultSet rs) throws SQLException { - Map map = Maps.newHashMap(); - map.put(REVIEW_ID, getLong(rs, REVIEW_ID)); - map.put(PROJECT_ID, getLong(rs, PROJECT_ID)); - map.put(ROOT_PROJECT_ID, getLong(rs, ROOT_PROJECT_ID)); - map.put(RULE_ID, getLong(rs, RULE_ID)); - map.put(SEVERITY, Objects.firstNonNull(SEVERITIES.get(getInt(rs, "failureLevel")), SEVERITY_MAJOR)); - map.put(MESSAGE, rs.getString(MESSAGE)); - map.put(LINE, getInt(rs, LINE)); - map.put(COST, getDouble(rs, COST)); - map.put(CHECKSUM, rs.getString(CHECKSUM)); - map.put(CREATED_AT, rs.getTimestamp(CREATED_AT)); - map.put(REVIEW_RESOLUTION, rs.getString(REVIEW_RESOLUTION)); - map.put(REVIEW_SEVERITY, Objects.firstNonNull(rs.getString(REVIEW_SEVERITY), SEVERITY_MAJOR)); - map.put(REVIEW_STATUS, rs.getString(REVIEW_STATUS)); - map.put(REVIEW_REPORTER_ID, getLong(rs, REVIEW_REPORTER_ID)); - map.put(REVIEW_ASSIGNEE_ID, getLong(rs, REVIEW_ASSIGNEE_ID)); - map.put(REVIEW_DATA, rs.getString(REVIEW_DATA)); - map.put(REVIEW_MANUAL_SEVERITY, rs.getBoolean(REVIEW_MANUAL_SEVERITY)); - map.put(REVIEW_UPDATED_AT, rs.getTimestamp(REVIEW_UPDATED_AT)); - map.put(REVIEW_MANUAL_VIOLATION, rs.getBoolean(REVIEW_MANUAL_VIOLATION)); - map.put(PLAN_ID, getLong(rs, PLAN_ID)); - return map; - } - } - - private static class ReviewCommentsHandler extends AbstractListHandler> { - static final String SQL = "select created_at as createdAt, updated_at as updatedAt, user_id as userId, review_text as reviewText from review_comments where review_id="; - - @Override - protected Map handleRow(ResultSet rs) throws SQLException { - Map map = Maps.newHashMap(); - map.put(CREATED_AT, rs.getTimestamp(CREATED_AT)); - map.put(UPDATED_AT, rs.getTimestamp(UPDATED_AT)); - map.put(USER_ID, getLong(rs, USER_ID)); - map.put(REVIEW_TEXT, rs.getString(REVIEW_TEXT)); - return map; - } - } - - @CheckForNull - static Long getLong(ResultSet rs, String columnName) throws SQLException { - long l = rs.getLong(columnName); - return rs.wasNull() ? null : l; - } - - @CheckForNull - static Double getDouble(ResultSet rs, String columnName) throws SQLException { - double d = rs.getDouble(columnName); - return rs.wasNull() ? null : d; - } - - @CheckForNull - static Integer getInt(ResultSet rs, String columnName) throws SQLException { - int i = rs.getInt(columnName); - return rs.wasNull() ? null : i; - } - -} diff --git a/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigration.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigration.java similarity index 83% rename from sonar-server/src/main/java/org/sonar/server/db/DatabaseMigration.java rename to sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigration.java index 2e99e4b30bb..4126138c7d7 100644 --- a/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigration.java +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigration.java @@ -17,16 +17,14 @@ * 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.server.db; - -import org.sonar.core.persistence.Database; +package org.sonar.server.db.migrations; /** - * Java alternative of ActiveRecord::Migration. + * Java alternative of ActiveRecord::Migration. Do not forget to declare implementation classes in {@link DatabaseMigrations#CLASSES} * @since 3.7 */ public interface DatabaseMigration { - void execute(Database db); + void execute(); } diff --git a/sonar-server/src/test/java/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigrations.java similarity index 64% rename from sonar-server/src/test/java/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest.java rename to sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigrations.java index c2ab498add3..982042cec98 100644 --- a/sonar-server/src/test/java/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest.java +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigrations.java @@ -19,21 +19,15 @@ */ package org.sonar.server.db.migrations; -import org.junit.Rule; -import org.junit.Test; -import org.sonar.core.persistence.TestDatabase; +import com.google.common.collect.ImmutableList; +import org.sonar.server.db.migrations.violation.ViolationMigration; -public class ConvertViolationsToIssuesTest { +import java.util.List; - @Rule - public TestDatabase db = new TestDatabase().schema(getClass(), "schema.sql"); +public interface DatabaseMigrations { - @Test - public void convert_violations() throws Exception { - db.prepareDbUnit(getClass(), "convert_violations.xml"); + List> CLASSES = ImmutableList.>of( + ViolationMigration.class + ); - new ConvertViolationsToIssues().execute(db.database()); - - db.assertDbUnit(getClass(), "convert_violations_result.xml", "issues", "issue_changes"); - } } diff --git a/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigrator.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigrator.java similarity index 82% rename from sonar-server/src/main/java/org/sonar/server/db/DatabaseMigrator.java rename to sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigrator.java index 6b36e055134..2a0a165c8b7 100644 --- a/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigrator.java +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigrator.java @@ -17,7 +17,7 @@ * 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.server.db; +package org.sonar.server.db.migrations; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.dbutils.DbUtils; @@ -40,10 +40,12 @@ public class DatabaseMigrator implements ServerComponent { private final MyBatis myBatis; private final Database database; + private final DatabaseMigration[] migrations; - public DatabaseMigrator(MyBatis myBatis, Database database) { + public DatabaseMigrator(MyBatis myBatis, Database database, DatabaseMigration[] migrations) { this.myBatis = myBatis; this.database = database; + this.migrations = migrations; } /** @@ -72,10 +74,9 @@ public class DatabaseMigrator implements ServerComponent { } public void executeMigration(String className) { + DatabaseMigration migration = getMigration(className); try { - Class migrationClass = (Class) Class.forName(className); - DatabaseMigration migration = migrationClass.newInstance(); - migration.execute(database); + migration.execute(); } catch (Exception e) { // duplication between log and exception because webapp does not correctly log initial stacktrace @@ -85,6 +86,15 @@ public class DatabaseMigrator implements ServerComponent { } } + private DatabaseMigration getMigration(String className) { + for (DatabaseMigration migration : migrations) { + if (migration.getClass().getName().equals(className)) { + return migration; + } + } + throw new IllegalArgumentException("Database migration not found: " + className); + } + @VisibleForTesting protected void createSchema(Connection connection, String dialectId) { DdlUtils.createSchema(connection, dialectId); diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Progress.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Progress.java new file mode 100644 index 00000000000..d74f6182b65 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Progress.java @@ -0,0 +1,72 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.db.migrations.violation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This task logs every minute the status of migration. It is destroyed + * when migration is finished. + */ +class Progress extends TimerTask { + + static final String THREAD_NAME = "Violation Migration Progress"; + static final long DELAY_MS = 60000L; + + private final AtomicInteger counter = new AtomicInteger(0); + private final Logger logger; + private final int totalViolations; + private final long start; + + Progress(int totalViolations, Logger logger, long startDate) { + this.totalViolations = totalViolations; + this.logger = logger; + this.start = startDate; + } + + Progress(int totalViolations) { + this(totalViolations, LoggerFactory.getLogger(Progress.class), System.currentTimeMillis()); + } + + void increment(int delta) { + counter.addAndGet(delta); + } + + @Override + public void run() { + int totalIssues = counter.get(); + long durationMinutes = (System.currentTimeMillis() - start) / 60000L; + int percents = (100 * totalIssues) / totalViolations; + if (totalIssues>0 && durationMinutes > 0) { + int frequency = (int) (totalIssues / durationMinutes); + int remaining = (totalViolations - totalIssues) / frequency; + logger.info(String.format( + "%d%% [%d/%d violations, %d minutes remaining]", percents, totalIssues, totalViolations, remaining) + ); + } else { + logger.info(String.format("%d%% [%d/%d violations]", percents, totalIssues, totalViolations)); + } + + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Referentials.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Referentials.java new file mode 100644 index 00000000000..2260b160f4d --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Referentials.java @@ -0,0 +1,135 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.db.migrations.violation; + +import com.google.common.collect.Maps; +import org.apache.commons.dbutils.DbUtils; +import org.apache.commons.dbutils.QueryRunner; +import org.apache.commons.dbutils.ResultSetHandler; +import org.sonar.core.persistence.Database; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Data loaded from database before migrating violations. It is + * shared amongst converter parallel tasks. + */ +class Referentials { + + static final int VIOLATION_GROUP_SIZE = 1000; + + private final Map loginsByUserId; + private final Map plansById; + private final Queue groupsOfViolationIds; + + // int is enough, it allows to upgrade up to 2 billions violations ! + private int totalViolations = 0; + + Referentials(Database database) throws SQLException { + loginsByUserId = selectLongString(database, "select id,login from users"); + plansById = selectLongString(database, "select id,kee from action_plans"); + groupsOfViolationIds = initGroupOfViolationIds(database); + } + + @CheckForNull + String actionPlan(@Nullable Long id) { + return id != null ? plansById.get(id) : null; + } + + @CheckForNull + String userLogin(@Nullable Long id) { + return id != null ? loginsByUserId.get(id) : null; + } + + int totalViolations() { + return totalViolations; + } + + Long[] pollGroupOfViolationIds() { + long[] longs = groupsOfViolationIds.poll(); + if (longs == null) { + return new Long[0]; + } + Long[] objects = new Long[longs.length]; + for (int i = 0; i < longs.length; i++) { + objects[i] = Long.valueOf(longs[i]); + } + return objects; + } + + private Map selectLongString(Database database, String sql) throws SQLException { + Connection connection = database.getDataSource().getConnection(); + try { + return new QueryRunner().query(connection, sql, new ResultSetHandler>() { + @Override + public Map handle(ResultSet rs) throws SQLException { + Map map = Maps.newHashMap(); + while (rs.next()) { + map.put(rs.getLong(1), rs.getString(2)); + } + return map; + } + }); + } finally { + DbUtils.closeQuietly(connection); + } + } + + private Queue initGroupOfViolationIds(Database database) throws SQLException { + Connection connection = database.getDataSource().getConnection(); + Statement stmt = null; + ResultSet rs = null; + try { + connection.setAutoCommit(false); + stmt = connection.createStatement(); + stmt.setFetchSize(10000); + rs = stmt.executeQuery("select id from rule_failures"); + ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); + + totalViolations = 0; + long[] block = new long[VIOLATION_GROUP_SIZE]; + int cursor = 0; + while (rs.next()) { + block[cursor++] = rs.getLong(1); + totalViolations++; + if (cursor == VIOLATION_GROUP_SIZE) { + queue.add(block); + block = new long[VIOLATION_GROUP_SIZE]; + cursor = 0; + } + } + if (cursor > 0) { + queue.add(block); + } + return queue; + } finally { + DbUtils.closeQuietly(connection, stmt, rs); + } + } + +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/SqlUtil.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/SqlUtil.java new file mode 100644 index 00000000000..df1a9d867ae --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/SqlUtil.java @@ -0,0 +1,63 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.db.migrations.violation; + +import org.slf4j.Logger; + +import javax.annotation.CheckForNull; +import java.sql.ResultSet; +import java.sql.SQLException; + +class SqlUtil { + + private SqlUtil() { + // only static methods + } + + /** + * Logback does not log exceptions associated to {@link java.sql.SQLException#getNextException()}. + * See http://jira.qos.ch/browse/LOGBACK-775 + */ + static void log(Logger logger, SQLException e) { + SQLException next = e.getNextException(); + while (next != null) { + logger.error("SQL error: {}. Message: {}", next.getSQLState(), next.getMessage()); + next = next.getNextException(); + } + } + + @CheckForNull + static Long getLong(ResultSet rs, String columnName) throws SQLException { + long l = rs.getLong(columnName); + return rs.wasNull() ? null : l; + } + + @CheckForNull + static Double getDouble(ResultSet rs, String columnName) throws SQLException { + double d = rs.getDouble(columnName); + return rs.wasNull() ? null : d; + } + + @CheckForNull + static Integer getInt(ResultSet rs, String columnName) throws SQLException { + int i = rs.getInt(columnName); + return rs.wasNull() ? null : i; + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverter.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverter.java new file mode 100644 index 00000000000..de525fc2b31 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverter.java @@ -0,0 +1,309 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.db.migrations.violation; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.apache.commons.dbutils.DbUtils; +import org.apache.commons.dbutils.QueryRunner; +import org.apache.commons.dbutils.handlers.AbstractListHandler; +import org.sonar.api.rule.Severity; +import org.sonar.core.persistence.Database; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Callable; + +class ViolationConverter implements Callable { + + private static final long ONE_YEAR = 365L * 24 * 60 * 60 * 1000; + private static final Date ONE_YEAR_AGO = new Date(System.currentTimeMillis() - ONE_YEAR); + + private static final String PROJECT_ID = "projectId"; + private static final String CREATED_AT = "createdAt"; + private static final String REVIEW_ID = "reviewId"; + private static final String SEVERITY = "severity"; + private static final String REVIEW_STATUS = "reviewStatus"; + private static final String REVIEW_MANUAL_SEVERITY = "reviewManualSeverity"; + private static final String REVIEW_SEVERITY = "reviewSeverity"; + private static final String REVIEW_UPDATED_AT = "reviewUpdatedAt"; + private static final String ROOT_PROJECT_ID = "rootProjectId"; + private static final String RULE_ID = "ruleId"; + private static final String MESSAGE = "message"; + private static final String LINE = "line"; + private static final String COST = "cost"; + private static final String CHECKSUM = "checksum"; + private static final String REVIEW_RESOLUTION = "reviewResolution"; + private static final String REVIEW_REPORTER_ID = "reviewReporterId"; + private static final String REVIEW_ASSIGNEE_ID = "reviewAssigneeId"; + private static final String REVIEW_DATA = "reviewData"; + private static final String REVIEW_MANUAL_VIOLATION = "reviewManualViolation"; + private static final String PLAN_ID = "planId"; + private static final String ISSUE_KEY = "issueKey"; + private static final String STATUS_OPEN = "OPEN"; + private static final String STATUS_CONFIRMED = "CONFIRMED"; + private static final String UPDATED_AT = "updatedAt"; + private static final String REVIEW_TEXT = "reviewText"; + private static final String USER_ID = "userId"; + private static final String SEVERITY_MAJOR = "MAJOR"; + + + private static final String SQL_ISSUE_COLUMNS = "kee, component_id, root_component_id, rule_id, severity, manual_severity, message, line, effort_to_fix, status, resolution, " + + "checksum, reporter, assignee, action_plan_key, issue_attributes, issue_creation_date, issue_update_date, created_at, updated_at"; + + private static final String SQL_INSERT_ISSUE = "INSERT INTO issues(" + SQL_ISSUE_COLUMNS + ")" + + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + private static final String SQL_INSERT_ISSUE_CHANGE = "INSERT INTO issue_changes(kee, issue_key, user_login, change_type, change_data, created_at, updated_at)" + + " VALUES (?, ?, ?, 'comment', ?, ?, ?)"; + + private static final String SQL_DELETE_RULE_FAILURES; + + static { + StringBuilder sb = new StringBuilder("delete rule_failures where "); + for (int i = 0; i < Referentials.VIOLATION_GROUP_SIZE; i++) { + if (i > 0) { + sb.append(" or "); + } + sb.append("id=?"); + } + SQL_DELETE_RULE_FAILURES = sb.toString(); + } + + static final String SQL_SELECT_RULE_FAILURES; + + static { + StringBuilder sb = new StringBuilder("select rev.id as reviewId, s.project_id as projectId, rf.rule_id as ruleId, " + + " rf.failure_level as failureLevel, rf.message as message, rf.line as line, " + + " rf.cost as cost, rf.created_at as createdAt, rf.checksum as checksum, rev.user_id as reviewReporterId, " + + " rev.assignee_id as reviewAssigneeId, rev.status as reviewStatus, " + + " rev.severity as reviewSeverity, rev.resolution as reviewResolution, rev.manual_severity as reviewManualSeverity, " + + " rev.data as reviewData, rev.updated_at as reviewUpdatedAt, " + + " s.root_project_id as rootProjectId, rev.manual_violation as reviewManualViolation, planreviews.action_plan_id as planId " + + " from rule_failures rf " + + " inner join snapshots s on s.id=rf.snapshot_id " + + " left join reviews rev on rev.rule_failure_permanent_id=rf.permanent_id " + + " left join action_plans_reviews planreviews on planreviews.review_id=rev.id " + + " where "); + for (int i = 0; i < Referentials.VIOLATION_GROUP_SIZE; i++) { + if (i > 0) { + sb.append(" or "); + } + sb.append("rf.id=?"); + } + SQL_SELECT_RULE_FAILURES = sb.toString(); + } + + private final Database db; + private final Referentials referentials; + private final Progress progress; + + ViolationConverter(Referentials referentials, Database db, Progress progress) { + this.referentials = referentials; + this.db = db; + this.progress = progress; + } + + @Override + public Object call() throws Exception { + // For each group of 1000 violation ids: + // - load related violations, reviews and action plans + // - in a transaction + // -- insert issues + // -- insert issue_changes if there are review comments + // -- delete violations + + Long[] violationIds = referentials.pollGroupOfViolationIds(); + while (violationIds.length>0) { + List> rows = selectRows(violationIds); + convert(rows, violationIds); + + violationIds = referentials.pollGroupOfViolationIds(); + } + return null; + } + + private List> selectRows(Long[] violationIds) throws SQLException { + Connection readConnection = null; + try { + readConnection = db.getDataSource().getConnection(); + ViolationHandler violationHandler = new ViolationHandler(); + return new QueryRunner().query(readConnection, SQL_SELECT_RULE_FAILURES, violationHandler, violationIds); + + } finally { + DbUtils.closeQuietly(readConnection); + } + } + + private void convert(List> rows, Long[] violationIds) throws SQLException { + Connection readConnection = null; + Connection writeConnection = null; + try { + readConnection = db.getDataSource().getConnection(); + writeConnection = db.getDataSource().getConnection(); + writeConnection.setAutoCommit(false); + + List allParams = Lists.newArrayList(); + List> allComments = Lists.newArrayList(); + + QueryRunner runner = new QueryRunner(); + for (Map row : rows) { + Long componentId = (Long) row.get(PROJECT_ID); + if (componentId == null) { + continue; + } + String issueKey = UUID.randomUUID().toString(); + String status, severity, reporter = null; + boolean manualSeverity; + Object createdAt = Objects.firstNonNull(row.get(CREATED_AT), ONE_YEAR_AGO); + Object updatedAt; + Long reviewId = (Long) row.get(REVIEW_ID); + if (reviewId == null) { + // violation without review + status = STATUS_OPEN; + manualSeverity = false; + severity = (String) row.get(SEVERITY); + updatedAt = createdAt; + } else { + // violation + review + String reviewStatus = (String) row.get(REVIEW_STATUS); + status = (STATUS_OPEN.equals(reviewStatus) ? STATUS_CONFIRMED : reviewStatus); + manualSeverity = Objects.firstNonNull((Boolean) row.get(REVIEW_MANUAL_SEVERITY), false); + severity = (String) row.get(REVIEW_SEVERITY); + updatedAt = Objects.firstNonNull(row.get(REVIEW_UPDATED_AT), ONE_YEAR_AGO); + if ((Boolean) row.get(REVIEW_MANUAL_VIOLATION)) { + reporter = referentials.userLogin((Long) row.get(REVIEW_REPORTER_ID)); + } + + List> comments = runner.query(readConnection, ReviewCommentsHandler.SQL + reviewId, new ReviewCommentsHandler()); + for (Map comment : comments) { + comment.put(ISSUE_KEY, issueKey); + allComments.add(comment); + } + } + Object[] params = new Object[20]; + params[0] = issueKey; + params[1] = componentId; + params[2] = row.get(ROOT_PROJECT_ID); + params[3] = row.get(RULE_ID); + params[4] = severity; + params[5] = manualSeverity; + params[6] = row.get(MESSAGE); + params[7] = row.get(LINE); + params[8] = row.get(COST); + params[9] = status; + params[10] = row.get(REVIEW_RESOLUTION); + params[11] = row.get(CHECKSUM); + params[12] = reporter; + params[13] = referentials.userLogin((Long) row.get(REVIEW_ASSIGNEE_ID)); + params[14] = referentials.actionPlan((Long) row.get(PLAN_ID)); + params[15] = row.get(REVIEW_DATA); + params[16] = createdAt; + params[17] = updatedAt; + params[18] = createdAt; + params[19] = updatedAt; + allParams.add(params); + } + runner.batch(writeConnection, SQL_INSERT_ISSUE, allParams.toArray(new Object[allParams.size()][])); + insertComments(writeConnection, allComments); + runner.update(writeConnection, SQL_DELETE_RULE_FAILURES, violationIds); + writeConnection.commit(); + progress.increment(rows.size()); + + } finally { + DbUtils.closeQuietly(readConnection); + DbUtils.closeQuietly(writeConnection); + } + } + + private void insertComments(Connection writeConnection, List> comments) throws SQLException { + List allParams = Lists.newArrayList(); + + for (Map comment : comments) { + String login = referentials.userLogin((Long) comment.get(USER_ID)); + if (login != null) { + Object[] params = new Object[6]; + params[0] = UUID.randomUUID().toString(); + params[1] = comment.get(ISSUE_KEY); + params[2] = login; + params[3] = comment.get(REVIEW_TEXT); + params[4] = comment.get(CREATED_AT); + params[5] = comment.get(UPDATED_AT); + allParams.add(params); + } + } + if (!allParams.isEmpty()) { + new QueryRunner().batch(writeConnection, SQL_INSERT_ISSUE_CHANGE, allParams.toArray(new Object[allParams.size()][])); + } + } + + + private static class ReviewCommentsHandler extends AbstractListHandler> { + static final String SQL = "select created_at as createdAt, updated_at as updatedAt, user_id as userId, review_text as reviewText from review_comments where review_id="; + + @Override + protected Map handleRow(ResultSet rs) throws SQLException { + Map map = Maps.newHashMap(); + map.put(CREATED_AT, rs.getTimestamp(CREATED_AT)); + map.put(UPDATED_AT, rs.getTimestamp(UPDATED_AT)); + map.put(USER_ID, SqlUtil.getLong(rs, USER_ID)); + map.put(REVIEW_TEXT, rs.getString(REVIEW_TEXT)); + return map; + } + } + + private static class ViolationHandler extends AbstractListHandler> { + private static final Map SEVERITIES = ImmutableMap.of(1, Severity.INFO, 2, Severity.MINOR, 3, Severity.MAJOR, 4, Severity.CRITICAL, 5, Severity.BLOCKER); + + + @Override + protected Map handleRow(ResultSet rs) throws SQLException { + Map map = Maps.newHashMap(); + map.put(REVIEW_ID, SqlUtil.getLong(rs, REVIEW_ID)); + map.put(PROJECT_ID, SqlUtil.getLong(rs, PROJECT_ID)); + map.put(ROOT_PROJECT_ID, SqlUtil.getLong(rs, ROOT_PROJECT_ID)); + map.put(RULE_ID, SqlUtil.getLong(rs, RULE_ID)); + map.put(SEVERITY, Objects.firstNonNull(SEVERITIES.get(SqlUtil.getInt(rs, "failureLevel")), SEVERITY_MAJOR)); + map.put(MESSAGE, rs.getString(MESSAGE)); + map.put(LINE, SqlUtil.getInt(rs, LINE)); + map.put(COST, SqlUtil.getDouble(rs, COST)); + map.put(CHECKSUM, rs.getString(CHECKSUM)); + map.put(CREATED_AT, rs.getTimestamp(CREATED_AT)); + map.put(REVIEW_RESOLUTION, rs.getString(REVIEW_RESOLUTION)); + map.put(REVIEW_SEVERITY, Objects.firstNonNull(rs.getString(REVIEW_SEVERITY), SEVERITY_MAJOR)); + map.put(REVIEW_STATUS, rs.getString(REVIEW_STATUS)); + map.put(REVIEW_REPORTER_ID, SqlUtil.getLong(rs, REVIEW_REPORTER_ID)); + map.put(REVIEW_ASSIGNEE_ID, SqlUtil.getLong(rs, REVIEW_ASSIGNEE_ID)); + map.put(REVIEW_DATA, rs.getString(REVIEW_DATA)); + map.put(REVIEW_MANUAL_SEVERITY, rs.getBoolean(REVIEW_MANUAL_SEVERITY)); + map.put(REVIEW_UPDATED_AT, rs.getTimestamp(REVIEW_UPDATED_AT)); + map.put(REVIEW_MANUAL_VIOLATION, rs.getBoolean(REVIEW_MANUAL_VIOLATION)); + map.put(PLAN_ID, SqlUtil.getLong(rs, PLAN_ID)); + return map; + } + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverters.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverters.java new file mode 100644 index 00000000000..640903d8850 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverters.java @@ -0,0 +1,77 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.db.migrations.violation; + +import com.google.common.collect.Lists; +import org.sonar.api.config.Settings; +import org.sonar.core.persistence.Database; + +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.*; + +class ViolationConverters { + + static final int DEFAULT_THREADS = 5; + static final String THREADS_PROPERTY = "sonar.violationMigration.threads"; + private final Settings settings; + + ViolationConverters(Settings settings) { + this.settings = settings; + } + + void execute(Referentials referentials, Database db) throws Exception { + Progress progress = new Progress(referentials.totalViolations()); + + List> converters = Lists.newArrayList(); + for (int i = 0; i < numberOfThreads(); i++) { + converters.add(new ViolationConverter(referentials, db, progress)); + } + + doExecute(progress, converters); + } + + void doExecute(TimerTask progress, List> converters) throws InterruptedException, ExecutionException { + Timer timer = new Timer(Progress.THREAD_NAME); + timer.schedule(progress, Progress.DELAY_MS, Progress.DELAY_MS); + try { + ExecutorService executor = Executors.newFixedThreadPool(converters.size()); + List> results = executor.invokeAll(converters); + executor.shutdown(); + for (Future result : results) { + result.get(); + } + } finally { + timer.cancel(); + } + } + + int numberOfThreads() { + int threads = settings.getInt(THREADS_PROPERTY); + if (threads < 0) { + throw new IllegalArgumentException(String.format("Bad value of %s: %d", THREADS_PROPERTY, threads)); + } + if (threads == 0) { + threads = DEFAULT_THREADS; + } + return threads; + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationMigration.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationMigration.java new file mode 100644 index 00000000000..88c60a8e815 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationMigration.java @@ -0,0 +1,75 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.db.migrations.violation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.config.Settings; +import org.sonar.api.utils.MessageException; +import org.sonar.core.persistence.Database; +import org.sonar.server.db.migrations.DatabaseMigration; + +import java.sql.SQLException; + +/** + * Used in the Active Record Migration 401 + */ +public class ViolationMigration implements DatabaseMigration { + + private final Settings settings; + + private Logger logger = LoggerFactory.getLogger(ViolationMigration.class); + private final Database db; + + public ViolationMigration(Database database, Settings settings) { + this.db = database; + this.settings = settings; + } + + @Override + public void execute() { + try { + migrate(); + + } catch (SQLException e) { + logger.error("Fail to convert violations to issues", e); + SqlUtil.log(logger, e); + throw MessageException.of("Fail to convert violations to issues"); + + } catch (Exception e) { + logger.error("Fail to convert violations to issues", e); + throw MessageException.of("Fail to convert violations to issues"); + } + } + + public void migrate() throws Exception { + logger.info("Initialize input"); + Referentials referentials = new Referentials(db); + + if (referentials.totalViolations() > 0) { + logger.info("Migrate {} violations", referentials.totalViolations()); + + ViolationConverters converters = new ViolationConverters(settings); + converters.execute(referentials, db); + } + } + + +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/package-info.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/package-info.java new file mode 100644 index 00000000000..969ef6eed7e --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/package-info.java @@ -0,0 +1,24 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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. + */ + +@ParametersAreNonnullByDefault +package org.sonar.server.db.migrations.violation; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java index 55f07767e23..e6cabb36702 100644 --- a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java +++ b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java @@ -81,7 +81,9 @@ import org.sonar.server.component.DefaultComponentFinder; import org.sonar.server.component.DefaultRubyComponentService; import org.sonar.server.configuration.Backup; import org.sonar.server.configuration.ProfilesManager; -import org.sonar.server.db.DatabaseMigrator; +import org.sonar.server.db.migrations.DatabaseMigration; +import org.sonar.server.db.migrations.DatabaseMigrations; +import org.sonar.server.db.migrations.DatabaseMigrator; import org.sonar.server.db.EmbeddedDatabaseFactory; import org.sonar.server.issue.ActionPlanService; import org.sonar.server.issue.ActionService; @@ -224,6 +226,9 @@ public final class Platform { rootContainer.addSingleton(MyBatis.class); rootContainer.addSingleton(DefaultServerUpgradeStatus.class); rootContainer.addSingleton(DatabaseServerCompatibility.class); + for (Class migrationClass : DatabaseMigrations.CLASSES) { + rootContainer.addSingleton(migrationClass); + } rootContainer.addSingleton(DatabaseMigrator.class); rootContainer.addSingleton(DatabaseVersion.class); for (Class daoClass : DaoUtils.getDaoClasses()) { diff --git a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java index 340eb9ef3a1..807cb564be7 100644 --- a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java +++ b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java @@ -57,7 +57,7 @@ import org.sonar.core.resource.ResourceKeyUpdaterDao; import org.sonar.core.timemachine.Periods; import org.sonar.server.configuration.Backup; import org.sonar.server.configuration.ProfilesManager; -import org.sonar.server.db.DatabaseMigrator; +import org.sonar.server.db.migrations.DatabaseMigrator; import org.sonar.server.platform.Platform; import org.sonar.server.platform.ServerIdGenerator; import org.sonar.server.platform.ServerSettings; diff --git a/sonar-server/src/main/webapp/WEB-INF/config/environment.rb b/sonar-server/src/main/webapp/WEB-INF/config/environment.rb index 3d6ea3cf3d1..32cfe4c771e 100644 --- a/sonar-server/src/main/webapp/WEB-INF/config/environment.rb +++ b/sonar-server/src/main/webapp/WEB-INF/config/environment.rb @@ -163,6 +163,11 @@ class ActiveRecord::Migration execute_ddl("drop trigger #{trigger_name}", "DROP TRIGGER #{trigger_name}") end + def self.write(text="") + # See migration.rb, the method write directly calls "puts" + Java::OrgSlf4j::LoggerFactory::getLogger('DbMigration').info(text) if verbose + end + private def self.execute_ddl(message, ddl) diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/401_migrate_violations_to_issues.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/401_migrate_violations_to_issues.rb index f1e7a5cd349..20bb8d034a6 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/401_migrate_violations_to_issues.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/401_migrate_violations_to_issues.rb @@ -25,7 +25,7 @@ class MigrateViolationsToIssues < ActiveRecord::Migration def self.up - Java::OrgSonarServerUi::JRubyFacade.getInstance().databaseMigrator().executeMigration('org.sonar.server.db.migrations.ConvertViolationsToIssues') + Java::OrgSonarServerUi::JRubyFacade.getInstance().databaseMigrator().executeMigration('org.sonar.server.db.migrations.violation.ViolationMigration') # Currently not possible in Java because of Oracle (triggers and sequences must be dropped) drop_table('rule_failures') @@ -34,4 +34,4 @@ class MigrateViolationsToIssues < ActiveRecord::Migration drop_table('action_plans_reviews') end -end \ No newline at end of file +end diff --git a/sonar-server/src/test/java/org/sonar/server/db/DatabaseMigratorTest.java b/sonar-server/src/test/java/org/sonar/server/db/migrations/DatabaseMigratorTest.java similarity index 84% rename from sonar-server/src/test/java/org/sonar/server/db/DatabaseMigratorTest.java rename to sonar-server/src/test/java/org/sonar/server/db/migrations/DatabaseMigratorTest.java index 2cc20b3c5e3..a8e3d06d87e 100644 --- a/sonar-server/src/test/java/org/sonar/server/db/DatabaseMigratorTest.java +++ b/sonar-server/src/test/java/org/sonar/server/db/migrations/DatabaseMigratorTest.java @@ -17,7 +17,7 @@ * 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.server.db; +package org.sonar.server.db.migrations; import org.apache.ibatis.session.SqlSession; import org.junit.Rule; @@ -42,12 +42,13 @@ public class DatabaseMigratorTest extends AbstractDaoTestCase { MyBatis mybatis = mock(MyBatis.class); Database database = mock(Database.class); + DatabaseMigration[] migrations = new DatabaseMigration[]{new FakeMigration()}; @Test public void should_support_only_creation_of_h2_database() throws Exception { when(database.getDialect()).thenReturn(new MySql()); - DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database); + DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database, migrations); assertThat(migrator.createDatabase()).isFalse(); verifyZeroInteractions(mybatis); @@ -55,25 +56,16 @@ public class DatabaseMigratorTest extends AbstractDaoTestCase { @Test public void fail_if_execute_unknown_migration() throws Exception { - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Fail to execute database migration: org.xxx.UnknownMigration"); + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Database migration not found: org.xxx.UnknownMigration"); - DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database); + DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database, migrations); migrator.executeMigration("org.xxx.UnknownMigration"); } - @Test - public void fail_if_execute_not_a_migration() throws Exception { - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Fail to execute database migration: java.lang.String"); - - DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database); - migrator.executeMigration("java.lang.String"); - } - @Test public void execute_migration() throws Exception { - DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database); + DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database, migrations); assertThat(FakeMigration.executed).isFalse(); migrator.executeMigration(FakeMigration.class.getName()); assertThat(FakeMigration.executed).isTrue(); @@ -89,7 +81,7 @@ public class DatabaseMigratorTest extends AbstractDaoTestCase { when(session.getConnection()).thenReturn(connection); when(mybatis.openSession()).thenReturn(session); - DatabaseMigrator databaseMigrator = new DatabaseMigrator(mybatis, database) { + DatabaseMigrator databaseMigrator = new DatabaseMigrator(mybatis, database, migrations) { @Override protected void createSchema(Connection connection, String dialectId) { } @@ -101,7 +93,7 @@ public class DatabaseMigratorTest extends AbstractDaoTestCase { public static class FakeMigration implements DatabaseMigration { static boolean executed = false; @Override - public void execute(Database db) { + public void execute() { executed = true; } } diff --git a/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ProgressTest.java b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ProgressTest.java new file mode 100644 index 00000000000..87c0e2fadb1 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ProgressTest.java @@ -0,0 +1,64 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.db.migrations.violation; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.slf4j.Logger; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class ProgressTest { + @Test + public void log_progress() throws Exception { + Logger logger = mock(Logger.class); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + + Progress progress = new Progress(5000, logger, System.currentTimeMillis()); + progress.run(); + progress.increment(200); + progress.increment(130); + progress.run(); + progress.increment(1670); + progress.run(); + + verify(logger, times(3)).info(argument.capture()); + assertThat(argument.getAllValues().get(0)).isEqualTo("0% [0/5000 violations]"); + assertThat(argument.getAllValues().get(1)).isEqualTo("6% [330/5000 violations]"); + assertThat(argument.getAllValues().get(2)).isEqualTo("40% [2000/5000 violations]"); + } + + @Test + public void log_remaining_time() throws Exception { + Logger logger = mock(Logger.class); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + + long fiveMinutesAgo = System.currentTimeMillis() - 5 * 60 * 1000; + Progress progress = new Progress(5000, logger, fiveMinutesAgo); + progress.increment(2000); + progress.run(); + + verify(logger).info(argument.capture()); + assertThat(argument.getValue()).isEqualTo("40% [2000/5000 violations, 7 minutes remaining]"); + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/SqlUtilTest.java b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/SqlUtilTest.java new file mode 100644 index 00000000000..69c15933875 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/SqlUtilTest.java @@ -0,0 +1,43 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.db.migrations.violation; + +import org.junit.Test; +import org.slf4j.Logger; + +import java.sql.SQLException; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class SqlUtilTest { + + @Test + public void log_all_sql_exceptions() { + SQLException root = new SQLException("this is root", "123"); + SQLException next = new SQLException("this is next", "456"); + root.setNextException(next); + + Logger logger = mock(Logger.class); + SqlUtil.log(logger, root); + + verify(logger).error("SQL error: {}. Message: {}", "456", "this is next"); + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationConvertersTest.java b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationConvertersTest.java new file mode 100644 index 00000000000..9b144fe7846 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationConvertersTest.java @@ -0,0 +1,82 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.db.migrations.violation; + +import com.google.common.collect.Lists; +import org.junit.Test; +import org.sonar.api.config.Settings; + +import java.util.List; +import java.util.TimerTask; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ViolationConvertersTest { + + @Test + public void default_number_of_threads() throws Exception { + assertThat(new ViolationConverters(new Settings()).numberOfThreads()).isEqualTo(ViolationConverters.DEFAULT_THREADS); + } + + @Test + public void configure_number_of_threads() throws Exception { + Settings settings = new Settings(); + settings.setProperty(ViolationConverters.THREADS_PROPERTY, 2); + assertThat(new ViolationConverters(settings).numberOfThreads()).isEqualTo(2); + } + + @Test + public void number_of_threads_should_not_be_negative() throws Exception { + try { + Settings settings = new Settings(); + settings.setProperty(ViolationConverters.THREADS_PROPERTY, -2); + new ViolationConverters(settings).numberOfThreads(); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).isEqualTo("Bad value of " + ViolationConverters.THREADS_PROPERTY + ": -2"); + } + } + + @Test + public void propagate_converter_failure() throws Exception { + Callable callable = mock(Callable.class); + when(callable.call()).thenThrow(new IllegalStateException("Need to cry")); + + List> callables = Lists.newArrayList(callable); + try { + new ViolationConverters(new Settings()).doExecute(new FakeTimerTask(), callables); + fail(); + } catch (ExecutionException e) { + assertThat(e.getCause().getMessage()).isEqualTo("Need to cry"); + } + + } + + static class FakeTimerTask extends TimerTask { + @Override + public void run() { + } + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationMigrationTest.java b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationMigrationTest.java new file mode 100644 index 00000000000..360e699a802 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationMigrationTest.java @@ -0,0 +1,66 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.db.migrations.violation; + +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.Settings; +import org.sonar.core.persistence.TestDatabase; + +import java.util.Set; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; + +public class ViolationMigrationTest { + + @Rule + public TestDatabase db = new TestDatabase().schema(getClass(), "schema.sql"); + + @Test + public void migrate_violations() throws Exception { + db.prepareDbUnit(getClass(), "migrate_violations.xml"); + + new ViolationMigration(db.database(), new Settings()).execute(); + + db.assertDbUnit(getClass(), "migrate_violations_result.xml", "issues", "issue_changes"); + assertMigrationEnded(); + } + + @Test + public void no_violations_to_migrate() throws Exception { + db.prepareDbUnit(getClass(), "no_violations_to_migrate.xml"); + + new ViolationMigration(db.database(), new Settings()).execute(); + + db.assertDbUnit(getClass(), "no_violations_to_migrate_result.xml", "issues", "issue_changes"); + assertMigrationEnded(); + } + + private void assertMigrationEnded() { + assertThat(db.count("select count(id) from rule_failures")).isEqualTo(0); + + // Progress thread is dead + Set threads = Thread.getAllStackTraces().keySet(); + for (Thread thread : threads) { + assertThat(thread.getName()).isNotEqualTo(Progress.THREAD_NAME); + } + } +} diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations_result.xml b/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations_result.xml deleted file mode 100644 index 8c0330bcb15..00000000000 --- a/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations_result.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations.xml b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations.xml similarity index 90% rename from sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations.xml rename to sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations.xml index 5a97e0df76e..0d4a6e8ebc0 100644 --- a/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations.xml +++ b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations.xml @@ -29,13 +29,11 @@ - - - + - - \ No newline at end of file + + diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations_result.xml b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations_result.xml new file mode 100644 index 00000000000..cafbaaf179e --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations_result.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate.xml b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate.xml new file mode 100644 index 00000000000..416efbc3086 --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate_result.xml b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate_result.xml new file mode 100644 index 00000000000..416efbc3086 --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate_result.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/schema.sql b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/schema.sql similarity index 100% rename from sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/schema.sql rename to sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/schema.sql -- 2.39.5