Skip to content

Commit

Permalink
[JENKINS-14701] Thorough Matrix support.
Browse files Browse the repository at this point in the history
This allows you to only run the script once - on the parent job,
before the "configuration" builds are run. This is supported through a
new checkbox in the configuration UI.
  • Loading branch information
jorgenpt committed Aug 9, 2012
1 parent 5d58561 commit 59b67f5
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 5 deletions.
78 changes: 74 additions & 4 deletions src/main/java/com/lookout/jenkins/EnvironmentScript.java
Expand Up @@ -4,6 +4,11 @@
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.matrix.MatrixAggregatable;
import hudson.matrix.MatrixAggregator;
import hudson.matrix.MatrixRun;
import hudson.matrix.MatrixBuild;
import hudson.matrix.MatrixProject;
import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
Expand All @@ -21,18 +26,21 @@
import jenkins.model.Jenkins;

import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;

/**
* Runs a specific chunk of code before each build, parsing output for new environment variables.
*
* @author Jørgen P. Tjernø
*/
public class EnvironmentScript extends BuildWrapper {
public class EnvironmentScript extends BuildWrapper implements MatrixAggregatable {
private final String script;
private final boolean onlyRunOnParent;

@DataBoundConstructor
public EnvironmentScript(String script) {
public EnvironmentScript(String script, boolean onlyRunOnParent) {
this.script = script;
this.onlyRunOnParent = onlyRunOnParent;
}

/**
Expand All @@ -42,12 +50,43 @@ public String getScript() {
return script;
}

public boolean shouldOnlyRunOnParent ()
{
return onlyRunOnParent;
}

@SuppressWarnings("rawtypes")
@Override
public Environment setUp(AbstractBuild build,
final Launcher launcher,
final BuildListener listener) throws IOException, InterruptedException {
if ((build instanceof MatrixRun) && shouldOnlyRunOnParent()) {
// If this is a matrix run and we have the onlyRunOnParent option
// enabled, we just retrieve the persisted environment from the
// PersistedEnvironment Action.
MatrixBuild parent = ((MatrixRun)build).getParentBuild();
if (parent != null) {
PersistedEnvironment persisted = parent.getAction(PersistedEnvironment.class);
if (persisted != null) {
return persisted.getEnvironment();
} else {
listener.error("[environment-script] Unable to load persisted environment from matrix parent job, not injecting any variables");
return new Environment() {};
}
} else {
// If there's no parent, then the module build was triggered
// manually, so we generate a new environment.
return generateEnvironment (build, launcher, listener);
}
} else {
// Otherwise we generate a new one.
return generateEnvironment (build, launcher, listener);
}
}

private Environment generateEnvironment(AbstractBuild<?, ?> build,
final Launcher launcher,
final BuildListener listener) throws IOException, InterruptedException {
// First we create the script in a temporary directory.
FilePath ws = build.getWorkspace();
FilePath scriptFile;
Expand Down Expand Up @@ -75,7 +114,6 @@ public Environment setUp(AbstractBuild build,
return null;
}


// Then we parse the variables out of it. We could use java.util.Properties, but it doesn't order the properties, so expanding variables with previous variables (like a shell script expects) doesn't work.
String[] lines = commandOutput.toString().split("(\n|\r\n)");
final Map<String, String> envAdditions = new HashMap<String, String>(lines.length);
Expand Down Expand Up @@ -137,6 +175,35 @@ public String[] buildCommandLine(FilePath scriptFile) {
}
}

/**
* Create an aggregator that will calculate the environment once iff
* onlyRunOnParent is true.
*
* The aggregator we return is called on the parent job for matrix jobs. In
* it we generate the environment once and persist it in an Action (of type
* {@link PersistedEnvironment}) if the job has onlyRunOnParent enabled. The
* subjobs ("configuration runs") will retrieve this and apply it to their
* environment, without performing the calculation.
*/
public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) {
if (!shouldOnlyRunOnParent()) {
return null;
}

return new MatrixAggregator(build, launcher, listener) {
@Override
public boolean startBuild() throws InterruptedException, IOException {
Environment env = generateEnvironment(build, launcher, listener);
if (env == null) {
return false;
}

build.addAction(new PersistedEnvironment(env));
return true;
}
};
}

/**
* Descriptor for {@link EnvironmentScript}. Used as a singleton.
* The class is marked as public so that it can be accessed from views.
Expand All @@ -155,6 +222,9 @@ public String getDisplayName() {
public boolean isApplicable(AbstractProject<?, ?> project) {
return true;
}

public boolean isMatrix(StaplerRequest request) {
return (request.findAncestorObject(AbstractProject.class) instanceof MatrixProject);
}
}
}

32 changes: 32 additions & 0 deletions src/main/java/com/lookout/jenkins/PersistedEnvironment.java
@@ -0,0 +1,32 @@
package com.lookout.jenkins;

import hudson.EnvVars;
import hudson.model.Action;
import hudson.tasks.BuildWrapper.Environment;

public class PersistedEnvironment implements Action {
private Environment environment;

public PersistedEnvironment (Environment environment) {
this.environment = environment;
}

public Environment getEnvironment () {
return environment;
}

public String getDisplayName() {
return "Environment Script variables";
}

public String getIconFileName() {
// TODO Auto-generated method stub
return null;
}

public String getUrlName() {
// TODO Auto-generated method stub
return null;
}

}
@@ -1,4 +1,17 @@
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<j:choose>
<j:when test="${descriptor.isMatrix(request)}">
<f:entry title="${%Run only on parent}"
help="${resURL}/plugin/environment-script/help-runOnlyOnParent.html">
<f:checkbox name="onlyRunOnParent"
checked="${instance.shouldOnlyRunOnParent()}" />
</f:entry>
</j:when>
<j:otherwise>
<f:invisibleEntry name="runOnlyOnParent" value="false" />
</j:otherwise>
</j:choose>

<f:entry title="${%Script Content}"
description="This script is executed before each build, and any output it produces will be evaluated as environment variables (key=value)."
help="${resURL}/plugin/environment-script/help-script.html">
Expand Down
43 changes: 43 additions & 0 deletions src/test/java/com/lookout/jenkins/CountBuilder.java
@@ -0,0 +1,43 @@
package com.lookout.jenkins;

import java.io.IOException;

import net.sf.json.JSONObject;

import org.kohsuke.stapler.StaplerRequest;

import hudson.Extension;
import hudson.Launcher;
import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.model.Descriptor;
import hudson.tasks.Builder;

/**
* {@link Builder} that simply counts how many times it was executed.
*
* @author Jørgen P. Tjernø <jorgen.tjerno@mylookout.com>
*/
public class CountBuilder extends Builder {
int count = 0;

public int getCount() {
return count;
}

public synchronized boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
count++;
return true;
}

@Extension
public static final class DescriptorImpl extends Descriptor<Builder> {
public Builder newInstance(StaplerRequest req, JSONObject data) {
throw new UnsupportedOperationException();
}

public String getDisplayName() {
return "Count Number Of Builds";
}
}
}
95 changes: 95 additions & 0 deletions src/test/java/com/lookout/jenkins/EnvironmentScriptMatrixTest.java
@@ -0,0 +1,95 @@
package com.lookout.jenkins;

import java.io.File;

import hudson.FilePath;
import hudson.matrix.Axis;
import hudson.matrix.AxisList;
import hudson.matrix.MatrixRun;
import hudson.matrix.DefaultMatrixExecutionStrategyImpl;
import hudson.matrix.MatrixBuild;
import hudson.matrix.MatrixProject;

import org.jvnet.hudson.test.CaptureEnvironmentBuilder;
import org.jvnet.hudson.test.HudsonTestCase;

public class EnvironmentScriptMatrixTest extends HudsonTestCase {
class MatrixTestJob {
public MatrixProject project;
public CaptureEnvironmentBuilder captureBuilder;
public CountBuilder countBuilder;

public MatrixTestJob (String script, boolean onlyRunOnParent) throws Exception {
project = createMatrixProject();

// This forces it to run the builds sequentially, to prevent any
// race conditions when concurrently updating the 'counter' file.
project.setExecutionStrategy(new DefaultMatrixExecutionStrategyImpl(true, null, null, null));

project.setAxes(new AxisList(new Axis("axis", "value1", "value2")));
project.getBuildWrappersList().add(new EnvironmentScript(script, onlyRunOnParent));

captureBuilder = new CaptureEnvironmentBuilder();
project.getBuildersList().add(captureBuilder);

countBuilder = new CountBuilder();
project.getBuildersList().add(countBuilder);
}
}

final static String SCRIPT_COUNTER =
"file='%s/counter'\n"
+ "if [ -f $file ]; then\n"
+ " let i=$(cat $file)+1\n"
+ "else\n"
+ " i=1\n"
+ "fi\n"
+ "echo 1 >was_run\n"
+ "echo $i >$file\n"
+ "echo seen=yes";


// Explicit constructor so that we can call createTmpDir.
public EnvironmentScriptMatrixTest () throws Exception {}

// Generate a random directory that we pass to the shell script.
File tempDir = createTmpDir();
String script = String.format(SCRIPT_COUNTER, tempDir.getPath());

public void testWithParentOnly () throws Exception {
MatrixTestJob job = new MatrixTestJob(script, true);
MatrixBuild build = buildAndAssert(job);

// We ensure that this was only run once (on the parent)
assertEquals("1", new FilePath(tempDir).child("counter").readToString().trim());

// Then make sure that it was in fact in the parent's WS that we ran.
assertTrue(build.getWorkspace().child("was_run").exists());
for (MatrixRun run : build.getRuns())
assertFalse(run.getWorkspace().child("was_run").exists());
}

public void testWithEachChild () throws Exception {
MatrixTestJob job = new MatrixTestJob(script, false);
MatrixBuild build = buildAndAssert(job);

// We ensure that this was only run twice - once for each axis combination - but not on the parent.
assertEquals("2", new FilePath(tempDir).child("counter").readToString().trim());

// Then make sure that it was in fact in the combination jobs' workspace.
assertFalse(build.getWorkspace().child("was_run").exists());
for (MatrixRun run : build.getRuns())
assertTrue(run.getWorkspace().child("was_run").exists());
}

private MatrixBuild buildAndAssert(MatrixTestJob job) throws Exception {
MatrixBuild build = assertBuildStatusSuccess(job.project.scheduleBuild2(0).get());

// Make sure that the environment variables set in the script are properly propagated.
assertEquals("yes", job.captureBuilder.getEnvVars().get("seen"));
// Make sure that the builder was executed twice, once for each axis value.
assertEquals(2, job.countBuilder.getCount());

return build;
}
}
Expand Up @@ -17,7 +17,7 @@ public TestJob (String script) throws Exception {
project = createFreeStyleProject();
builder = new CaptureEnvironmentBuilder();
project.getBuildersList().add(builder);
project.getBuildWrappersList().add(new EnvironmentScript(script));
project.getBuildWrappersList().add(new EnvironmentScript(script, false));
}
}

Expand Down

0 comments on commit 59b67f5

Please sign in to comment.