diff --git a/pom.xml b/pom.xml index 3e4216428..7828d26d7 100644 --- a/pom.xml +++ b/pom.xml @@ -102,6 +102,11 @@ org.jenkins-ci.plugins display-url-api + + org.jenkins-ci.plugins + junit + true + org.jenkins-ci.plugins branch-api diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java index 385f53706..fa12ff2c7 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java @@ -55,6 +55,50 @@ public String toString() { } } + /** + * A summary of the passed, failed and skipped tests + */ + public static class TestResults { + + private int successful; + private int failed; + private int skipped; + + @Restricted(DoNotUse.class) + public TestResults() { + } + + public TestResults(int successful, int failed, int skipped) { + this.successful = successful; + this.failed = failed; + this.skipped = skipped; + } + + public int getSuccessful() { + return successful; + } + + public void setSuccessful(int successful) { + this.successful = successful; + } + + public int getFailed() { + return failed; + } + + public void setFailed(int failed) { + this.failed = failed; + } + + public int getSkipped() { + return skipped; + } + + public void setSkipped(int skipped) { + this.skipped = skipped; + } + } + /** * The commit hash to set the status on */ @@ -107,6 +151,11 @@ public String toString() { */ private int buildNumber; + /** + * A summary of the test results. + */ + private TestResults testResults; + // Used for marshalling/unmarshalling @Restricted(DoNotUse.class) public BitbucketBuildStatus() {} @@ -142,6 +191,9 @@ public BitbucketBuildStatus(@NonNull BitbucketBuildStatus other) { this.refname = other.refname; this.buildDuration = other.buildDuration; this.parent = other.parent; + if(other.testResults != null) { + this.testResults = new TestResults(other.testResults.failed, other.testResults.successful, other.testResults.skipped); + } } public String getHash() { @@ -223,4 +275,12 @@ public void setParent(String parent) { public String getParent() { return parent; } + + public TestResults getTestResults() { + return testResults; + } + + public void setTestResults(TestResults testResults) { + this.testResults = testResults; + } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotifications.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotifications.java index 0021f491b..e1b2b7a41 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotifications.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotifications.java @@ -186,7 +186,9 @@ private static void createStatus(@NonNull Run build, buildStatus.setBuildDuration(build.getDuration()); buildStatus.setBuildNumber(build.getNumber()); buildStatus.setParent(notificationParentKey); - // TODO testResults should be provided by an extension point that integrates JUnit or anything else plugin + if(!isCloud) { + buildStatus.setTestResults(TestResultsAdapter.getTestResults(build)); + } notifier.notifyBuildStatus(buildStatus); if (result != null) { listener.getLogger().println("[Bitbucket] Build result notified"); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResultsAdapter.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResultsAdapter.java new file mode 100644 index 000000000..709c7cdf1 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResultsAdapter.java @@ -0,0 +1,84 @@ +/* + * The MIT License + * + * Copyright (c) 2025, ugrave. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.cloudbees.jenkins.plugins.bitbucket.impl.notifier; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus.TestResults; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.model.Run; +import hudson.tasks.test.AbstractTestResultAction; +import java.util.List; + +class TestResultsAdapter { + + private static final boolean JUNIT_PLUGIN_INSTALLED = isJUnitPluginInstalled(); + + static @Nullable TestResults getTestResults(@NonNull Run build) { + return JUNIT_PLUGIN_INSTALLED ? getTestResultsFromBuildAction(build) : null; + } + + private static @Nullable TestResults getTestResultsFromBuildAction(@NonNull Run build) { + List testResultActions = build.getActions(AbstractTestResultAction.class); + if (testResultActions.isEmpty()) { + return null; + } + return testResultActions.stream() + .collect(TestResultSummary::new, TestResultSummary::accept, TestResultSummary::combine) + .getTestResults(); + } + + private static boolean isJUnitPluginInstalled() { + try { + Class.forName("hudson.tasks.test.AbstractTestResultAction"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + private static class TestResultSummary { + + private int totalCount; + private int failCount; + private int skipCount; + + void accept(@NonNull AbstractTestResultAction value) { + totalCount += value.getTotalCount(); + failCount += value.getFailCount(); + skipCount += value.getSkipCount(); + } + + void combine(@NonNull TestResultSummary other) { + totalCount += other.totalCount; + failCount += other.failCount; + skipCount += other.skipCount; + } + + @NonNull + TestResults getTestResults() { + return new TestResults( totalCount - failCount - skipCount, failCount, skipCount); + } + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJUnit5Test.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJUnit5Test.java index 689b57d43..c0535448c 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJUnit5Test.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJUnit5Test.java @@ -34,6 +34,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus.Status; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus.TestResults; import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; @@ -46,6 +47,7 @@ import hudson.model.StreamBuildListener; import hudson.scm.SCM; import hudson.scm.SCMRevisionState; +import hudson.tasks.test.AbstractTestResultAction; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Collections; @@ -115,7 +117,7 @@ void test_status_notification_for_given_build_result(UnaryOperator testResultAction = mock(AbstractTestResultAction.class); + when(testResultAction.getTotalCount()).thenReturn(6); + when(testResultAction.getFailCount()).thenReturn(2); + when(testResultAction.getSkipCount()).thenReturn(1); + doReturn(List.of(testResultAction)).when(build).getActions(AbstractTestResultAction.class); + return build; } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResultsAdapterTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResultsAdapterTest.java new file mode 100644 index 000000000..d7d87f91c --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResultsAdapterTest.java @@ -0,0 +1,96 @@ +/* + * The MIT License + * + * Copyright (c) 2025, ugrave. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.cloudbees.jenkins.plugins.bitbucket.impl.notifier; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus.TestResults; +import hudson.model.Run; +import hudson.tasks.test.AbstractTestResultAction; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestResultsAdapterTest { + + @Test + void shouldReturnNullIfNoTestResultAvailable() { + Run build = Mockito.mock(Run.class); + + TestResults result = TestResultsAdapter.getTestResults(build); + + assertThat(result).isNull(); + Mockito.verify(build).getActions(AbstractTestResultAction.class); + } + + @Test + void shouldSummariesTestResult() { + Run build = Mockito.mock(Run.class); + Mockito.when(build.getActions(Mockito.any())).thenReturn( + List.of( + new MockTestResultAction(4, 2, 1), + new MockTestResultAction(2, 1, 1) + ) + ); + + TestResults result = TestResultsAdapter.getTestResults(build); + + assertThat(result) + .extracting(TestResults::getSuccessful, TestResults::getFailed, TestResults::getSkipped) + .containsExactly(1, 3, 2); + } + + private static class MockTestResultAction extends AbstractTestResultAction { + + private final int totalCount; + private final int failCount; + private final int skipCount; + + private MockTestResultAction(int totalCount, int failCount, int skipCount) { + this.totalCount = totalCount; + this.failCount = failCount; + this.skipCount = skipCount; + } + + @Override + public int getFailCount() { + return failCount; + } + + @Override + public int getTotalCount() { + return totalCount; + } + + @Override + public int getSkipCount() { + return skipCount; + } + + @Override + public Object getResult() { + return null; + } + } +}