diff --git a/core/src/main/java/hudson/matrix/MatrixBuild.java b/core/src/main/java/hudson/matrix/MatrixBuild.java index d5c3259f9736b37ed4fa7bf0f4a5d2ec9c98a086..0ae4e5484e3314f38a4e0ee8a3798f7065f62dd3 100644 --- a/core/src/main/java/hudson/matrix/MatrixBuild.java +++ b/core/src/main/java/hudson/matrix/MatrixBuild.java @@ -23,6 +23,7 @@ */ package hudson.matrix; +import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; @@ -42,6 +43,7 @@ import java.io.PrintStream; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; +import java.util.HashSet; import java.util.List; import org.kohsuke.stapler.StaplerRequest; @@ -115,8 +117,10 @@ public class MatrixBuild extends AbstractBuild { */ public List getRuns() { List r = new ArrayList(); - for(MatrixConfiguration c : getParent().getItems()) - r.add(c.getBuildByNumber(getNumber())); + for(MatrixConfiguration c : getParent().getItems()) { + MatrixRun b = c.getBuildByNumber(getNumber()); + if (b != null) r.add(b); + } return r; } @@ -174,6 +178,17 @@ public class MatrixBuild extends AbstractBuild { axes = p.getAxes(); Collection activeConfigurations = p.getActiveConfigurations(); final int n = getNumber(); + + String touchStoneFilter = p.getTouchStoneCombinationFilter(); + Collection touchStoneConfigurations = new HashSet(); + Collection delayedConfigurations = new HashSet(); + for (MatrixConfiguration c: activeConfigurations) { + if (touchStoneFilter != null && c.getCombination().evalGroovyExpression(p.getAxes(), p.getTouchStoneCombinationFilter())) { + touchStoneConfigurations.add(c); + } else { + delayedConfigurations.add(c); + } + } for (MatrixAggregator a : aggregators) if(!a.startBuild()) @@ -181,73 +196,41 @@ public class MatrixBuild extends AbstractBuild { try { if(!p.isRunSequentially()) - for(MatrixConfiguration c : activeConfigurations) + for(MatrixConfiguration c : touchStoneConfigurations) scheduleConfigurationBuild(logger, c); - // this occupies an executor unnecessarily. - // it would be nice if this can be placed in a temproary executor. - Result r = Result.SUCCESS; - for (MatrixConfiguration c : activeConfigurations) { + for (MatrixConfiguration c : touchStoneConfigurations) { if(p.isRunSequentially()) scheduleConfigurationBuild(logger, c); - String whyInQueue = ""; - long startTime = System.currentTimeMillis(); - - // wait for the completion - int appearsCancelledCount = 0; - while(true) { - MatrixRun b = c.getBuildByNumber(n); - - // two ways to get beyond this. one is that the build starts and gets done, - // or the build gets cancelled before it even started. - Result buildResult = null; - if(b!=null && !b.isBuilding()) - buildResult = b.getResult(); - Queue.Item qi = c.getQueueItem(); - if(b==null && qi==null) - appearsCancelledCount++; - else - appearsCancelledCount = 0; - - if(appearsCancelledCount>=5) { - // there's conceivably a race condition in computating b and qi, as their computation - // are not synchronized. There are indeed several reports of Hudson incorrectly assuming - // builds being cancelled. See - // http://www.nabble.com/Master-slave-problem-tt14710987.html and also - // http://www.nabble.com/Anyone-using-AccuRev-plugin--tt21634577.html#a21671389 - // because of this, we really make sure that the build is cancelled by doing this 5 - // times over 5 seconds - logger.println(Messages.MatrixBuild_AppearsCancelled(c.getDisplayName())); - buildResult = Result.ABORTED; - } + Result buildResult = waitForCompletion(listener, c); + r = r.combine(buildResult); + } + + if (p.getTouchStoneResultCondition() != null && r.isWorseThan(p.getTouchStoneResultCondition())) { + logger.printf("Touchstone configurations resulted in %s, so aborting...\n", r); + return r; + } + + if(!p.isRunSequentially()) + for(MatrixConfiguration c : delayedConfigurations) + scheduleConfigurationBuild(logger, c); - if(buildResult!=null) { - r = r.combine(buildResult); - if(b!=null) - for (MatrixAggregator a : aggregators) - if(!a.endRun(b)) - return Result.FAILURE; - break; - } else { - if(qi!=null) { - // if the build seems to be stuck in the queue, display why - String why = qi.getWhy(); - if(!why.equals(whyInQueue) && System.currentTimeMillis()-startTime>5000) { - logger.println(c.getDisplayName()+" is still in the queue: "+why); - whyInQueue = why; - } - } - } - Thread.sleep(1000); - } + for (MatrixConfiguration c : delayedConfigurations) { + if(p.isRunSequentially()) + scheduleConfigurationBuild(logger, c); + Result buildResult = waitForCompletion(listener, c); + r = r.combine(buildResult); } return r; } catch( InterruptedException e ) { logger.println("Aborted"); return Result.ABORTED; - } finally { + } catch (AggregatorFailureException e) { + return Result.FAILURE; + } + finally { // if the build was aborted in the middle. Cancel all the configuration builds. Queue q = Hudson.getInstance().getQueue(); synchronized(q) {// avoid micro-locking in q.cancel. @@ -266,6 +249,58 @@ public class MatrixBuild extends AbstractBuild { } } } + + private Result waitForCompletion(BuildListener listener, MatrixConfiguration c) throws InterruptedException, IOException, AggregatorFailureException { + String whyInQueue = ""; + long startTime = System.currentTimeMillis(); + + // wait for the completion + int appearsCancelledCount = 0; + while(true) { + MatrixRun b = c.getBuildByNumber(getNumber()); + + // two ways to get beyond this. one is that the build starts and gets done, + // or the build gets cancelled before it even started. + Result buildResult = null; + if(b!=null && !b.isBuilding()) + buildResult = b.getResult(); + Queue.Item qi = c.getQueueItem(); + if(b==null && qi==null) + appearsCancelledCount++; + else + appearsCancelledCount = 0; + + if(appearsCancelledCount>=5) { + // there's conceivably a race condition in computating b and qi, as their computation + // are not synchronized. There are indeed several reports of Hudson incorrectly assuming + // builds being cancelled. See + // http://www.nabble.com/Master-slave-problem-tt14710987.html and also + // http://www.nabble.com/Anyone-using-AccuRev-plugin--tt21634577.html#a21671389 + // because of this, we really make sure that the build is cancelled by doing this 5 + // times over 5 seconds + listener.getLogger().println(Messages.MatrixBuild_AppearsCancelled(c.getDisplayName())); + buildResult = Result.ABORTED; + } + + if(buildResult!=null) { + for (MatrixAggregator a : aggregators) + if(!a.endRun(b)) + throw new AggregatorFailureException(); + return buildResult; + } + + if(qi!=null) { + // if the build seems to be stuck in the queue, display why + String why = qi.getWhy(); + if(!why.equals(whyInQueue) && System.currentTimeMillis()-startTime>5000) { + listener.getLogger().println(c.getDisplayName()+" is still in the queue: "+why); + whyInQueue = why; + } + } + + Thread.sleep(1000); + } + } private void scheduleConfigurationBuild(PrintStream logger, MatrixConfiguration c) { logger.println(Messages.MatrixBuild_Triggering(c.getDisplayName())); @@ -277,4 +312,10 @@ public class MatrixBuild extends AbstractBuild { a.endBuild(); } } + + /** + * A private exception to help maintain the correct control flow after extracting the 'waitForCompletion' method + */ + private static class AggregatorFailureException extends Exception {} + } diff --git a/core/src/main/java/hudson/matrix/MatrixProject.java b/core/src/main/java/hudson/matrix/MatrixProject.java index 2d56f3146252fd3fae695e8f5ee857ce7877fdb0..4e1c00019fba6466e4c20b0f25a9adff4a4ee214 100644 --- a/core/src/main/java/hudson/matrix/MatrixProject.java +++ b/core/src/main/java/hudson/matrix/MatrixProject.java @@ -40,6 +40,7 @@ import hudson.model.JDK; import hudson.model.Job; import hudson.model.Label; import hudson.model.Node; +import hudson.model.Result; import hudson.model.SCMedItem; import hudson.model.Saveable; import hudson.model.TopLevelItem; @@ -134,6 +135,9 @@ public class MatrixProject extends AbstractProject im private transient /*final*/ Set activeConfigurations = new LinkedHashSet(); private boolean runSequentially; + + private String touchStoneCombinationFilter; + private Result touchStoneResultCondition; public MatrixProject(String name) { super(Hudson.getInstance(), name); @@ -195,7 +199,24 @@ public class MatrixProject extends AbstractProject im return combinationFilter; } - protected void updateTransientActions() { + public String getTouchStoneCombinationFilter() { + return touchStoneCombinationFilter; + } + + public void setTouchStoneCombinationFilter( + String touchStoneCombinationFilter) { + this.touchStoneCombinationFilter = touchStoneCombinationFilter; + } + + public Result getTouchStoneResultCondition() { + return touchStoneResultCondition; + } + + public void setTouchStoneResultCondition(Result touchStoneResultCondition) { + this.touchStoneResultCondition = touchStoneResultCondition; + } + + protected void updateTransientActions() { synchronized(transientActions) { super.updateTransientActions(); @@ -533,10 +554,19 @@ public class MatrixProject extends AbstractProject im } } - if(req.getParameter("hasCombinationFilter")!=null) + if(req.getParameter("hasCombinationFilter")!=null) { this.combinationFilter = Util.nullify(req.getParameter("combinationFilter")); - else + } else { this.combinationFilter = null; + } + + if (req.getParameter("hasTouchStoneCombinationFilter")!=null) { + this.touchStoneCombinationFilter = Util.nullify(req.getParameter("touchStoneCombinationFilter")); + String touchStoneResultCondition = req.getParameter("touchStoneResultCondition"); + this.touchStoneResultCondition = Result.fromString(touchStoneResultCondition); + } else { + this.touchStoneCombinationFilter = null; + } // parse system axes newAxes.add(Axis.parsePrefixed(req,"jdk")); diff --git a/core/src/main/java/hudson/model/Result.java b/core/src/main/java/hudson/model/Result.java index bf6457e113880161299af03dae2d86a82677c7b5..9c715d7289a7348bb46ef41bfafeaa14f97b8b52 100644 --- a/core/src/main/java/hudson/model/Result.java +++ b/core/src/main/java/hudson/model/Result.java @@ -119,6 +119,13 @@ public final class Result implements Serializable, CustomExportedBean { public String toExportedObject() { return name; } + + public static Result fromString(String s) { + for (Result r : all) + if (s.equals(r.name)) + return r; + return FAILURE; + } private static final long serialVersionUID = 1L; @@ -130,10 +137,7 @@ public final class Result implements Serializable, CustomExportedBean { } protected Object fromString(String s) { - for (Result r : all) - if (s.equals(r.name)) - return r; - return FAILURE; + return Result.fromString(s); } }; } diff --git a/core/src/main/resources/hudson/matrix/MatrixProject/configure-entries.jelly b/core/src/main/resources/hudson/matrix/MatrixProject/configure-entries.jelly index 4f85464e077bd44e1af2980b7b2a593a225f229f..e3b2e5fb45277c259cadcbc78286540eaf6d0867 100644 --- a/core/src/main/resources/hudson/matrix/MatrixProject/configure-entries.jelly +++ b/core/src/main/resources/hudson/matrix/MatrixProject/configure-entries.jelly @@ -114,6 +114,19 @@ THE SOFTWARE. + + + + + + + + + diff --git a/test/src/main/java/org/jvnet/hudson/test/FailureBuilder.java b/test/src/main/java/org/jvnet/hudson/test/FailureBuilder.java index 773821dc48aad66e0119f050d314c66bdb24c04e..ff64e0a6060586fc014ee50245f9494312d22ffd 100644 --- a/test/src/main/java/org/jvnet/hudson/test/FailureBuilder.java +++ b/test/src/main/java/org/jvnet/hudson/test/FailureBuilder.java @@ -41,6 +41,7 @@ import java.io.IOException; * @author Kohsuke Kawaguchi */ public class FailureBuilder extends Builder { + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { listener.getLogger().println("Simulating a failure"); build.setResult(Result.FAILURE); diff --git a/test/src/main/java/org/jvnet/hudson/test/UnstableBuilder.java b/test/src/main/java/org/jvnet/hudson/test/UnstableBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..433e2e24b5c071a485d39567d09fe05f1f881436 --- /dev/null +++ b/test/src/main/java/org/jvnet/hudson/test/UnstableBuilder.java @@ -0,0 +1,61 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi + * + * 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 org.jvnet.hudson.test; + +import hudson.Launcher; +import hudson.Extension; +import hudson.model.AbstractBuild; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.model.Result; +import hudson.tasks.Builder; +import net.sf.json.JSONObject; +import org.kohsuke.stapler.StaplerRequest; + +import java.io.IOException; + +/** + * Mock {@link Builder} that always cause a build to fail. + * + * @author Kohsuke Kawaguchi + */ +public class UnstableBuilder extends Builder { + + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + listener.getLogger().println("Simulating an unstable build"); + build.setResult(Result.UNSTABLE); + return false; + } + + @Extension + public static final class DescriptorImpl extends Descriptor { + public Builder newInstance(StaplerRequest req, JSONObject data) { + throw new UnsupportedOperationException(); + } + + public String getDisplayName() { + return "Make build unstable"; + } + } +} diff --git a/test/src/main/resources/org/jvnet/hudson/test/UnstableBuilder/config.jelly b/test/src/main/resources/org/jvnet/hudson/test/UnstableBuilder/config.jelly new file mode 100644 index 0000000000000000000000000000000000000000..31b61f2adaff63547c23845fd1e53fb916d1d985 --- /dev/null +++ b/test/src/main/resources/org/jvnet/hudson/test/UnstableBuilder/config.jelly @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/test/src/test/java/hudson/matrix/MatrixProjectTest.java b/test/src/test/java/hudson/matrix/MatrixProjectTest.java index 550ee360803dd89923e0ae85361e63d67d81e8c2..f78e111c9b24cf963cb38c21bfd823fe584fe3c9 100644 --- a/test/src/test/java/hudson/matrix/MatrixProjectTest.java +++ b/test/src/test/java/hudson/matrix/MatrixProjectTest.java @@ -33,6 +33,7 @@ import hudson.tasks.Fingerprinter; import org.jvnet.hudson.test.HudsonTestCase; import org.jvnet.hudson.test.SingleFileSCM; import org.jvnet.hudson.test.Email; +import org.jvnet.hudson.test.UnstableBuilder; import java.io.IOException; import java.util.List; @@ -84,6 +85,31 @@ public class MatrixProjectTest extends HudsonTestCase { assertFalse(run.getLog().contains("-Dprop=${db}")); } } + + /** + * Test that configuration filters work + */ + public void testConfigurationFilter() throws Exception { + MatrixProject p = createMatrixProject(); + p.setCombinationFilter("db==\"mysql\""); + MatrixBuild build = p.scheduleBuild2(0).get(); + assertEquals(2, build.getRuns().size()); + } + + /** + * Test that touch stone builds work + */ + public void testTouchStone() throws Exception { + MatrixProject p = createMatrixProject(); + p.setTouchStoneCombinationFilter("db==\"mysql\""); + p.setTouchStoneResultCondition(Result.SUCCESS); + MatrixBuild build = p.scheduleBuild2(0).get(); + assertEquals(4, build.getRuns().size()); + + p.getBuildersList().add(new UnstableBuilder()); + build = p.scheduleBuild2(0).get(); + assertEquals(2, build.getRuns().size()); + } @Override protected MatrixProject createMatrixProject() throws IOException { diff --git a/war/resources/help/matrix/touchstone.html b/war/resources/help/matrix/touchstone.html new file mode 100644 index 0000000000000000000000000000000000000000..012833aacfdffbdfcdfc1c32ed03f3923de58711 --- /dev/null +++ b/war/resources/help/matrix/touchstone.html @@ -0,0 +1,5 @@ +
+ Use a touchstone build if you want to run a sanity check before building all the combinations. + You can select a number of combinations using a combination filter. These will be built first. + If the result satisfies the result condition, the rest of the combinations will be built. +
\ No newline at end of file