Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-75082] Include test results in build status notification #1008

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>display-url-api</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>junit</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>branch-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,50 @@
}
}

/**
* 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() {
}

Check warning on line 69 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 68-69 are not covered by tests

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

Check warning on line 83 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 82-83 are not covered by tests

public int getFailed() {
return failed;
}

public void setFailed(int failed) {
this.failed = failed;
}

Check warning on line 91 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 90-91 are not covered by tests

public int getSkipped() {
return skipped;
}

public void setSkipped(int skipped) {
this.skipped = skipped;
}

Check warning on line 99 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 98-99 are not covered by tests
}

/**
* The commit hash to set the status on
*/
Expand Down Expand Up @@ -107,6 +151,11 @@
*/
private int buildNumber;

/**
* A summary of the test results.
*/
private TestResults testResults;

// Used for marshalling/unmarshalling
@Restricted(DoNotUse.class)
public BitbucketBuildStatus() {}
Expand Down Expand Up @@ -142,6 +191,9 @@
this.refname = other.refname;
this.buildDuration = other.buildDuration;
this.parent = other.parent;
if(other.testResults != null) {

Check warning on line 194 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 194 is only partially covered, one branch is missing
this.testResults = new TestResults(other.testResults.failed, other.testResults.successful, other.testResults.skipped);

Check warning on line 195 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 195 is not covered by tests
}
}

public String getHash() {
Expand Down Expand Up @@ -223,4 +275,12 @@
public String getParent() {
return parent;
}

public TestResults getTestResults() {
return testResults;
}

public void setTestResults(TestResults testResults) {
this.testResults = testResults;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

Check warning on line 33 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResultsAdapter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 33 is not covered by tests

private static final boolean JUNIT_PLUGIN_INSTALLED = isJUnitPluginInstalled();

static @Nullable TestResults getTestResults(@NonNull Run<?, ?> build) {
return JUNIT_PLUGIN_INSTALLED ? getTestResultsFromBuildAction(build) : null;

Check warning on line 38 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResultsAdapter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 38 is only partially covered, one branch is missing
}

private static @Nullable TestResults getTestResultsFromBuildAction(@NonNull Run<?, ?> build) {
List<AbstractTestResultAction> 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;

Check warning on line 56 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResultsAdapter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 55-56 are not covered by tests
}
}

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

Check warning on line 76 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResultsAdapter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 73-76 are not covered by tests

@NonNull
TestResults getTestResults() {
return new TestResults( totalCount - failCount - skipCount, failCount, skipCount);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -115,7 +117,7 @@ void test_status_notification_for_given_build_result(UnaryOperator<BitbucketBuil
assertThat(captor.getValue().getState()).isEqualTo(expectedStatus.name());
}

@Issue("JENKINS-72780")
@Issue({"JENKINS-72780", "JENKINS-75082"})
@Test
void test_status_notification_name_when_UseReadableNotificationIds_is_true(@NonNull JenkinsRule r) throws Exception {
StreamBuildListener taskListener = new StreamBuildListener(System.out, StandardCharsets.UTF_8);
Expand All @@ -141,9 +143,12 @@ void test_status_notification_name_when_UseReadableNotificationIds_is_true(@NonN
verify(apiClient).postBuildStatus(captor.capture());
assertThat(captor.getValue().getKey()).isEqualTo("P/BRANCH-JOB");
assertThat(captor.getValue().getParent()).isEqualTo("P");
assertThat(captor.getValue().getTestResults())
.extracting(TestResults::getSuccessful, TestResults::getFailed, TestResults::getSkipped)
.containsExactly(3, 2, 1);
}

@Issue("JENKINS-75203")
@Issue({"JENKINS-75203", "JENKINS-75082"})
@Test
void test_status_notification_parent_key_null_if_cloud_is_true(@NonNull JenkinsRule r) throws Exception {
StreamBuildListener taskListener = new StreamBuildListener(System.out, StandardCharsets.UTF_8);
Expand All @@ -169,6 +174,7 @@ void test_status_notification_parent_key_null_if_cloud_is_true(@NonNull JenkinsR
verify(apiClient).postBuildStatus(captor.capture());
assertThat(captor.getValue().getKey()).isNotEmpty();
assertThat(captor.getValue().getParent()).isNull();
assertThat(captor.getValue().getTestResults()).isNull();
}

@Issue("JENKINS-74970")
Expand Down Expand Up @@ -259,6 +265,12 @@ private WorkflowRun prepareBuildForNotification(@NonNull JenkinsRule r, @NonNull
Branch branch = new Branch(scmSource.getId(), scmRevision.getHead(), scm, Collections.emptyList());
when(projectFactory.getBranch(job)).thenReturn(branch);
project.setProjectFactory(projectFactory);
AbstractTestResultAction<?> 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<MockTestResultAction> {

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