Skip to content

Commit

Permalink
Merge pull request #31 from abayer/jenkins-27395
Browse files Browse the repository at this point in the history
[JENKINS-27395] Add per-stage test splitting option
  • Loading branch information
abayer committed Nov 27, 2017
2 parents fde66c2 + 6b2fd1d commit 89aad65
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 17 deletions.
39 changes: 28 additions & 11 deletions pom.xml
Expand Up @@ -3,7 +3,7 @@
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>2.15</version>
<version>2.36</version>
<relativePath />
</parent>
<artifactId>parallel-test-executor</artifactId>
Expand All @@ -18,7 +18,7 @@
</license>
</licenses>
<properties>
<jenkins.version>1.642.3</jenkins.version>
<jenkins.version>2.7.3</jenkins.version>
</properties>
<repositories>
<repository>
Expand All @@ -42,12 +42,18 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>parameterized-trigger</artifactId>
<version>2.18</version>
<version>2.33</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>matrix-project</artifactId>
<version>1.7.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>junit</artifactId>
<version>1.15</version>
<version>1.22</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
Expand All @@ -64,41 +70,52 @@
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-step-api</artifactId>
<version>2.3</version>
<version>2.12</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
<version>1.21</version>
<version>1.30</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-api</artifactId>
<version>2.23</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>2.10</version>
<version>2.37</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-job</artifactId>
<version>2.3</version>
<version>2.11.1</version>
<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.3</version>
<version>2.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-basic-steps</artifactId>
<version>2.1</version>
<version>2.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>2.10</version>
<version>2.37</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
Expand Down
@@ -1,5 +1,7 @@
package org.jenkinsci.plugins.parallel_test_executor;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.AbortException;
Expand All @@ -16,6 +18,13 @@
import hudson.tasks.test.AbstractTestResultAction;
import hudson.tasks.test.TabulatedResult;
import hudson.tasks.test.TestResult;
import org.jenkinsci.plugins.workflow.actions.LabelAction;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner;
import org.jenkinsci.plugins.workflow.graphanalysis.FlowScanningUtils;
import org.jenkinsci.plugins.workflow.graphanalysis.NodeStepTypePredicate;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
Expand All @@ -30,6 +39,8 @@
import org.apache.commons.io.Charsets;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
* @author Kohsuke Kawaguchi
Expand Down Expand Up @@ -122,7 +133,8 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
}
FilePath dir = workspace.child("test-splits");
dir.deleteRecursive();
List<InclusionExclusionPattern> splits = findTestSplits(parallelism, build, listener, includesPatternFile != null);
List<InclusionExclusionPattern> splits = findTestSplits(parallelism, build, listener, includesPatternFile != null,
null);
for (int i = 0; i < splits.size(); i++) {
InclusionExclusionPattern pattern = splits.get(i);
try (OutputStream os = dir.child("split." + i + "." + (pattern.isIncludes() ? "include" : "exclude") + ".txt").write();
Expand All @@ -143,12 +155,29 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
return true;
}

static List<InclusionExclusionPattern> findTestSplits(Parallelism parallelism, Run<?,?> build, TaskListener listener, boolean generateInclusions) {
static List<InclusionExclusionPattern> findTestSplits(Parallelism parallelism, Run<?,?> build, TaskListener listener,
boolean generateInclusions,
@CheckForNull final String stageName) {
TestResult tr = findPreviousTestResult(build, listener);
if (tr == null) {
listener.getLogger().println("No record available, so executing everything in one place");
return Collections.singletonList(new InclusionExclusionPattern(Collections.<String>emptyList(), false));
} else {
Run<?,?> prevRun = tr.getRun();
if (prevRun instanceof FlowExecutionOwner.Executable && stageName != null) {
FlowExecutionOwner owner = ((FlowExecutionOwner.Executable)prevRun).asFlowExecutionOwner();
if (owner != null) {
FlowExecution execution = owner.getOrNull();
if (execution != null) {
DepthFirstScanner scanner = new DepthFirstScanner();
FlowNode stageId = scanner.findFirstMatch(execution, new StageNamePredicate(stageName));
if (stageId != null) {
tr = ((hudson.tasks.junit.TestResult) tr).getResultForPipelineBlock(stageId.getId());
}

}
}
}

Map<String/*fully qualified class name*/, TestClass> data = new TreeMap<>();
collect(tr, data);
Expand Down Expand Up @@ -312,4 +341,19 @@ public String getDisplayName() {
return "Parallel test job execution";
}
}

private static class StageNamePredicate implements Predicate<FlowNode> {
private final String stageName;
public StageNamePredicate(@Nonnull String stageName) {
this.stageName = stageName;
}
@Override
public boolean apply(@Nullable FlowNode input) {
if (input != null) {
LabelAction labelAction = input.getPersistentAction(LabelAction.class);
return labelAction != null && stageName.equals(labelAction.getDisplayName());
}
return false;
}
}
}
Expand Up @@ -24,6 +24,8 @@ public final class SplitStep extends AbstractStepImpl {

private boolean generateInclusions;

private String stage;

@DataBoundConstructor public SplitStep(Parallelism parallelism) {
this.parallelism = parallelism;
}
Expand All @@ -39,6 +41,15 @@ public void setGenerateInclusions(boolean generateInclusions) {
this.generateInclusions = generateInclusions;
}

public String getStage() {
return stage;
}

@DataBoundSetter
public void setStage(String stage) {
this.stage = stage;
}

@Extension public static final class DescriptorImpl extends AbstractStepDescriptorImpl {

public DescriptorImpl() {
Expand All @@ -65,10 +76,12 @@ public static final class Execution extends AbstractSynchronousStepExecution<Lis

@Override protected List<?> run() throws Exception {
if (step.generateInclusions) {
return ParallelTestExecutor.findTestSplits(step.parallelism, build, listener, step.generateInclusions);
return ParallelTestExecutor.findTestSplits(step.parallelism, build, listener, step.generateInclusions,
step.stage);
} else {
List<List<String>> result = new ArrayList<>();
for (InclusionExclusionPattern pattern : ParallelTestExecutor.findTestSplits(step.parallelism, build, listener, step.generateInclusions)) {
for (InclusionExclusionPattern pattern : ParallelTestExecutor.findTestSplits(step.parallelism, build, listener,
step.generateInclusions, step.stage)) {
result.add(pattern.getList());
}
return result;
Expand Down
Expand Up @@ -5,15 +5,21 @@
import org.jenkinsci.plugins.workflow.cps.SnippetizerTester;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.recipes.LocalData;

import static org.junit.Assert.assertTrue;

public class ParallelTestExecutorTest {

@ClassRule
public static BuildWatcher buildWatcher = new BuildWatcher();

@Rule
public JenkinsRule jenkinsRule = new JenkinsRule();

Expand Down Expand Up @@ -52,4 +58,44 @@ public void workflowGenerateInclusions() throws Exception {
jenkinsRule.assertLogContains("splits[1]: includes=true list=[two.java, two.class]", b2);
}

@Issue("JENKINS-27395")
@Test
public void splitTestsWithinStage() throws Exception {
WorkflowJob p = jenkinsRule.jenkins.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition(
"def splits = splitTests parallelism: count(2), generateInclusions: true, stage: 'first'\n" +
"echo \"splits.size=${splits.size()}\"; for (int i = 0; i < splits.size(); i++) {\n" +
" def split = splits[i]; echo \"splits[${i}]: includes=${split.includes} list=${split.list}\"\n" +
"}\n" +
"def allSplits = splitTests parallelism: count(2), generateInclusions: true\n" +
"echo \"allSplits.size=${allSplits.size()}\"; for (int i = 0; i < allSplits.size(); i++) {\n" +
" def split = allSplits[i]; echo \"allSplits[${i}]: includes=${split.includes} list=${split.list}\"\n" +
"}\n" +
"stage('first') {\n" +
" node {\n" +
" writeFile file: 'TEST-1.xml', text: '<testsuite name=\"one\"><testcase name=\"x\"/></testsuite>'\n" +
" writeFile file: 'TEST-2.xml', text: '<testsuite name=\"two\"><testcase name=\"y\"/></testsuite>'\n" +
" junit 'TEST-*.xml'\n" +
" }\n" +
"}\n" +
"stage('second') {\n" +
" node {\n" +
" writeFile file: 'TEST-3.xml', text: '<testsuite name=\"three\"><testcase name=\"a\"/></testsuite>'\n" +
" writeFile file: 'TEST-4.xml', text: '<testsuite name=\"four\"><testcase name=\"b\"/></testsuite>'\n" +
" junit 'TEST-*.xml'\n" +
" }\n" +
"}\n", true));
WorkflowRun b1 = jenkinsRule.assertBuildStatusSuccess(p.scheduleBuild2(0));
jenkinsRule.assertLogContains("splits.size=1", b1);
jenkinsRule.assertLogContains("splits[0]: includes=false list=[]", b1);
jenkinsRule.assertLogContains("allSplits.size=1", b1);
jenkinsRule.assertLogContains("allSplits[0]: includes=false list=[]", b1);
WorkflowRun b2 = jenkinsRule.assertBuildStatusSuccess(p.scheduleBuild2(0));
jenkinsRule.assertLogContains("splits.size=2", b2);
jenkinsRule.assertLogContains("splits[0]: includes=false list=[two.java, two.class]", b2);
jenkinsRule.assertLogContains("splits[1]: includes=true list=[two.java, two.class]", b2);
jenkinsRule.assertLogContains("allSplits.size=2", b2);
jenkinsRule.assertLogContains("allSplits[0]: includes=false list=[one.java, one.class, two.java, two.class]", b2);
jenkinsRule.assertLogContains("allSplits[1]: includes=true list=[one.java, one.class, two.java, two.class]", b2);
}
}
Expand Up @@ -82,7 +82,7 @@ public void findTestSplits() throws Exception {
when(action.getResult()).thenReturn(testResult);

CountDrivenParallelism parallelism = new CountDrivenParallelism(5);
List<InclusionExclusionPattern> splits = ParallelTestExecutor.findTestSplits(parallelism, build, listener, false);
List<InclusionExclusionPattern> splits = ParallelTestExecutor.findTestSplits(parallelism, build, listener, false, null);
assertEquals(5, splits.size());
for (InclusionExclusionPattern split : splits) {
assertFalse(split.isIncludes());
Expand All @@ -96,7 +96,7 @@ public void findTestSplitsInclusions() throws Exception {
when(action.getResult()).thenReturn(testResult);

CountDrivenParallelism parallelism = new CountDrivenParallelism(5);
List<InclusionExclusionPattern> splits = ParallelTestExecutor.findTestSplits(parallelism, build, listener, true);
List<InclusionExclusionPattern> splits = ParallelTestExecutor.findTestSplits(parallelism, build, listener, true, null);
assertEquals(5, splits.size());
List<String> exclusions = new ArrayList<>(splits.get(0).getList());
List<String> inclusions = new ArrayList<>();
Expand Down

0 comments on commit 89aad65

Please sign in to comment.