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;
+ }
+ }
+}