Skip to content

Commit

Permalink
feat: add a cycle tester
Browse files Browse the repository at this point in the history
  • Loading branch information
petey committed Oct 12, 2017
1 parent 5545b06 commit 90f7abf
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 2 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ npm install screwdriver-workflow-parser
```

```
const { getWorkflow, getNextJobs } = require('screwdriver-workflow-parser');
const { getWorkflow, getNextJobs, hasCycle } = require('screwdriver-workflow-parser');
// Calculate the directed graph workflow from a pipeline config (and parse legacy workflows)
const workflowGraph = getWorkflow(pipelineConfig, { useLegacy: true });
Expand All @@ -26,7 +26,12 @@ const workflowGraph = getWorkflow(pipelineConfig, { useLegacy: true });
const commitJobsToTrigger = getNextJobs(workflowGraph, { trigger: '~commit' });
// Get a list of job names to start as a result of a pull-request event, e.g. [ 'PR-123:a' ]
const prJobsToTrigger = getNextJobs(workflowGraph, { trigger: '~pr', prNum: 123 });
const prJobsToTrigger = getNextJobs(workflowGraph, { trigger: '~pr', prNum: 123 });
// Check to see if a given workflow graph has a loop in it. A -> B -> A
if (hasCycle(workflowGraph)) {
console.error('Graph contains a loop.');
}
```

## Testing
Expand Down
42 changes: 42 additions & 0 deletions lib/hasCycle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

const getNextJobs = require('./getNextJobs');

/**
* Recursively verify if there is a cycle starting from job
* @method walk
* @param {Object} workflowGraph Directed graph representation of workflow
* @param {String} jobName Job Name
* @param {Set} visitedJobs Unique list of visited jobs
* @return {Boolean} True if a cycle detected
*/
const walk = (workflowGraph, jobName, visitedJobs) => {
visitedJobs.add(jobName);

const triggerList = getNextJobs(workflowGraph, { trigger: jobName, prNum: 1 });

// Hit a leaf node, must be good
if (triggerList.length === 0) {
return false;
}

// Check to see if we visited this job before
if (triggerList.some(name => visitedJobs.has(name))) {
return true;
}

// recursively walk starting from the new jobs
return triggerList.some(name => walk(workflowGraph, name, visitedJobs));
};

/**
* Calculate if the workflow contains a cycle, e.g. A -> B -> A
* @method hasCycle
* @param {Object} workflowGraph Graph representation of workflow
* @return {Boolean} True if a cycle exists anywhere in the workflow
*/
const hasCycle = workflowGraph =>
// Check from all the nodes to capture deteached workflows
workflowGraph.nodes.some(node => walk(workflowGraph, node.name, new Set()));

module.exports = hasCycle;
87 changes: 87 additions & 0 deletions test/lib/hasCycle.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict';

const assert = require('chai').assert;
const hasCycle = require('../../lib/hasCycle');

describe('hasCyles', () => {
it('should return true if a workflow has a cycle', () => {
const workflow = {
nodes: [
{ name: '~pr' },
{ name: '~commit' },
{ name: 'A' },
{ name: 'B' },
{ name: 'C' },
{ name: 'D' },
{ name: 'E' },
{ name: 'F' }
],
edges: [
{ src: '~commit', dest: 'A' }, // start
{ src: 'A', dest: 'B' }, // parallel
{ src: 'A', dest: 'C' }, // parallel
{ src: 'B', dest: 'D' }, // serial
{ src: 'C', dest: 'E' }, // serial
{ src: 'D', dest: 'F', join: true }, // join
{ src: 'E', dest: 'F', join: true }, // join
{ src: 'F', dest: 'A' } // cycle
]
};

assert.isTrue(hasCycle(workflow));
});

it('should return true if a detached workflow has a cycle', () => {
const workflow = {
nodes: [
{ name: '~pr' },
{ name: '~commit' },
{ name: 'A' },
{ name: 'B' },
{ name: 'C' },
{ name: 'D' }
],
edges: [
{ src: '~commit', dest: 'A' }, // start
{ src: 'A', dest: 'B' }, // end
{ src: 'C', dest: 'D' }, // detached start
{ src: 'D', dest: 'C' } // detached cycle
]
};

assert.isTrue(hasCycle(workflow));
});

it('should return true if a job requires itself', () => {
const workflow = {
nodes: [
{ name: '~pr' },
{ name: '~commit' },
{ name: 'A' }
],
edges: [
{ src: '~commit', dest: 'A' }, // start
{ src: 'A', dest: 'A' } // cycle
]
};

assert.isTrue(hasCycle(workflow));
});

it('should return false if a workflow does not have a cycle', () => {
const workflow = {
nodes: [
{ name: '~pr' },
{ name: '~commit' },
{ name: 'A' },
{ name: 'B' }
],
edges: [
{ src: '~commit', dest: 'A' }, // start
{ src: 'A', dest: 'B' } // end
]
};

assert.isFalse(hasCycle(workflow));
});
});

0 comments on commit 90f7abf

Please sign in to comment.