Skip to content
Permalink
Browse files
[JENKINS-27395] Add a junitResults step
This uses the run's externalizable ID and the step's FlowNode.getId()
together as a unique key to track which SuiteResults are associated
with a particular execution, and allows getting a TestResult object
from a run ID and 1 or more node IDs passed to an existing TestResult
object that contains suites for that run/those nodes.

Note that JUnitResultArchiverTest.zip has been moved around and
renamed due to
jenkinsci/jenkins-test-harness@79d9603
breaking tests otherwise, since the JUnitResultArchiveTest/ directory
will end up getting picked up as JENKINS_HOME otherwise.
  • Loading branch information
abayer committed Jul 26, 2017
1 parent de3022c commit 375f91d78da541fec0f095d996619d5e7cdfc18f
67 pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>2.12</version>
<version>2.31</version>
<relativePath />
</parent>
<artifactId>junit</artifactId>
@@ -13,8 +13,8 @@
<description>Allows JUnit-format test results to be published.</description>
<url>http://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin</url>
<properties>
<jenkins.version>1.580.1</jenkins.version>
<java.level>6</java.level>
<jenkins.version>2.7.3</jenkins.version>
<java.level>7</java.level>
</properties>
<licenses>
<license>
@@ -44,7 +44,23 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>structs</artifactId>
<version>1.2</version>
<version>1.7</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-step-api</artifactId>
<version>2.12</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-api</artifactId>
<version>2.19</version>
<exclusions>
<exclusion>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>scm-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
@@ -64,5 +80,48 @@
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>2.37</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-job</artifactId>
<version>2.11.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-basic-steps</artifactId>
<version>2.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-support</artifactId>
<version>2.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-support</artifactId>
<version>2.14</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>pipeline-stage-step</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-durable-task-step</artifactId>
<version>2.13</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -88,20 +88,27 @@ public String getTestResultLocationMessage() {
return (TestResult) super.parse(testResultLocations, build, launcher, listener);
}

@Deprecated
@Override
public TestResult parseResult(String testResultLocations, Run<?,?> build, FilePath workspace,
Launcher launcher, TaskListener listener)
throws InterruptedException, IOException {
return parseResult(testResultLocations, build, null, workspace, launcher, listener);
}

@Override
public TestResult parseResult(String testResultLocations,
Run<?,?> build, FilePath workspace, Launcher launcher,
TaskListener listener)
throws InterruptedException, IOException
{
public TestResult parseResult(String testResultLocations, Run<?,?> build, String nodeId, FilePath workspace,
Launcher launcher, TaskListener listener)
throws InterruptedException, IOException {
final long buildTime = build.getTimestamp().getTimeInMillis();
final long timeOnMaster = System.currentTimeMillis();

// [BUG 3123310] TODO - Test Result Refactor: review and fix TestDataPublisher/TestAction subsystem]
// also get code that deals with testDataPublishers from JUnitResultArchiver.perform

return workspace.act(new ParseResultCallable(testResultLocations, buildTime,
timeOnMaster, keepLongStdio, allowEmptyResults));
timeOnMaster, keepLongStdio, allowEmptyResults,
build.getExternalizableId(), nodeId));
}

