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 + } +}