From ab0ad0b2afd5bc1f3ca4e02f28f10533a8a4bda5 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Thu, 5 Aug 2021 19:38:43 +0100 Subject: [PATCH] Cleanup test results when run/job is deleted --- .../database/DatabaseSchemaLoader.java | 51 ++++--- .../database/DatabaseTestResultStorage.java | 43 ++++++ .../database/TestResultCleanupListener.java | 64 ++++++++ .../DatabaseTestResultStorage/config.jelly | 6 + .../help-skipCleanupRunsOnDeletion.html | 3 + .../DatabaseTestResultStorageTest.java | 138 +++++++++++++++++- 6 files changed, 282 insertions(+), 23 deletions(-) create mode 100644 src/main/java/io/jenkins/plugins/junit/storage/database/TestResultCleanupListener.java create mode 100644 src/main/resources/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorage/config.jelly create mode 100644 src/main/resources/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorage/help-skipCleanupRunsOnDeletion.html diff --git a/src/main/java/io/jenkins/plugins/junit/storage/database/DatabaseSchemaLoader.java b/src/main/java/io/jenkins/plugins/junit/storage/database/DatabaseSchemaLoader.java index 279adef..17965bf 100644 --- a/src/main/java/io/jenkins/plugins/junit/storage/database/DatabaseSchemaLoader.java +++ b/src/main/java/io/jenkins/plugins/junit/storage/database/DatabaseSchemaLoader.java @@ -2,45 +2,54 @@ import hudson.init.Initializer; import io.jenkins.plugins.junit.storage.JunitTestResultStorageConfiguration; -import java.sql.SQLException; -import javax.sql.DataSource; import org.flywaydb.core.Flyway; import org.jenkinsci.plugins.database.Database; import org.jenkinsci.plugins.database.GlobalDatabaseConfiguration; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; +import javax.sql.DataSource; +import java.util.logging.Level; +import java.util.logging.Logger; + import static hudson.init.InitMilestone.SYSTEM_CONFIG_ADAPTED; @Restricted(NoExternalUse.class) public class DatabaseSchemaLoader { - + + private static final Logger LOGGER = Logger.getLogger(DatabaseSchemaLoader.class.getName()); + static boolean MIGRATED; @Initializer(after = SYSTEM_CONFIG_ADAPTED) - public static void migrateSchema() throws SQLException { + public static void migrateSchema() { JunitTestResultStorageConfiguration configuration = JunitTestResultStorageConfiguration.get(); if (configuration.getStorage() instanceof DatabaseTestResultStorage) { - DatabaseTestResultStorage storage = (DatabaseTestResultStorage) configuration.getStorage(); - DataSource dataSource = storage.getConnectionSupplier().database().getDataSource(); + try { + DatabaseTestResultStorage storage = (DatabaseTestResultStorage) configuration.getStorage(); + DataSource dataSource = storage.getConnectionSupplier().database().getDataSource(); - Database database = GlobalDatabaseConfiguration.get().getDatabase(); + Database database = GlobalDatabaseConfiguration.get().getDatabase(); - assert database != null; - String databaseDriverName = database.getClass().getName(); - String schemaLocation = "postgres"; - if (databaseDriverName.contains("mysql")) { - schemaLocation = "mysql"; + assert database != null; + String databaseDriverName = database.getClass().getName(); + String schemaLocation = "postgres"; + if (databaseDriverName.contains("mysql")) { + schemaLocation = "mysql"; + } + Flyway flyway = Flyway + .configure(DatabaseSchemaLoader.class.getClassLoader()) + .baselineOnMigrate(true) + .table("junit_flyway_schema_history") + .dataSource(dataSource) + .locations("db/migration/" + schemaLocation) + .load(); + flyway.migrate(); + MIGRATED = true; + } catch (Exception e) { + // TODO add admin monitor + LOGGER.log(Level.SEVERE, "Error migrating database, correct this error before using the junit plugin", e); } - Flyway flyway = Flyway - .configure(DatabaseSchemaLoader.class.getClassLoader()) - .baselineOnMigrate(true) - .table("junit_flyway_schema_history") - .dataSource(dataSource) - .locations("db/migration/" + schemaLocation) - .load(); - flyway.migrate(); - MIGRATED = true; } } } diff --git a/src/main/java/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorage.java b/src/main/java/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorage.java index 321c1e0..0739531 100644 --- a/src/main/java/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorage.java +++ b/src/main/java/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorage.java @@ -3,6 +3,7 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; +import hudson.ExtensionList; import hudson.Util; import hudson.model.Job; import hudson.model.Run; @@ -31,12 +32,15 @@ import java.util.Map; import java.util.Objects; import jenkins.model.Jenkins; +import net.sf.json.JSONObject; import org.apache.commons.lang3.StringUtils; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.database.Database; import org.jenkinsci.plugins.database.GlobalDatabaseConfiguration; import org.jenkinsci.remoting.SerializableOnlyOverRemoting; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.StaplerRequest; @Extension @@ -44,6 +48,8 @@ public class DatabaseTestResultStorage extends JunitTestResultStorage { private transient ConnectionSupplier connectionSupplier; + private boolean skipCleanupRunsOnDeletion; + @DataBoundConstructor public DatabaseTestResultStorage() {} @@ -54,6 +60,15 @@ public ConnectionSupplier getConnectionSupplier() { return connectionSupplier; } + public boolean isSkipCleanupRunsOnDeletion() { + return skipCleanupRunsOnDeletion; + } + + @DataBoundSetter + public void setSkipCleanupRunsOnDeletion(boolean skipCleanupRunsOnDeletion) { + this.skipCleanupRunsOnDeletion = skipCleanupRunsOnDeletion; + } + @Override public RemotePublisher createRemotePublisher(Run build) throws IOException { try { getConnectionSupplier().connection(); // make sure we start a local server and create table first @@ -269,6 +284,34 @@ private List retrieveCaseResult(String whereCondition) { }); } + + public Void deleteRun() { + return query(connection -> { + try (PreparedStatement statement = connection.prepareStatement( + "DELETE FROM caseResults WHERE job = ? AND build = ?" + )) { + statement.setString(1, job); + statement.setInt(2, build); + + statement.execute(); + } + return null; + }); + } + + public Void deleteJob() { + return query(connection -> { + try (PreparedStatement statement = connection.prepareStatement( + "DELETE FROM caseResults WHERE job = ?" + )) { + statement.setString(1, job); + + statement.execute(); + } + return null; + }); + } + @Override public List getAllPackageResults() { return query(connection -> { diff --git a/src/main/java/io/jenkins/plugins/junit/storage/database/TestResultCleanupListener.java b/src/main/java/io/jenkins/plugins/junit/storage/database/TestResultCleanupListener.java new file mode 100644 index 0000000..1322d8e --- /dev/null +++ b/src/main/java/io/jenkins/plugins/junit/storage/database/TestResultCleanupListener.java @@ -0,0 +1,64 @@ +package io.jenkins.plugins.junit.storage.database; + +import hudson.Extension; +import hudson.model.Item; +import hudson.model.Run; +import hudson.model.listeners.ItemListener; +import hudson.model.listeners.RunListener; +import io.jenkins.plugins.junit.storage.FileJunitTestResultStorage; +import io.jenkins.plugins.junit.storage.JunitTestResultStorage; +import io.jenkins.plugins.junit.storage.TestResultImpl; + +public class TestResultCleanupListener { + + @Extension + public static class RunCleanupListener extends RunListener { + @Override + public void onDeleted(Run run) { + JunitTestResultStorage junitTestResultStorage = JunitTestResultStorage.find(); + if (junitTestResultStorage instanceof FileJunitTestResultStorage) { + return; + } + + if (junitTestResultStorage instanceof DatabaseTestResultStorage) { + DatabaseTestResultStorage storage = (DatabaseTestResultStorage) junitTestResultStorage; + if (storage.isSkipCleanupRunsOnDeletion()) { + return; + } + } + + TestResultImpl testResult = junitTestResultStorage.load(run.getParent().getFullName(), run.getNumber()); + + if (testResult instanceof DatabaseTestResultStorage.TestResultStorage) { + DatabaseTestResultStorage.TestResultStorage storage = (DatabaseTestResultStorage.TestResultStorage) testResult; + + storage.deleteRun(); + } + } + } + + @Extension + public static class JobCleanupListener extends ItemListener { + @Override + public void onDeleted(Item item) { + JunitTestResultStorage junitTestResultStorage = JunitTestResultStorage.find(); + if (junitTestResultStorage instanceof FileJunitTestResultStorage) { + return; + } + + if (junitTestResultStorage instanceof DatabaseTestResultStorage) { + DatabaseTestResultStorage storage = (DatabaseTestResultStorage) junitTestResultStorage; + if (storage.isSkipCleanupRunsOnDeletion()) { + return; + } + } + + TestResultImpl testResult = junitTestResultStorage.load(item.getFullName(), 0); + + if (testResult instanceof DatabaseTestResultStorage.TestResultStorage) { + DatabaseTestResultStorage.TestResultStorage storage = (DatabaseTestResultStorage.TestResultStorage) testResult; + storage.deleteJob(); + } + } + } +} diff --git a/src/main/resources/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorage/config.jelly b/src/main/resources/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorage/config.jelly new file mode 100644 index 0000000..8fbdbf7 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorage/config.jelly @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorage/help-skipCleanupRunsOnDeletion.html b/src/main/resources/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorage/help-skipCleanupRunsOnDeletion.html new file mode 100644 index 0000000..f4526c6 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorage/help-skipCleanupRunsOnDeletion.html @@ -0,0 +1,3 @@ +

If checked tests results will not be automatically removed when a job or build is deleted.

+ +

If you need more complex test result cleanup then please raise an issue on GitHub.

\ No newline at end of file diff --git a/src/test/java/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorageTest.java b/src/test/java/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorageTest.java index c988c4d..03376fe 100644 --- a/src/test/java/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorageTest.java +++ b/src/test/java/io/jenkins/plugins/junit/storage/database/DatabaseTestResultStorageTest.java @@ -10,6 +10,7 @@ import hudson.tasks.junit.TestResultSummary; import hudson.tasks.junit.TrendTestResultSummary; import hudson.util.Secret; +import io.jenkins.plugins.junit.storage.JunitTestResultStorage; import io.jenkins.plugins.junit.storage.JunitTestResultStorageConfiguration; import io.jenkins.plugins.junit.storage.TestResultImpl; import java.io.ByteArrayInputStream; @@ -52,6 +53,7 @@ public class DatabaseTestResultStorageTest { + public static final String TEST_IMAGE = "postgres:12-alpine"; @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); @@ -60,7 +62,7 @@ public class DatabaseTestResultStorageTest { @Test public void smokes() throws Exception { - try (PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:12-alpine")) { + try (PostgreSQLContainer postgres = new PostgreSQLContainer<>(TEST_IMAGE)) { setupPlugin(postgres); r.createOnlineSlave(Label.get("remote")); @@ -173,7 +175,138 @@ public void smokes() throws Exception { } } - private void setupPlugin(PostgreSQLContainer postgres) { + @Test + public void testResultCleanup() throws Exception { + try (PostgreSQLContainer postgres = new PostgreSQLContainer<>(TEST_IMAGE)) { + setupPlugin(postgres); + + r.createOnlineSlave(Label.get("remote")); + WorkflowJob p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('remote') {\n" + + " writeFile file: 'x.xml', text: '''" + + "" + + "" + + "" + + "'''\n" + + " def s = junit 'x.xml'\n" + + " echo(/summary: fail=$s.failCount skip=$s.skipCount pass=$s.passCount total=$s.totalCount/)\n" + + " writeFile file: 'x.xml', text: '''" + + "" + + "'''\n" + + " s = junit 'x.xml'\n" + + " echo(/next summary: fail=$s.failCount skip=$s.skipCount pass=$s.passCount total=$s.totalCount/)\n" + + "}", true)); + + WorkflowRun b = p.scheduleBuild2(0).get(); + r.assertBuildStatus(Result.UNSTABLE, b); + + b = p.scheduleBuild2(0).get(); + r.assertBuildStatus(Result.UNSTABLE, b); + + b = p.scheduleBuild2(0).get(); + r.assertBuildStatus(Result.UNSTABLE, b); + + // 3 sets of test results + try (Connection connection = requireNonNull(GlobalDatabaseConfiguration.get().getDatabase()).getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT count(*) FROM caseResults"); + ResultSet result = statement.executeQuery()) { + result.next(); + int anInt = result.getInt(1); + assertThat(anInt, is(12)); + } + + b.delete(); + + // 2 sets of test results + try (Connection connection = requireNonNull(GlobalDatabaseConfiguration.get().getDatabase()).getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT count(*) FROM caseResults"); + ResultSet result = statement.executeQuery()) { + result.next(); + int anInt = result.getInt(1); + assertThat(anInt, is(8)); + } + + p.delete(); + + // 0 test results + try (Connection connection = requireNonNull(GlobalDatabaseConfiguration.get().getDatabase()).getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT count(*) FROM caseResults"); + ResultSet result = statement.executeQuery()) { + result.next(); + int anInt = result.getInt(1); + assertThat(anInt, is(0)); + } + } + } + + @Test + public void testResultCleanup_skipped_if_disabled() throws Exception { + try (PostgreSQLContainer postgres = new PostgreSQLContainer<>(TEST_IMAGE)) { + setupPlugin(postgres); + + DatabaseTestResultStorage storage = new DatabaseTestResultStorage(); + storage.setSkipCleanupRunsOnDeletion(true); + JunitTestResultStorageConfiguration.get().setStorage(storage); + + + r.createOnlineSlave(Label.get("remote")); + WorkflowJob p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('remote') {\n" + + " writeFile file: 'x.xml', text: '''" + + "" + + "" + + "" + + "'''\n" + + " def s = junit 'x.xml'\n" + + " echo(/summary: fail=$s.failCount skip=$s.skipCount pass=$s.passCount total=$s.totalCount/)\n" + + " writeFile file: 'x.xml', text: '''" + + "" + + "'''\n" + + " s = junit 'x.xml'\n" + + " echo(/next summary: fail=$s.failCount skip=$s.skipCount pass=$s.passCount total=$s.totalCount/)\n" + + "}", true)); + + WorkflowRun b = p.scheduleBuild2(0).get(); + r.assertBuildStatus(Result.UNSTABLE, b); + + + // 1 sets of test results + try (Connection connection = requireNonNull(GlobalDatabaseConfiguration.get().getDatabase()).getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT count(*) FROM caseResults"); + ResultSet result = statement.executeQuery()) { + result.next(); + int anInt = result.getInt(1); + assertThat(anInt, is(4)); + } + + b.delete(); + + // 1 set of test results + try (Connection connection = requireNonNull(GlobalDatabaseConfiguration.get().getDatabase()).getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT count(*) FROM caseResults"); + ResultSet result = statement.executeQuery()) { + result.next(); + int anInt = result.getInt(1); + assertThat(anInt, is(4)); + } + + p.delete(); + + // 1 set of test results + try (Connection connection = requireNonNull(GlobalDatabaseConfiguration.get().getDatabase()).getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT count(*) FROM caseResults"); + ResultSet result = statement.executeQuery()) { + result.next(); + int anInt = result.getInt(1); + assertThat(anInt, is(4)); + } + } + } + + + private void setupPlugin(PostgreSQLContainer postgres) throws SQLException { // comment this out if you hit the below test containers issue postgres.start(); @@ -184,6 +317,7 @@ private void setupPlugin(PostgreSQLContainer postgres) { database.setValidationQuery("SELECT 1"); GlobalDatabaseConfiguration.get().setDatabase(database); JunitTestResultStorageConfiguration.get().setStorage(new DatabaseTestResultStorage()); + DatabaseSchemaLoader.migrateSchema(); } // https://gist.github.com/mikbuch/299568988fa7997cb28c7c84309232b1