private static final class ParseResultCallable extends MasterToSlaveFileCallable<TestResult> {
@@ -110,14 +117,18 @@ public TestResult parseResult(String testResultLocations,
private final long nowMaster;
private final boolean keepLongStdio;
private final boolean allowEmptyResults;
private final String runId;
private final String nodeId;

private ParseResultCallable(String testResults, long buildTime, long nowMaster,
boolean keepLongStdio, boolean allowEmptyResults) {
boolean keepLongStdio, boolean allowEmptyResults, String runId, String nodeId) {
this.buildTime = buildTime;
this.testResults = testResults;
this.nowMaster = nowMaster;
this.keepLongStdio = keepLongStdio;
this.allowEmptyResults = allowEmptyResults;
this.runId = runId;
this.nodeId = nodeId;
}

public TestResult invoke(File ws, VirtualChannel channel) throws IOException {
@@ -129,7 +140,7 @@ public TestResult invoke(File ws, VirtualChannel channel) throws IOException {

String[] files = ds.getIncludedFiles();
if (files.length > 0) {
result = new TestResult(buildTime + (nowSlave - nowMaster), ds, keepLongStdio);
result = new TestResult(buildTime + (nowSlave - nowMaster), ds, keepLongStdio, runId, nodeId);
result.tally();
} else {
if (this.allowEmptyResults) {
@@ -53,6 +53,7 @@
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.tasks.SimpleBuildStep;
import org.kohsuke.stapler.DataBoundSetter;
@@ -62,7 +63,7 @@
*
* @author Kohsuke Kawaguchi
*/
public class JUnitResultArchiver extends Recorder implements SimpleBuildStep {
public class JUnitResultArchiver extends Recorder implements SimpleBuildStep, JUnitTask {

/**
* {@link FileSet} "includes" string, like "foo/bar/*.xml"
@@ -124,8 +125,15 @@ public JUnitResultArchiver(
private TestResult parse(String expandedTestResults, Run<?,?> run, @Nonnull FilePath workspace, Launcher launcher, TaskListener listener)
throws IOException, InterruptedException
{
return new JUnitParser(this.isKeepLongStdio(),
this.isAllowEmptyResults()).parseResult(expandedTestResults, run, workspace, launcher, listener);
return parse(this, null, expandedTestResults, run, workspace, launcher, listener);

}

private static TestResult parse(@Nonnull JUnitTask task, String nodeId, String expandedTestResults, Run<?,?> run,
@Nonnull FilePath workspace, Launcher launcher, TaskListener listener)
throws IOException, InterruptedException {
return new JUnitParser(task.isKeepLongStdio(), task.isAllowEmptyResults())
.parseResult(expandedTestResults, run, nodeId, workspace, launcher, listener);
}

@Deprecated
@@ -142,11 +150,19 @@ protected TestResult parse(String expandedTestResults, AbstractBuild build, Laun
@Override
public void perform(Run build, FilePath workspace, Launcher launcher,
TaskListener listener) throws InterruptedException, IOException {
TestResultAction action = parseAndAttach(this, null, build, workspace, launcher, listener);

if (action != null && action.getResult().getFailCount() > 0)
build.setResult(Result.UNSTABLE);
}

public static TestResultAction parseAndAttach(@Nonnull JUnitTask task, String nodeId, Run build, FilePath workspace,
Launcher launcher, TaskListener listener) throws InterruptedException, IOException {
listener.getLogger().println(Messages.JUnitResultArchiver_Recording());

final String testResults = build.getEnvironment(listener).expand(this.testResults);
final String testResults = build.getEnvironment(listener).expand(task.getTestResults());

TestResult result = parse(testResults, build, workspace, launcher, listener);
TestResult result = parse(task, nodeId, testResults, build, workspace, launcher, listener);

synchronized (build) {
// TODO can the build argument be omitted now, or is it used prior to the call to addAction?
@@ -160,26 +176,26 @@ public void perform(Run build, FilePath workspace, Launcher launcher,
result.freeze(action);
action.mergeResult(result, listener);
}
action.setHealthScaleFactor(getHealthScaleFactor()); // overwrites previous value if appending
action.setHealthScaleFactor(task.getHealthScaleFactor()); // overwrites previous value if appending
if (result.isEmpty()) {
if (build.getResult() == Result.FAILURE) {
// most likely a build failed before it gets to the test phase.
// don't report confusing error message.
return;
return null;
}
if (this.allowEmptyResults) {
if (task.isAllowEmptyResults()) {
// User allow empty results
listener.getLogger().println(Messages.JUnitResultArchiver_ResultIsEmpty());
return;
return null;
}
// most likely a configuration error in the job - e.g. false pattern to match the JUnit result files
throw new AbortException(Messages.JUnitResultArchiver_ResultIsEmpty());
}

// TODO: Move into JUnitParser [BUG 3123310]
List<Data> data = action.getData();
if (testDataPublishers != null) {
for (TestDataPublisher tdp : testDataPublishers) {
if (task.getTestDataPublishers() != null) {
for (TestDataPublisher tdp : task.getTestDataPublishers()) {
Data d = tdp.contributeTestData(build, workspace, launcher, listener, result);
if (d != null) {
data.add(d);
@@ -193,8 +209,7 @@ public void perform(Run build, FilePath workspace, Launcher launcher,
build.addAction(action);
}

if (action.getResult().getFailCount() > 0)
build.setResult(Result.UNSTABLE);
return action;
}
}

@@ -0,0 +1,15 @@
package hudson.tasks.junit;

import java.util.List;

public interface JUnitTask {
String getTestResults();

double getHealthScaleFactor();

List<TestDataPublisher> getTestDataPublishers();

boolean isKeepLongStdio();

boolean isAllowEmptyResults();
}

0 comments on commit 375f91d

Please sign in to comment.