From 2dbb20e367db134f8ec162458c4a2bd07d6c98ca Mon Sep 17 00:00:00 2001 From: Riley Bauer Date: Fri, 2 Nov 2018 22:29:39 -0700 Subject: [PATCH] Adds many more static workflow parser tests --- frontend/package-lock.json | 46 +-- frontend/src/lib/StaticGraphParser.test.ts | 431 ++++++++++++++++++++- 2 files changed, 422 insertions(+), 55 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ba040886918..930a252616e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,5 +1,5 @@ { - "name": "seira-frontend", + "name": "pipelines-frontend", "version": "0.1.0", "lockfileVersion": 1, "requires": true, @@ -594,15 +594,6 @@ "@types/react-router": "4.0.31" } }, - "@types/react-swipeable-views": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/react-swipeable-views/-/react-swipeable-views-0.12.2.tgz", - "integrity": "sha512-c+OFdmEMUtdGeADR7OmnIUTNoejJTBjO64vBPFkdIpKiABS+DXtUHCzM34U/+Wy7FGeLoPHpKGnm2tcMMst/LA==", - "dev": true, - "requires": { - "@types/react": "16.4.14" - } - }, "@types/react-test-renderer": { "version": "16.0.2", "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.0.2.tgz", @@ -11354,41 +11345,6 @@ "whatwg-fetch": "2.0.3" } }, - "react-swipeable-views": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/react-swipeable-views/-/react-swipeable-views-0.13.0.tgz", - "integrity": "sha512-r6H8lbtcI99oKykpLxYrI6O9im1lJ4D5/hf8bkNeQLdHZ9ftxS03qgEtguy3GpT5VB9yS4gErYWeaTrhCrysEg==", - "requires": { - "@babel/runtime": "7.0.0", - "dom-helpers": "3.3.1", - "prop-types": "15.6.2", - "react-swipeable-views-core": "0.13.0", - "react-swipeable-views-utils": "0.13.0", - "warning": "4.0.2" - } - }, - "react-swipeable-views-core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/react-swipeable-views-core/-/react-swipeable-views-core-0.13.0.tgz", - "integrity": "sha512-MAe119eSN4obiqsIp+qoUWtLbyjz+dWEfz+qPurPvyIFoXxuxpBnsDy36+C7cBaCi5z4dRmfoMlm1dBAdIzvig==", - "requires": { - "@babel/runtime": "7.0.0", - "warning": "4.0.2" - } - }, - "react-swipeable-views-utils": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/react-swipeable-views-utils/-/react-swipeable-views-utils-0.13.0.tgz", - "integrity": "sha512-1I4BhDqA6qkRdW0nexnudh/QdvVAVy0a7M5OyU2TrjaTovg6ufBouzqfqjZfUZUxVdOftTkPtisHmcqqZ+b1TA==", - "requires": { - "@babel/runtime": "7.0.0", - "fbjs": "0.8.17", - "keycode": "2.2.0", - "prop-types": "15.6.2", - "react-event-listener": "0.6.3", - "react-swipeable-views-core": "0.13.0" - } - }, "react-test-renderer": { "version": "16.5.2", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.5.2.tgz", diff --git a/frontend/src/lib/StaticGraphParser.test.ts b/frontend/src/lib/StaticGraphParser.test.ts index 4060238136c..35b134a1c80 100644 --- a/frontend/src/lib/StaticGraphParser.test.ts +++ b/frontend/src/lib/StaticGraphParser.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createGraph } from './StaticGraphParser'; +import { createGraph, getNodeInfo } from './StaticGraphParser'; describe('StaticGraphParser', () => { @@ -24,21 +24,66 @@ describe('StaticGraphParser', () => { entrypoint: 'template-1', templates: [ { - dag: { - tasks: [ - { - name: 'task-1', - template: 'task-1', - }, - ], - }, + dag: { tasks: [{ name: 'task-1', template: 'task-1', },], }, name: 'template-1', }, + { + container: {}, + name: 'container-1', + }, ], }, }; } + // In this pipeline, the conditionals are the templates: 'condition-1' and 'condition-2' + // 'condition-1-child' and 'condition-2-child' are not displayed in the static graph, but they + // are used by the parser to properly connect the nodes. + function newConditionalWorkflow(): any { + return { + spec: { + entrypoint: 'pipeline-flip-coin', + templates: [ + { + name: 'condition-1', + steps: [[{ + name: 'condition-1-child', + template: 'condition-1-child', + when: '{{inputs.parameters.flip-output}} == heads' + }]] + }, + { dag: { tasks: [{ name: 'heads', template: 'heads' }] }, name: 'condition-1-child' }, + { + name: 'condition-2', + steps: [[{ + name: 'condition-2-child', + template: 'condition-2-child', + when: '{{inputs.parameters.flip-output}} == tails' + }]] + }, + { dag: { tasks: [{ name: 'tails', template: 'tails' }] }, name: 'condition-2-child' }, + { + dag: { + tasks: [ + { dependencies: ['flip'], name: 'condition-1', template: 'condition-1' }, + { dependencies: ['flip'], name: 'condition-2', template: 'condition-2' }, + { name: 'flip', template: 'flip' } + ] + }, + name: 'pipeline-flip-coin' + }, + { + container: { args: [ /* omitted */], command: ['sh', '-c'], }, + name: 'flip', + outputs: { parameters: [{ name: 'flip-output', valueFrom: { path: '/tmp/output' } }] } + }, + { container: { command: ['echo', '\'heads\''], }, name: 'heads', }, + { container: { command: ['echo', '\'tails\''], }, name: 'tails', }, + ] + } + }; + } + describe('createGraph', () => { it('Creates a single node with no edges for a workflow with one step.', () => { const workflow = newWorkflow(); @@ -79,6 +124,56 @@ describe('StaticGraphParser', () => { expect(g.edges()[0].w).toBe('task-2'); }); + it('Shows conditional nodes without adding conditional children as nodes', () => { + const g = createGraph(newConditionalWorkflow()); + expect(g.nodeCount()).toBe(5); + ['flip', 'condition-1', 'condition-2', 'heads', 'tails'].forEach((nodeName) => { + expect(g.nodes()).toContain(nodeName); + }); + }); + + it('Connects conditional graph correctly', () => { + const g = createGraph(newConditionalWorkflow()); + // 'flip' has two possible outcomes: 'condition-1' and 'condition-2' + expect(g.edges()[0].v).toBe('flip'); + expect(g.edges()[0].w).toBe('condition-1'); + expect(g.edges()[1].v).toBe('flip'); + expect(g.edges()[1].w).toBe('condition-2'); + // condition-1 means the 'heads' node will run + expect(g.edges()[2].v).toBe('condition-1'); + expect(g.edges()[2].w).toBe('heads'); + // condition-2 means the 'tails' node will run + expect(g.edges()[3].v).toBe('condition-2'); + expect(g.edges()[3].w).toBe('tails'); + // Confirm there are no other nodes or edges + expect(g.nodeCount()).toBe(5); + expect(g.edgeCount()).toBe(4); + }); + + it('Finds conditionals and colors them', () => { + const g = createGraph(newConditionalWorkflow()); + g.nodes().forEach((nodeName) => { + const node = g.node(nodeName); + if (nodeName.startsWith('condition')) { + expect(node.bgColor).toBe('cornsilk'); + } else { + expect(node.bgColor).toBeUndefined(); + } + }); + }); + + it('Renders extremely simple workflows with no steps or DAGs', () => { + const simpleWorkflow = { + spec: { + entrypoint: 'template-1', + templates: [ { container: {}, name: 'template-1', }, ], + }, + } as any; + const g = createGraph(simpleWorkflow); + expect(g.nodeCount()).toBe(1); + expect(g.edgeCount()).toBe(0); + }); + // This test covers the way that compiled Pipelines handle DSL-defined exit-handlers. // These exit-handlers are different from Argo's 'onExit'. // The compiled Pipelines will contain 1 DAG for the entry point, that has a single task which @@ -129,7 +224,7 @@ describe('StaticGraphParser', () => { expect(() => createGraph(workflow)).toThrowError('Pipeline had template with multiple steps.'); }); - it('Returns an error message if the workflow spec has a template with multiple steps', () => { + it('Throws an error message if the workflow spec has a template with multiple steps', () => { const workflow = { spec: { templates: [ @@ -147,4 +242,320 @@ describe('StaticGraphParser', () => { expect(() => createGraph(workflow)).toThrowError('Pipeline had template with multiple steps'); }); }); + + describe('getNodeInfo', () => { + it('Returns nodeInfo containing only \'unknown\' nodeType if nodeId is undefined', () => { + const nodeInfo = getNodeInfo(newWorkflow(), undefined); + expect(nodeInfo).toEqual({ nodeType: 'unknown' }); + }); + + it('Returns nodeInfo containing only \'unknown\' nodeType if nodeId is empty', () => { + const nodeInfo = getNodeInfo(newWorkflow(), ''); + expect(nodeInfo).toEqual({ nodeType: 'unknown' }); + }); + + it('Returns nodeInfo containing only \'unknown\' nodeType if workflow is undefined', () => { + const nodeInfo = getNodeInfo(undefined, 'someId'); + expect(nodeInfo).toEqual({ nodeType: 'unknown' }); + }); + + it('Returns nodeInfo containing only \'unknown\' nodeType if workflow.spec is undefined', () => { + const workflow = newWorkflow(); + workflow.spec = undefined; + const nodeInfo = getNodeInfo(workflow, 'someId'); + expect(nodeInfo).toEqual({ nodeType: 'unknown' }); + }); + + it('Returns nodeInfo containing only \'unknown\' nodeType if workflow.spec.templates is undefined', () => { + const workflow = newWorkflow(); + workflow.spec.templates = undefined; + const nodeInfo = getNodeInfo(workflow, 'someId'); + expect(nodeInfo).toEqual({ nodeType: 'unknown' }); + }); + + it('Returns nodeInfo containing only \'unknown\' nodeType if no template matches provided ID', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + container: {}, + name: 'template-1', + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'id-not-in-spec'); + expect(nodeInfo).toEqual({ nodeType: 'unknown' }); + }); + + it('Returns nodeInfo containing only \'unknown\' nodeType if template matching provided ID has no container or steps', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + // No container or steps here + name: 'template-1', + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo).toEqual({ nodeType: 'unknown' }); + }); + + it('Returns nodeInfo containing only \'unknown\' nodeType if template matching provided ID has no container and empty steps', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + // No container here + name: 'template-1', + steps: [], + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo).toEqual({ nodeType: 'unknown' }); + }); + + it('Returns nodeInfo containing only \'unknown\' nodeType if template matching provided ID has no container and array of empty steps', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + // No container here + name: 'template-1', + steps: [[]], + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo).toEqual({ nodeType: 'unknown' }); + }); + + it('Returns nodeInfo containing container args', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + container: { + args: ['arg1', 'arg2'], + }, + name: 'template-1', + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo.nodeType).toBe('container'); + expect(nodeInfo.containerInfo!.args).toEqual(['arg1', 'arg2']); + }); + + it('Returns nodeInfo containing container commands', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + container: { + command: ['command1', 'command2'] + }, + name: 'template-1', + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo.nodeType).toBe('container'); + expect(nodeInfo.containerInfo!.command).toEqual(['command1', 'command2']); + }); + + it('Returns nodeInfo containing container image name', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + container: { + image: 'someImageID' + }, + name: 'template-1', + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo.nodeType).toBe('container'); + expect(nodeInfo.containerInfo!.image).toBe('someImageID'); + }); + + it('Returns nodeInfo containing container inputs as list of name/value duples', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + container: {}, + inputs: { + parameters: [ + { name: 'param1', value: 'val1' }, + { name: 'param2', value: 'val2' }, + ], + }, + name: 'template-1', + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo.nodeType).toBe('container'); + expect(nodeInfo.containerInfo!.inputs).toEqual([['param1', 'val1'], ['param2', 'val2']]); + }); + + it('Returns empty strings for inputs with no specified value', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + container: {}, + inputs: { + parameters: [ + { name: 'param1' }, + { name: 'param2' }, + ], + }, + name: 'template-1', + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo.nodeType).toBe('container'); + expect(nodeInfo.containerInfo!.inputs).toEqual([['param1', ''], ['param2', '']]); + }); + + it('Returns nodeInfo containing container outputs as list of name/value duples, pulling from valueFrom if necessary', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + container: {}, + name: 'template-1', + outputs: { + parameters: [ + { name: 'param1', value: 'val1' }, + { name: 'param2', valueFrom: { jsonPath: 'jsonPath' } }, + { name: 'param3', valueFrom: { path: 'path' } }, + { name: 'param4', valueFrom: { parameter: 'parameterReference' } }, + { name: 'param5', valueFrom: { jqFilter: 'jqFilter' } }, + ], + }, + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo.nodeType).toBe('container'); + expect(nodeInfo.containerInfo!.outputs).toEqual([ + ['param1', 'val1'], + ['param2', 'jsonPath'], + ['param3', 'path'], + ['param4', 'parameterReference'], + ['param5', 'jqFilter'], + ]); + }); + + it('Returns empty strings for outputs with no specified value', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + container: {}, + name: 'template-1', + outputs: { + parameters: [ + { name: 'param1' }, + { name: 'param2' }, + ], + }, + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo.nodeType).toBe('container'); + expect(nodeInfo.containerInfo!.outputs).toEqual([['param1', ''], ['param2', '']]); + }); + + it('Returns nodeType \'steps\' when node template has steps', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + name: 'template-1', + steps: [[ + 'something', + ]] + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo.nodeType).toBe('steps'); + expect(nodeInfo.stepsInfo).toEqual({ conditional: '', parameters: [[]]); + }); + + it('Returns nodeInfo with step template\'s conditional when node template has \'when\' property', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + name: 'template-1', + steps: [[ { when: '{{someVar}} == something' } ]] + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo.nodeType).toBe('steps'); + expect(nodeInfo.stepsInfo).toEqual({ conditional: '{{someVar}} == something', parameters: [[]]); + }); + + it('Returns nodeInfo with step template\'s arguments when node template has \'when\' property', () => { + const workflow = { + spec: { + entrypoint: 'template-1', + templates: [ + { + name: 'template-1', + steps: [[{ + arguments: { + parameters: [ + { name: 'param1', value: 'val1' }, + { name: 'param2', value: 'val2' }, + ], + }, + }]] + }, + ], + }, + } as any; + const nodeInfo = getNodeInfo(workflow, 'template-1'); + expect(nodeInfo.nodeType).toBe('steps'); + expect(nodeInfo.stepsInfo) + .toEqual({ conditional: '', parameters: [['param1', 'val1'], ['param2', 'val2']]); + }); + }); });