Skip to content

Commit

Permalink
[JENKINS-29894] Allow to specify includesPatternFile
Browse files Browse the repository at this point in the history
If provided, only the first bucket will use the excludesPatternFile, the other buckets will use includesPatternFile
  • Loading branch information
Vlatombe committed Aug 11, 2015
1 parent f11a8e4 commit 7908178
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 22 deletions.
@@ -0,0 +1,119 @@
package org.jenkinsci.plugins.parallel_test_executor;

import com.google.common.collect.Lists;
import hudson.Extension;
import hudson.FilePath;
import hudson.model.AbstractBuild;
import hudson.model.Action;
import hudson.model.FileParameterValue;
import hudson.model.ParametersAction;
import hudson.model.TaskListener;
import hudson.plugins.parameterizedtrigger.AbstractBuildParameterFactory;
import hudson.plugins.parameterizedtrigger.AbstractBuildParameterFactoryDescriptor;
import hudson.plugins.parameterizedtrigger.AbstractBuildParameters;
import hudson.plugins.parameterizedtrigger.FileBuildParameterFactory;
import hudson.util.IOException2;
import org.kohsuke.stapler.DataBoundConstructor;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.logging.Logger;

/**
* Essentially a copy-paste of {@link hudson.plugins.parameterizedtrigger.BinaryFileParameterFactory} that takes a
* list of mappings "name -> filePattern" to generate parameters.
*
* @author Vincent Latombe <vincent@latombe.net>
*/
public class MultipleBinaryFileParameterFactory extends AbstractBuildParameterFactory {
public static class Tuple {
public Tuple(String parameterName, String filePattern) {
this.parameterName = parameterName;
this.filePattern = filePattern;
}
public final String parameterName;
public final String filePattern;
}

private final List<Tuple> parametersList;
private final FileBuildParameterFactory.NoFilesFoundEnum noFilesFoundAction;

@DataBoundConstructor
public MultipleBinaryFileParameterFactory(List<Tuple> parametersList, FileBuildParameterFactory.NoFilesFoundEnum noFilesFoundAction) {
this.parametersList = parametersList;
this.noFilesFoundAction = noFilesFoundAction;
}

public MultipleBinaryFileParameterFactory(List<Tuple> parametersList) {
this(parametersList, FileBuildParameterFactory.NoFilesFoundEnum.SKIP);
}

public FileBuildParameterFactory.NoFilesFoundEnum getNoFilesFoundAction() {
return noFilesFoundAction;
}

@Override
public List<AbstractBuildParameters> getParameters(AbstractBuild<?, ?> build, TaskListener listener) throws IOException, InterruptedException, AbstractBuildParameters.DontTriggerException {
List<AbstractBuildParameters> result = Lists.newArrayList();
int totalFiles = 0;
for (final Tuple t : parametersList) {
// save them into the master because FileParameterValue might need files after the slave workspace have disappeared/reused
FilePath target = new FilePath(build.getRootDir()).child("parameter-files");
int k = build.getWorkspace().copyRecursiveTo(t.filePattern, target);
totalFiles += k;
if (k > 0) {
for (final FilePath f : target.list(t.filePattern)) {
LOGGER.fine("Triggering build with " + f.getName());

result.add(new AbstractBuildParameters() {
@Override
public Action getAction(AbstractBuild<?, ?> build, TaskListener listener) throws IOException, InterruptedException, DontTriggerException {
assert f.getChannel() == null; // we copied files locally. This file must be local to the master
FileParameterValue fv = new FileParameterValue(t.parameterName, new File(f.getRemote()), f.getName());
if ($setLocation != null) {
try {
$setLocation.invoke(fv, t.parameterName);
} catch (IllegalAccessException e) {
// be defensive as the core might change
} catch (InvocationTargetException e) {
// be defensive as the core might change
}
}
return new ParametersAction(fv);
}
});
}
}
}
if (totalFiles ==0) {
noFilesFoundAction.failCheck(listener);
}

return result;
}


@Extension
public static class DescriptorImpl extends AbstractBuildParameterFactoryDescriptor {
@Override
public String getDisplayName() {
return "Multiple Binary Files (not meant to be used)";
}
}

private static Method $setLocation;

static {
// work around NPE fixed in the core at 4a95cc6f9269108e607077dc9fd57f06e4c9af26
try {
$setLocation = FileParameterValue.class.getDeclaredMethod("setLocation",String.class);
$setLocation.setAccessible(true);
} catch (NoSuchMethodException e) {
// ignore
}
}
private static final Logger LOGGER = Logger.getLogger(MultipleBinaryFileParameterFactory.class.getName());
}
Expand Up @@ -4,6 +4,7 @@
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.*;
import hudson.plugins.parameterizedtrigger.*;
import hudson.tasks.BuildStepDescriptor;
Expand All @@ -17,7 +18,6 @@
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
Expand All @@ -36,18 +36,20 @@ public class ParallelTestExecutor extends Builder {

private String testJob;
private String patternFile;
private String includesPatternFile;
private String testReportFiles;
private boolean doNotArchiveTestResults = false;
private List<AbstractBuildParameters> parameters;

@DataBoundConstructor
public ParallelTestExecutor(Parallelism parallelism, String testJob, String patternFile, String testReportFiles, boolean archiveTestResults, List<AbstractBuildParameters> parameters) {
public ParallelTestExecutor(Parallelism parallelism, String testJob, String patternFile, String testReportFiles, boolean archiveTestResults, List<AbstractBuildParameters> parameters, String includesPatternFile) {
this.parallelism = parallelism;
this.testJob = testJob;
this.patternFile = patternFile;
this.testReportFiles = testReportFiles;
this.parameters = parameters;
this.doNotArchiveTestResults = !archiveTestResults;
this.includesPatternFile = Util.fixEmpty(includesPatternFile);
}

public Parallelism getParallelism() {
Expand All @@ -62,6 +64,10 @@ public String getPatternFile() {
return patternFile;
}

public String getIncludesPatternFile() {
return includesPatternFile;
}

public String getTestReportFiles() {
return testReportFiles;
}
Expand Down Expand Up @@ -101,13 +107,14 @@ public int compareTo(Knapsack that) {
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
FilePath dir = build.getWorkspace().child("test-splits");
dir.deleteRecursive();
List<List<String>> splits = findTestSplits(parallelism, build, listener);
List<InclusionExclusionPattern> splits = findTestSplits(parallelism, build, listener, includesPatternFile != null);
for (int i = 0; i < splits.size(); i++) {
OutputStream os = dir.child("split." + i + ".txt").write();
InclusionExclusionPattern pattern = splits.get(i);
OutputStream os = dir.child("split." + i + "." + (pattern.includes ? "include" : "exclude") + ".txt").write();
try {
PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, Charsets.UTF_8));
for (String exclusion : splits.get(i)) {
pw.println(exclusion);
for (String filePattern : pattern.list) {
pw.println(filePattern);
}
pw.close();
} finally {
Expand All @@ -124,11 +131,11 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
return true;
}

static List<List<String>> findTestSplits(Parallelism parallelism, Run<?,?> build, TaskListener listener) {
static List<InclusionExclusionPattern> findTestSplits(Parallelism parallelism, Run<?,?> build, TaskListener listener, boolean generateInclusions) {
TestResult tr = findPreviousTestResult(build, listener);
if (tr == null) {
listener.getLogger().println("No record available, so executing everything in one place");
return Collections.singletonList(Collections.<String>emptyList());
return Collections.singletonList(new InclusionExclusionPattern(Collections.<String>emptyList(), false));
} else {

Map<String/*fully qualified class name*/, TestClass> data = new HashMap<String, TestClass>();
Expand Down Expand Up @@ -173,21 +180,33 @@ static List<List<String>> findTestSplits(Parallelism parallelism, Run<?,?> build
listener.getLogger().printf("%d test classes (%dms) divided into %d sets. Min=%dms, Average=%dms, Max=%dms, stddev=%dms\n",
data.size(), total, n, min, average, max, stddev);

List<List<String>> r = new ArrayList<List<String>>();
List<InclusionExclusionPattern> r = new ArrayList<InclusionExclusionPattern>();
for (int i = 0; i < n; i++) {
Knapsack k = knapsacks.get(i);
List<String> exclusions = new ArrayList<String>();
r.add(exclusions);
boolean shouldIncludeElements = generateInclusions && i != 0;
List<String> elements = new ArrayList<String>();
r.add(new InclusionExclusionPattern(elements, shouldIncludeElements));
for (TestClass d : sorted) {
if (d.knapsack == k) continue;
exclusions.add(d.getSourceFileName(".java"));
exclusions.add(d.getSourceFileName(".class"));
if (shouldIncludeElements == (d.knapsack == k)) {
elements.add(d.getSourceFileName(".java"));
elements.add(d.getSourceFileName(".class"));
}
}
}
return r;
}
}

static class InclusionExclusionPattern {
boolean includes;
List<String> list;

InclusionExclusionPattern(List<String> list, boolean includes) {
this.list = list;
this.includes = includes;
}
}

/**
* Collects all the test reports
*/
Expand Down Expand Up @@ -218,11 +237,16 @@ public Action getAction(AbstractBuild<?, ?> build, TaskListener listener) throws
}

// actual logic of child process triggering is left up to the parameterized build
List<MultipleBinaryFileParameterFactory.Tuple> tuples = new ArrayList<MultipleBinaryFileParameterFactory.Tuple>();
tuples.add(new MultipleBinaryFileParameterFactory.Tuple(getPatternFile(), "test-splits/split.*.exclude.txt"));
if (includesPatternFile != null) {
tuples.add(new MultipleBinaryFileParameterFactory.Tuple(getIncludesPatternFile(), "test-splits/split.*.include.txt"));
}
MultipleBinaryFileParameterFactory factory = new MultipleBinaryFileParameterFactory(tuples);
BlockableBuildTriggerConfig config = new BlockableBuildTriggerConfig(
testJob,
blocking,
Collections.<AbstractBuildParameterFactory>singletonList(
new BinaryFileParameterFactory(getPatternFile(), "test-splits/split.*.txt")),
Collections.<AbstractBuildParameterFactory>singletonList(factory),
parameterList
);

Expand Down
Expand Up @@ -11,6 +11,9 @@ f.entry(title:"Test job to run", field:"testJob") {
f.entry(title:"Exclusion file name in the test job", field:"patternFile") {
f.textbox()
}
f.entry(title:"Optional inclusion file name in the test job", field:"includesPatternFile") {
f.textbox()
}
f.entry(title:"Degree of parallelism", field:"parallelism") {
f.hetero_radio(field:"parallelism", descriptors:Jenkins.instance.getDescriptorList(Parallelism.class))
}
Expand Down
@@ -0,0 +1,7 @@
<div>
A text file that lists one Java source file name per line gets created by this path inside
the workspace of the test job.
The path is relative to the workspace of the test job.
Your test job must honor this inclusion list while executing tests.
See the help of this builder for more details.
</div>
Expand Up @@ -10,7 +10,14 @@
and the test job is triggered for each unit, with the exclusion file placed inside the workspace at your specified location.

<p>
You are responsible for configuring the build script in the test job to honor the exclusion file.
Optionally, if your test job supports it, you may provide a test-inclusion file name. If defined, the plugin will
generate inclusion lists for all parallel units but one which will still use exclusion list. This avoids new test
cases from being included in all units on their first run. If you don't use an inclusion file, when a new test is
added, the first build afterward will be executed in all the sub-builds,
because it's not in the exclusion list on any of the units.

<p>
You are responsible for configuring the build script in the test job to honor the exclusion and inclusion file.
A standard technique is to write the build script to always refer to a fixed exclusion list file,
and check in an empty file by that name to your SCM. You can then specify that file as the "exclusion file name"
in the configuration of this builder, and the builder will overwrite the empty file from SCM
Expand All @@ -23,9 +30,4 @@
<p>
At the end of the executions of the test job, the specified report directories are brought back into
this job's workspace, then the standard JUnit test report collector will tally them.

<p>
We use an exclusion list as opposed to an inclusion list so that newly added tests can be accounted for.
A related gotcha is that if a new test is added, the first build afterward will be executed in all the sub-builds,
because it's not in the exclusion list on any of the units.
</div>

0 comments on commit 7908178

Please sign in to comment.