diff --git a/pom.xml b/pom.xml
index 204a85f0..41db0596 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,7 +48,7 @@
scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git
https://github.com/jenkinsci/${project.artifactId}-plugin
HEAD
-
+
repo.jenkins-ci.org
@@ -70,5 +70,23 @@
workflow-step-api
1.15
+
+ ${project.groupId}
+ workflow-job
+ 1.15
+ test
+
+
+ ${project.groupId}
+ workflow-cps
+ 2.2-SNAPSHOT
+ test
+
+
+ ${project.groupId}
+ workflow-basic-steps
+ 1.15
+ test
+
diff --git a/src/main/java/org/jenkinsci/plugins/workflow/graph/FlowScanner.java b/src/main/java/org/jenkinsci/plugins/workflow/graph/FlowScanner.java
new file mode 100644
index 00000000..a7676dcf
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/workflow/graph/FlowScanner.java
@@ -0,0 +1,367 @@
+package org.jenkinsci.plugins.workflow.graph;
+
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016, CloudBees, Inc.
+ *
+ * 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.
+ */
+
+import com.google.common.base.Predicate;
+import hudson.model.Action;
+import org.jenkinsci.plugins.workflow.actions.ErrorAction;
+import org.jenkinsci.plugins.workflow.actions.LabelAction;
+import org.jenkinsci.plugins.workflow.actions.LogAction;
+import org.jenkinsci.plugins.workflow.actions.StageAction;
+import org.jenkinsci.plugins.workflow.actions.WorkspaceAction;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Generified algorithms for scanning flows for information
+ * Supports a variety of algorithms for searching, and pluggable conditions
+ * Worth noting: predicates may be stateful here
+ *
+ * ANALYSIS method will
+ * @author Sam Van Oort
+ */
+public class FlowScanner {
+
+ /**
+ * Create a predicate that will match on all FlowNodes having a specific action present
+ * @param actionClass Action class to look for
+ * @param Action type
+ * @return Predicate that will match when FlowNode has the action given
+ */
+ @Nonnull
+ public static Predicate createPredicateWhereActionExists(@Nonnull final Class actionClass) {
+ return new Predicate() {
+ @Override
+ public boolean apply(FlowNode input) {
+ return (input != null && input.getAction(actionClass) != null);
+ }
+ };
+ }
+
+ // Default predicates
+ static final Predicate MATCH_HAS_LABEL = createPredicateWhereActionExists(LabelAction.class);
+ static final Predicate MATCH_IS_STAGE = createPredicateWhereActionExists(StageAction.class);
+ static final Predicate MATCH_HAS_WORKSPACE = createPredicateWhereActionExists(WorkspaceAction.class);
+ static final Predicate MATCH_HAS_ERROR = createPredicateWhereActionExists(ErrorAction.class);
+ static final Predicate MATCH_HAS_LOG = createPredicateWhereActionExists(LogAction.class);
+
+ public interface FlowNodeVisitor {
+ /**
+ * Visit the flow node, and indicate if we should continue analysis
+ * @param f Node to visit
+ * @return False if node is done
+ */
+ public boolean visit(@Nonnull FlowNode f);
+ }
+
+ /** Interface to be used for scanning/analyzing FlowGraphs with support for different visit orders
+ */
+ public interface ScanAlgorithm {
+
+ /**
+ * Search for first node (walking from the heads through parents) that matches the condition
+ * @param heads Nodes to start searching from
+ * @param stopNodes Search doesn't go beyond any of these nodes, null or empty will run to end of flow
+ * @param matchPredicate Matching condition for search
+ * @return First node matching condition, or null if none found
+ */
+ @CheckForNull
+ public FlowNode findFirstMatch(@CheckForNull Collection heads, @CheckForNull Collection stopNodes, @Nonnull Predicate matchPredicate);
+
+ /**
+ * Search for first node (walking from the heads through parents) that matches the condition
+ * @param heads Nodes to start searching from
+ * @param stopNodes Search doesn't go beyond any of these nodes, null or empty will run to end of flow
+ * @param matchPredicate Matching condition for search
+ * @return All nodes matching condition
+ */
+ @Nonnull
+ public Collection filter(@CheckForNull Collection heads, @CheckForNull Collection stopNodes, @Nonnull Predicate matchPredicate);
+
+ /** Used for extracting metrics from the flow graph */
+ public void visitAll(@CheckForNull Collection heads, FlowNodeVisitor visitor);
+ }
+
+ /**
+ * Base class for flow scanners, which offers basic methods and stubs for algorithms
+ * Scanners store state internally, and are not thread-safe but are reusable
+ * Scans/analysis of graphs is implemented via internal iteration to allow reusing algorithm bodies
+ * However internal iteration has access to additional information
+ */
+ public static abstract class AbstractFlowScanner implements ScanAlgorithm {
+
+ // State variables, not all need be used
+ protected ArrayDeque _queue;
+ protected FlowNode _current;
+
+ // Public APIs need to invoke this before searches
+ protected abstract void initialize();
+
+ protected abstract void setHeads(@Nonnull Collection heads);
+
+ /**
+ * Actual meat of the iteration, get the next node to visit, using & updating state as needed
+ * @param blackList Nodes that are not eligible for visiting
+ * @return Next node to visit, or null if we've exhausted the node list
+ */
+ @CheckForNull
+ protected abstract FlowNode next(@Nonnull Collection blackList);
+
+
+ /** Fast internal scan from start through single-parent (unbranched) nodes until we hit a node with one of the following:
+ * - Multiple parents
+ * - No parents
+ * - Satisfies the endCondition predicate
+ *
+ * @param endCondition Predicate that ends search
+ * @return Node satisfying condition
+ */
+ @CheckForNull
+ protected static FlowNode linearScanUntil(@Nonnull FlowNode start, @Nonnull Predicate endCondition) {
+ while(true) {
+ if (endCondition.apply(start)){
+ break;
+ }
+ List parents = start.getParents();
+ if (parents == null || parents.size() == 0 || parents.size() > 1) {
+ break;
+ }
+ start = parents.get(0);
+ }
+ return start;
+ }
+
+ /** Convert stop nodes to a collection that can efficiently be checked for membership, handling nulls if needed */
+ @Nonnull
+ protected Collection convertToFastCheckable(@CheckForNull Collection nodeCollection) {
+ if (nodeCollection == null || nodeCollection.size()==0) {
+ return Collections.EMPTY_SET;
+ } else if (nodeCollection instanceof Set) {
+ return nodeCollection;
+ }
+ return nodeCollection.size() > 5 ? new HashSet(nodeCollection) : nodeCollection;
+ }
+
+ @CheckForNull
+ public FlowNode findFirstMatch(@CheckForNull Collection heads, @Nonnull Predicate matchPredicate) {
+ return this.findFirstMatch(heads, null, matchPredicate);
+ }
+
+ @Nonnull
+ public Collection findAllMatches(@CheckForNull Collection heads, @Nonnull Predicate matchPredicate) {
+ return this.filter(heads, null, matchPredicate);
+ }
+
+ // Basic algo impl
+ public FlowNode findFirstMatch(@CheckForNull Collection heads,
+ @CheckForNull Collection endNodes,
+ Predicate matchCondition) {
+ if (heads == null || heads.size() == 0) {
+ return null;
+ }
+
+ initialize();
+ Collection fastEndNodes = convertToFastCheckable(endNodes);
+ Collection filteredHeads = new HashSet(heads);
+ filteredHeads.removeAll(fastEndNodes);
+ this.setHeads(filteredHeads);
+
+ while ((_current = next(fastEndNodes)) != null) {
+ if (matchCondition.apply(_current)) {
+ return _current;
+ }
+ }
+ return null;
+ }
+
+ // Basic algo impl
+ public List filter(@CheckForNull Collection heads,
+ @CheckForNull Collection endNodes,
+ Predicate matchCondition) {
+ if (heads == null || heads.size() == 0) {
+ return Collections.EMPTY_LIST;
+ }
+ initialize();
+ Collection fastEndNodes = convertToFastCheckable(endNodes);
+ Collection filteredHeads = new HashSet(heads);
+ filteredHeads.removeAll(fastEndNodes);
+ this.setHeads(filteredHeads);
+ ArrayList nodes = new ArrayList();
+
+ while ((_current = next(fastEndNodes)) != null) {
+ if (matchCondition.apply(_current)) {
+ nodes.add(_current);
+ }
+ }
+ return nodes;
+ }
+
+ /** Used for extracting metrics from the flow graph */
+ public void visitAll(@CheckForNull Collection heads, FlowNodeVisitor visitor) {
+ if (heads == null || heads.size() == 0) {
+ return;
+ }
+ initialize();
+ this.setHeads(heads);
+ Collection endNodes = Collections.EMPTY_SET;
+
+ boolean continueAnalysis = true;
+ while (continueAnalysis && (_current = next(endNodes)) != null) {
+ continueAnalysis = visitor.visit(_current);
+ }
+ }
+ }
+
+ /** Does a simple and efficient depth-first search */
+ public static class DepthFirstScanner extends AbstractFlowScanner {
+
+ protected HashSet _visited = new HashSet();
+
+ protected void initialize() {
+ if (this._queue == null) {
+ this._queue = new ArrayDeque();
+ } else {
+ this._queue.clear();
+ }
+ this._visited.clear();
+ this._current = null;
+ }
+
+ @Override
+ protected void setHeads(@Nonnull Collection heads) {
+ // Needs to handle blacklist
+ _queue.addAll(heads);
+ }
+
+ @Override
+ protected FlowNode next(@Nonnull Collection blackList) {
+ FlowNode output = null;
+ if (_current != null) {
+ List parents = _current.getParents();
+ if (parents != null) {
+ for (FlowNode f : parents) {
+ if (!blackList.contains(f) && !_visited.contains(f)) {
+ if (output == null ) {
+ output = f;
+ } else {
+ _queue.push(f);
+ }
+ }
+ }
+ }
+ }
+
+ if (output == null && _queue.size() > 0) {
+ output = _queue.pop();
+ }
+ _visited.add(output);
+ return output;
+ }
+ }
+
+ /**
+ * Scans through a single ancestry, does not cover parallel branches
+ */
+ public static class LinearScanner extends AbstractFlowScanner {
+ protected boolean isFirst = true;
+
+ @Override
+ protected void initialize() {
+ isFirst = true;
+ }
+
+ @Override
+ protected void setHeads(@Nonnull Collection heads) {
+ if (heads.size() > 0) {
+ this._current = heads.iterator().next();
+ }
+ }
+
+ @Override
+ protected FlowNode next(@Nonnull Collection blackList) {
+ if (_current == null) {
+ return null;
+ }
+ if (isFirst) { // Kind of cheating, but works
+ isFirst = false;
+ return _current;
+ }
+ List parents = _current.getParents();
+ if (parents != null && parents.size() > 0) {
+ for (FlowNode f : parents) {
+ if (!blackList.contains(f)) {
+ return f;
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Scanner that jumps over nested blocks
+ */
+ public static class BlockHoppingScanner extends LinearScanner {
+
+ protected FlowNode jumpBlock(FlowNode current) {
+ return (current instanceof BlockEndNode) ?
+ ((BlockEndNode)current).getStartNode() : current;
+ }
+
+ @Override
+ protected FlowNode next(@Nonnull Collection blackList) {
+ if (_current == null) {
+ return null;
+ }
+ if (isFirst) { // Hax, but solves the problem
+ isFirst = false;
+ return _current;
+ }
+ List parents = _current.getParents();
+ if (parents != null && parents.size() > 0) {
+ for (FlowNode f : parents) {
+ if (!blackList.contains(f)) {
+ FlowNode jumped = jumpBlock(f);
+ if (jumped != f) {
+ _current = jumped;
+ return next(blackList);
+ } else {
+ return f;
+ }
+ }
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/workflow/graph/IncrementalFlowAnalysisCache.java b/src/main/java/org/jenkinsci/plugins/workflow/graph/IncrementalFlowAnalysisCache.java
new file mode 100644
index 00000000..6742dc76
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/workflow/graph/IncrementalFlowAnalysisCache.java
@@ -0,0 +1,184 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016, CloudBees, Inc.
+ *
+ * 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.jenkinsci.plugins.workflow.graph;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import org.jenkinsci.plugins.workflow.flow.FlowExecution;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides an efficient way to find the most recent (closest to head) node matching a condition, and get info about it
+ *
+ * This is useful in cases where we are watching an in-progress pipeline execution.
+ * It uses caching and only looks at new nodes (the delta since last execution).
+ * @TODO Thread safety?
+ * @author Sam Van Oort
+ */
+public class IncrementalFlowAnalysisCache {
+
+ Function analysisFunction;
+ Predicate matchCondition;
+ Cache> analysisCache = CacheBuilder.newBuilder().initialCapacity(100).build();
+
+ protected static class IncrementalAnalysis {
+ protected List lastHeadIds = new ArrayList(); // We don't want to hold refs to the actual nodes
+ protected T lastValue;
+
+ /** Gets value from a flownode */
+ protected Function valueExtractor;
+
+ protected Predicate nodeMatchCondition;
+
+ public IncrementalAnalysis(@Nonnull Predicate nodeMatchCondition, @Nonnull Function valueExtractFunction){
+ this.nodeMatchCondition = nodeMatchCondition;
+ this.valueExtractor = valueExtractFunction;
+ }
+
+ /**
+ * Look up a value scanned from the flow
+ * If the heads haven't changed in the flow, return the current heads
+ * If they have, only hunt from the current value until the last one
+ * @param exec
+ * @return
+ */
+ @CheckForNull
+ public T getUpdatedValue(@CheckForNull FlowExecution exec) {
+ if (exec == null) {
+ return null;
+ }
+ List heads = exec.getCurrentHeads();
+ if (heads == null || heads.size() == 0) {
+ return null;
+ }
+ return getUpdatedValueInternal(exec, heads);
+ }
+
+ @CheckForNull
+ public T getUpdatedValue(@CheckForNull FlowExecution exec, @Nonnull List heads) {
+ if (exec == null || heads.size() == 0) {
+ return null;
+ }
+ return getUpdatedValueInternal(exec, heads);
+ }
+
+ /**
+ * Internal implementation
+ * @param exec Execution, used in obtaining node instances
+ * @param heads Heads to scan from, cannot be empty
+ * @return Updated value or null if not present
+ */
+ @CheckForNull
+ protected T getUpdatedValueInternal(@Nonnull FlowExecution exec, @Nonnull List heads) {
+ boolean hasChanged = heads.size() == lastHeadIds.size();
+ if (hasChanged) {
+ for (FlowNode f : heads) {
+ if (!lastHeadIds.contains(f.getId())) {
+ hasChanged = false;
+ break;
+ }
+ }
+ }
+ if (!hasChanged) {
+ updateInternal(exec, heads);
+ }
+ return lastValue;
+ }
+
+ // FlowExecution is used for look
+ protected void updateInternal(@Nonnull FlowExecution exec, @Nonnull List heads) {
+ ArrayList stopNodes = new ArrayList();
+ // Fetch the actual flow nodes to use as halt conditions
+ for (String nodeId : this.lastHeadIds) {
+ try {
+ stopNodes.add(exec.getNode(nodeId));
+ } catch (IOException ioe) {
+ throw new IllegalStateException(ioe);
+ }
+ }
+ FlowNode matchNode = new FlowScanner.BlockHoppingScanner().findFirstMatch(heads, stopNodes, this.nodeMatchCondition);
+ this.lastValue = this.valueExtractor.apply(matchNode);
+
+ this.lastHeadIds.clear();
+ for (FlowNode f : exec.getCurrentHeads()) {
+ lastHeadIds.add(f.getId());
+ }
+ }
+ }
+
+ /**
+ * Get the latest value, using the heads of a FlowExecutions
+ * @param f Flow executions
+ * @return Analysis value, or null no nodes match condition/flow has not begun
+ */
+ @CheckForNull
+ public T getAnalysisValue(@CheckForNull FlowExecution f) {
+ if (f == null) {
+ return null;
+ } else {
+ return getAnalysisValue(f, f.getCurrentHeads());
+ }
+ }
+
+ @CheckForNull
+ public T getAnalysisValue(@CheckForNull FlowExecution exec, @CheckForNull List heads) {
+ if (exec != null && heads == null && heads.size() != 0) {
+ String url;
+ try {
+ url = exec.getUrl();
+ } catch (IOException ioe) {
+ throw new IllegalStateException(ioe);
+ }
+ IncrementalAnalysis analysis = analysisCache.getIfPresent(url);
+ if (analysis != null) {
+ return analysis.getUpdatedValue(exec, heads);
+ } else {
+ IncrementalAnalysis newAnalysis = new IncrementalAnalysis(matchCondition, analysisFunction);
+ T value = newAnalysis.getUpdatedValue(exec, heads);
+ analysisCache.put(url, newAnalysis);
+ return value;
+ }
+ }
+ return null;
+ }
+
+ public IncrementalFlowAnalysisCache(Predicate matchCondition, Function analysisFunction) {
+ this.matchCondition = matchCondition;
+ this.analysisFunction = analysisFunction;
+ }
+
+ public IncrementalFlowAnalysisCache(Predicate matchCondition, Function analysisFunction, Cache myCache) {
+ this.matchCondition = matchCondition;
+ this.analysisFunction = analysisFunction;
+ this.analysisCache = myCache;
+ }
+}
diff --git a/src/test/java/org/jenkinsci/plugins/workflow/graph/TestFlowScanner.java b/src/test/java/org/jenkinsci/plugins/workflow/graph/TestFlowScanner.java
new file mode 100644
index 00000000..3983664c
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/workflow/graph/TestFlowScanner.java
@@ -0,0 +1,233 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016, CloudBees, Inc.
+ *
+ * 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.jenkinsci.plugins.workflow.graph;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
+import org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode;
+import org.jenkinsci.plugins.workflow.flow.FlowExecution;
+import org.jenkinsci.plugins.workflow.job.WorkflowJob;
+import org.jenkinsci.plugins.workflow.job.WorkflowRun;
+import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.BuildWatcher;
+import org.jvnet.hudson.test.JenkinsRule;
+
+import javax.annotation.Nonnull;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class TestFlowScanner {
+
+ @ClassRule
+ public static BuildWatcher buildWatcher = new BuildWatcher();
+
+ @Rule public JenkinsRule r = new JenkinsRule();
+
+ public static Predicate predicateMatchStepDescriptor(@Nonnull final String descriptorId) {
+ Predicate outputPredicate = new Predicate() {
+ @Override
+ public boolean apply(FlowNode input) {
+ if (input instanceof StepAtomNode) {
+ StepAtomNode san = (StepAtomNode)input;
+ StepDescriptor sd = san.getDescriptor();
+ return sd != null && descriptorId.equals(sd.getId());
+ }
+ return false;
+ }
+ };
+ return outputPredicate;
+ }
+
+ static final class CollectingVisitor implements FlowScanner.FlowNodeVisitor {
+ ArrayList visited = new ArrayList();
+
+ @Override
+ public boolean visit(@Nonnull FlowNode f) {
+ visited.add(f);
+ return true;
+ }
+
+ public void reset() {
+ this.visited.clear();
+ }
+
+ public ArrayList getVisited() {
+ return visited;
+ }
+ };
+
+ /** Tests the basic scan algorithm, predicate use, start/stop nodes */
+ @Test
+ public void testSimpleScan() throws Exception {
+ WorkflowJob job = r.jenkins.createProject(WorkflowJob.class, "Convoluted");
+ job.setDefinition(new CpsFlowDefinition(
+ "sleep 2 \n" +
+ "echo 'donothing'\n" +
+ "echo 'doitagain'"
+ ));
+ WorkflowRun b = r.assertBuildStatusSuccess(job.scheduleBuild2(0));
+ FlowExecution exec = b.getExecution();
+ FlowScanner.ScanAlgorithm[] scans = {new FlowScanner.LinearScanner(),
+ new FlowScanner.DepthFirstScanner(),
+ new FlowScanner.BlockHoppingScanner()
+ };
+
+ Predicate echoPredicate = predicateMatchStepDescriptor("org.jenkinsci.plugins.workflow.steps.EchoStep");
+ List heads = exec.getCurrentHeads();
+
+ // Test expected scans with no stop nodes given (different ways of specifying none)
+ for (FlowScanner.ScanAlgorithm sa : scans) {
+ System.out.println("Testing class: "+sa.getClass());
+ FlowNode node = sa.findFirstMatch(heads, null, echoPredicate);
+ Assert.assertEquals(exec.getNode("5"), node);
+ node = sa.findFirstMatch(heads, Collections.EMPTY_LIST, echoPredicate);
+ Assert.assertEquals(exec.getNode("5"), node);
+ node = sa.findFirstMatch(heads, Collections.EMPTY_SET, echoPredicate);
+ Assert.assertEquals(exec.getNode("5"), node);
+
+ Collection nodeList = sa.filter(heads, null, echoPredicate);
+ FlowNode[] expected = new FlowNode[]{exec.getNode("5"), exec.getNode("4")};
+ Assert.assertArrayEquals(expected, nodeList.toArray());
+ nodeList = sa.filter(heads, Collections.EMPTY_LIST, echoPredicate);
+ Assert.assertArrayEquals(expected, nodeList.toArray());
+ nodeList = sa.filter(heads, Collections.EMPTY_SET, echoPredicate);
+ Assert.assertArrayEquals(expected, nodeList.toArray());
+ }
+
+ // Test with no matches
+ for (FlowScanner.ScanAlgorithm sa : scans) {
+ System.out.println("Testing class: "+sa.getClass());
+ FlowNode node = sa.findFirstMatch(heads, null, (Predicate)Predicates.alwaysFalse());
+ Assert.assertNull(node);
+
+ Collection nodeList = sa.filter(heads, null, (Predicate) Predicates.alwaysFalse());
+ Assert.assertNotNull(nodeList);
+ Assert.assertEquals(0, nodeList.size());
+ }
+
+
+ CollectingVisitor vis = new CollectingVisitor();
+ // Verify we touch head and foot nodes too
+ for (FlowScanner.ScanAlgorithm sa : scans) {
+ System.out.println("Testing class: " + sa.getClass());
+ Collection nodeList = sa.filter(heads, null, (Predicate) Predicates.alwaysTrue());
+ vis.reset();
+ sa.visitAll(heads, vis);
+ Assert.assertEquals(5, nodeList.size());
+ Assert.assertEquals(5, vis.getVisited().size());
+ }
+
+ // Test with a stop node given, sometimes no matches
+ Collection noMatchEndNode = Collections.singleton(exec.getNode("5"));
+ Collection singleMatchEndNode = Collections.singleton(exec.getNode("4"));
+ for (FlowScanner.ScanAlgorithm sa : scans) {
+ FlowNode node = sa.findFirstMatch(heads, noMatchEndNode, echoPredicate);
+ Assert.assertNull(node);
+
+ Collection nodeList = sa.filter(heads, noMatchEndNode, echoPredicate);
+ Assert.assertNotNull(nodeList);
+ Assert.assertEquals(0, nodeList.size());
+
+ // Now we try with a stop list the reduces node set for multiple matches
+ node = sa.findFirstMatch(heads, singleMatchEndNode, echoPredicate);
+ Assert.assertEquals(exec.getNode("5"), node);
+ nodeList = sa.filter(heads, singleMatchEndNode, echoPredicate);
+ Assert.assertNotNull(nodeList);
+ Assert.assertEquals(1, nodeList.size());
+ Assert.assertEquals(exec.getNode("5"), nodeList.iterator().next());
+ }
+ }
+
+ /** Tests the basic scan algorithm where blocks are involved */
+ @Test
+ public void testBlockScan() throws Exception {
+ WorkflowJob job = r.jenkins.createProject(WorkflowJob.class, "Convoluted");
+ job.setDefinition(new CpsFlowDefinition(
+ "echo 'first'\n" +
+ "timeout(time: 10, unit: 'SECONDS') {\n" +
+ " echo 'second'\n" +
+ " echo 'third'\n" +
+ "}\n" +
+ "sleep 1"
+ ));
+ WorkflowRun b = r.assertBuildStatusSuccess(job.scheduleBuild2(0));
+ Predicate matchEchoStep = predicateMatchStepDescriptor("org.jenkinsci.plugins.workflow.steps.EchoStep");
+
+ // Test blockhopping
+ FlowScanner.BlockHoppingScanner blockHoppingScanner = new FlowScanner.BlockHoppingScanner();
+ Collection matches = blockHoppingScanner.filter(b.getExecution().getCurrentHeads(), null, matchEchoStep);
+
+ // This means we jumped the blocks
+ Assert.assertEquals(1, matches.size());
+
+ FlowScanner.DepthFirstScanner depthFirstScanner = new FlowScanner.DepthFirstScanner();
+ matches = depthFirstScanner.filter(b.getExecution().getCurrentHeads(), null, matchEchoStep);
+
+ // Nodes all covered
+ Assert.assertEquals(3, matches.size());
+ }
+
+ /** And the parallel case */
+ @Test
+ public void testParallelScan() throws Exception {
+ WorkflowJob job = r.jenkins.createProject(WorkflowJob.class, "Convoluted");
+ job.setDefinition(new CpsFlowDefinition(
+ "echo 'first'\n" +
+ "def steps = [:]\n" +
+ "steps['1'] = {\n" +
+ " echo 'do 1 stuff'\n" +
+ "}\n" +
+ "steps['2'] = {\n" +
+ " echo '2a'\n" +
+ " echo '2b'\n" +
+ "}\n" +
+ "parallel steps\n" +
+ "echo 'final'"
+ ));
+ WorkflowRun b = r.assertBuildStatusSuccess(job.scheduleBuild2(0));
+ Collection heads = b.getExecution().getCurrentHeads();
+ Predicate matchEchoStep = predicateMatchStepDescriptor("org.jenkinsci.plugins.workflow.steps.EchoStep");
+
+ FlowScanner.ScanAlgorithm scanner = new FlowScanner.LinearScanner();
+ Collection matches = scanner.filter(heads, null, matchEchoStep);
+ Assert.assertTrue(matches.size() >= 3 && matches.size() <= 4);
+
+ scanner = new FlowScanner.DepthFirstScanner();
+ matches = scanner.filter(heads, null, matchEchoStep);
+ Assert.assertTrue(matches.size() == 5);
+
+ scanner = new FlowScanner.BlockHoppingScanner();
+ matches = scanner.filter(heads, null, matchEchoStep);
+ Assert.assertTrue(matches.size() == 2);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/jenkinsci/plugins/workflow/graph/TestIncrementalFlowAnalysis.java b/src/test/java/org/jenkinsci/plugins/workflow/graph/TestIncrementalFlowAnalysis.java
new file mode 100644
index 00000000..5e1c87ee
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/workflow/graph/TestIncrementalFlowAnalysis.java
@@ -0,0 +1,79 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016, CloudBees, Inc.
+ *
+ * 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.jenkinsci.plugins.workflow.graph;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import org.jenkinsci.plugins.workflow.actions.LabelAction;
+import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
+import org.jenkinsci.plugins.workflow.flow.FlowExecution;
+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.JenkinsRule;
+
+
+/**
+ * @author svanoort
+ */
+public class TestIncrementalFlowAnalysis {
+ @ClassRule
+ public static BuildWatcher buildWatcher = new BuildWatcher();
+
+ @Rule
+ public JenkinsRule r = new JenkinsRule();
+
+ /** Tests the basic incremental analysis */
+ @Test
+ public void testIncrementalAnalysis() throws Exception {
+ WorkflowJob job = r.jenkins.createProject(WorkflowJob.class, "Convoluted");
+ job.setDefinition(new CpsFlowDefinition(
+ "for (int i=0; i<4; i++) {\n" +
+ " stage \"stage-$i\"\n" +
+ " echo \"Doing $i\"\n" +
+ "}"
+ ));
+
+ // Search conditions
+ Predicate labelledNode = FlowScanner.createPredicateWhereActionExists(LabelAction.class);
+ Function getLabelFunction = new Function() {
+ @Override
+ public String apply(FlowNode input) {
+ LabelAction labelled = input.getAction(LabelAction.class);
+ return (labelled != null) ? labelled.getDisplayName() : null;
+ }
+ };
+
+ IncrementalFlowAnalysisCache incrementalAnalysis = new IncrementalFlowAnalysisCache(labelledNode, getLabelFunction);
+ WorkflowRun b = r.assertBuildStatusSuccess(job.scheduleBuild2(0));
+ FlowExecution exec = b.getExecution();
+ FlowNode test = exec.getNode("4");
+
+ // TODO add tests based on calling incremental analysis from points further along flow, possible in some paralle cases
+ }
+}