blob: 9e89f018c0c3c54deaa068e405371f3f575d2b6a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env mocha */
'use strict'
const assert = require('assert')
const composer = require('../composer')
const wsk = require('../client')()
const name = 'TestAction'
// deploy action
const define = action => wsk.actions.delete(action.name).catch(() => { }).then(() => wsk.actions.create(action))
// deploy and invoke composition
const invoke = (composition, params = {}, blocking = true) => wsk.compositions.deploy(Object.assign({ name }, composition.compile()), true)
.then(() => wsk.actions.invoke({ name, params, blocking }))
.then(activation => activation.response.success ? activation : Promise.reject(Object.assign(new Error(), { error: activation })))
// redis configuration
const redis = process.env.REDIS ? { uri: process.env.REDIS } : false
if (process.env.REDIS && process.env.REDIS_CA) redis.ca = process.env.REDIS_CA
// openwhisk configuration
const openwhisk = process.env.__OW_IGNORE_CERTS ? { ignore_certs: true } : {}
describe('composer', function () {
let n, x, y // dummy variables
this.timeout(60000)
before('deploy test actions', function () {
if (!redis) console.error('------------------------------------------------\nMissing redis configuration, skipping some tests\n------------------------------------------------')
return define({ name: 'echo', action: 'const main = x=>x' })
.then(() => define({ name: 'DivideByTwo', action: 'function main({n}) { return { n: n / 2 } }' }))
.then(() => define({ name: 'TripleAndIncrement', action: 'function main({n}) { return { n: n * 3 + 1 } }' }))
.then(() => define({ name: 'isNotOne', action: 'function main({n}) { return { value: n != 1 } }' }))
.then(() => define({ name: 'isEven', action: 'function main({n}) { return { value: n % 2 == 0 } }' }))
.then(() => wsk.compositions.deploy(Object.assign({ name: '_DivideByTwo' }, composer.seq('DivideByTwo').compile()), true))
})
describe('blocking invocations', function () {
describe('actions', function () {
it('action must return true', function () {
return invoke(composer.action('isNotOne'), { n: 0 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
})
it('action must return false', function () {
return invoke(composer.action('isNotOne'), { n: 1 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: false }))
})
it('action must return activationId', function () {
return invoke(composer.async('isNotOne'), { n: 1, $composer: { openwhisk } }).then(activation => assert.ok(activation.response.result.activationId))
})
it('action name must parse to fully qualified', function () {
let combos = [
{ n: 42, s: false, e: 'Name must be a string' },
{ n: '', s: false, e: 'Name is not valid' },
{ n: ' ', s: false, e: 'Name is not valid' },
{ n: '/', s: false, e: 'Name is not valid' },
{ n: '//', s: false, e: 'Name is not valid' },
{ n: '/a', s: false, e: 'Name is not valid' },
{ n: '/a/b/c/d', s: false, e: 'Name is not valid' },
{ n: '/a/b/c/d/', s: false, e: 'Name is not valid' },
{ n: 'a/b/c/d', s: false, e: 'Name is not valid' },
{ n: '/a/ /b', s: false, e: 'Name is not valid' },
{ n: 'a', e: false, s: '/_/a' },
{ n: 'a/b', e: false, s: '/_/a/b' },
{ n: 'a/b/c', e: false, s: '/a/b/c' },
{ n: '/a/b', e: false, s: '/a/b' },
{ n: '/a/b/c', e: false, s: '/a/b/c' }
]
combos.forEach(({ n, s, e }) => {
if (s) {
// good cases
assert.ok(composer.action(n).name, s)
} else {
// error cases
try {
composer.action(n)
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith(e))
}
}
})
})
it('invalid options', function () {
try {
invoke(composer.action('foo', 42))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Invalid argument'))
}
})
it('too many arguments', function () {
try {
invoke(composer.action('foo', {}, 'foo'))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Too many arguments'))
}
})
})
describe('dynamic', function () {
it('dynamic action invocation', function () {
return invoke(composer.dynamic(), { type: 'action', name: 'DivideByTwo', params: { n: 42 } }).then(activation => assert.deepStrictEqual(activation.response.result, { n: 21 }))
})
it('missing type', function () {
return invoke(composer.dynamic(), { name: 'DivideByTwo', params: { n: 42 } }).then(() => assert.fail(), activation => assert.ok(activation.error.response.result.error))
})
it('invalid type', function () {
return invoke(composer.dynamic(), { type: 42, name: 'DivideByTwo', params: { n: 42 } }).then(() => assert.fail(), activation => assert.ok(activation.error.response.result.error))
})
it('missing name', function () {
return invoke(composer.dynamic(), { type: 'action', params: { n: 42 } }).then(() => assert.fail(), activation => assert.ok(activation.error.response.result.error))
})
it('missing params', function () {
return invoke(composer.dynamic(), { type: 'action', name: 'DivideByTwo' }).then(() => assert.fail(), activation => assert.ok(activation.error.response.result.error))
})
})
describe('literals', function () {
it('true', function () {
return invoke(composer.literal(true)).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
})
it('42', function () {
return invoke(composer.literal(42)).then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
})
it('invalid argument', function () {
try {
invoke(composer.literal(invoke))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Invalid argument'))
}
})
it('too many arguments', function () {
try {
invoke(composer.literal('foo', 'foo'))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Too many arguments'))
}
})
})
describe('functions', function () {
it('function must return true', function () {
return invoke(composer.function(({ n }) => n % 2 === 0), { n: 4 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
})
it('function must return false', function () {
return invoke(composer.function(function ({ n }) { return n % 2 === 0 }), { n: 3 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: false }))
})
it('function must fail', function () {
return invoke(composer.function(() => n)).then(() => assert.fail(), activation => assert.ok(activation.error.response.result.error))
})
it('function must throw', function () {
return invoke(composer.function(() => ({ error: 'foo', n: 42 }))).then(() => assert.fail(), activation => assert.deepStrictEqual(activation.error.response.result, { error: 'foo' }))
})
it('function must mutate params', function () {
return invoke(composer.function(params => { params.foo = 'foo' }), { n: 42 }).then(activation => assert.deepStrictEqual(activation.response.result, { foo: 'foo', n: 42 }))
})
it('function as string', function () {
return invoke(composer.function('({ n }) => n % 2 === 0'), { n: 4 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
})
it('function may return a promise', function () {
return invoke(composer.function(({ n }) => Promise.resolve(n % 2 === 0)), { n: 4 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
})
it('invalid argument', function () {
try {
invoke(composer.function(42))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Invalid argument'))
}
})
it('too many arguments', function () {
try {
invoke(composer.function(() => n, () => { }))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Too many arguments'))
}
})
})
describe('deserialize', function () {
it('should deserialize a serialized composition', function () {
const json = {
'type': 'sequence',
'components': [{
'type': 'action',
'name': 'echo'
}, {
'type': 'action',
'name': 'echo'
}]
}
return invoke(composer.parse(json), { message: 'hi' }).then(activation => assert.deepStrictEqual(activation.response.result, { message: 'hi' }))
})
})
describe('tasks', function () {
describe('action tasks', function () {
it('action must return true', function () {
return invoke(composer.task('isNotOne'), { n: 0 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
})
})
describe('function tasks', function () {
it('function must return true', function () {
return invoke(composer.task(({ n }) => n % 2 === 0), { n: 4 }).then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
})
})
describe('null task', function () {
it('null task must return input', function () {
return invoke(composer.task(null), { foo: 'foo' }).then(activation => assert.deepStrictEqual(activation.response.result, { foo: 'foo' }))
})
it('null task must fail on error input', function () {
return invoke(composer.task(null), { error: 'foo' }).then(() => assert.fail(), activation => assert.deepStrictEqual(activation.error.response.result, { error: 'foo' }))
})
})
describe('invalid tasks', function () {
it('a Boolean is not a valid task', function () {
try {
invoke(composer.task(false))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Invalid argument'))
}
})
it('a number is not a valid task', function () {
try {
invoke(composer.task(42))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Invalid argument'))
}
})
it('a dictionary is not a valid task', function () {
try {
invoke(composer.task({ foo: 'foo' }))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Invalid argument'))
}
})
})
it('too many arguments', function () {
try {
invoke(composer.task('foo', 'foo'))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Too many arguments'))
}
})
})
describe('combinators', function () {
describe('sequence', function () {
it('flat', function () {
return invoke(composer.sequence('TripleAndIncrement', 'DivideByTwo', 'DivideByTwo'), { n: 5 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 4 }))
})
it('nested right', function () {
return invoke(composer.sequence('TripleAndIncrement', composer.sequence('DivideByTwo', 'DivideByTwo')), { n: 5 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 4 }))
})
it('nested left', function () {
return invoke(composer.sequence(composer.sequence('TripleAndIncrement', 'DivideByTwo'), 'DivideByTwo'), { n: 5 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 4 }))
})
it('seq', function () {
return invoke(composer.seq('TripleAndIncrement', 'DivideByTwo', 'DivideByTwo'), { n: 5 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 4 }))
})
})
describe('parallel', function () {
const test = redis ? it : it.skip
test('parallel', function () {
return invoke(composer.parallel('TripleAndIncrement', 'DivideByTwo'), { n: 42, $composer: { redis, openwhisk } })
.then(activation => assert.deepStrictEqual(activation.response.result, { value: [{ n: 127 }, { n: 21 }] }))
})
test('par', function () {
return invoke(composer.par('DivideByTwo', 'TripleAndIncrement', 'isEven'), { n: 42, $composer: { redis, openwhisk } })
.then(activation => assert.deepStrictEqual(activation.response.result, { value: [{ n: 21 }, { n: 127 }, { value: true }] }))
})
test('map', function () {
return invoke(composer.map('TripleAndIncrement', 'DivideByTwo'), { value: [{ n: 3 }, { n: 5 }, { n: 7 }], $composer: { redis, openwhisk } })
.then(activation => assert.deepStrictEqual(activation.response.result, { value: [{ n: 5 }, { n: 8 }, { n: 11 }] }))
})
})
describe('if', function () {
it('condition = true', function () {
return invoke(composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement'), { n: 4 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 2 }))
})
it('condition = false', function () {
return invoke(composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement'), { n: 3 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 10 }))
})
it('condition = true, then branch only', function () {
return invoke(composer.if('isEven', 'DivideByTwo'), { n: 4 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 2 }))
})
it('condition = false, then branch only', function () {
return invoke(composer.if('isEven', 'DivideByTwo'), { n: 3 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 3 }))
})
it('condition = true, nosave option', function () {
return invoke(composer.if_nosave('isEven', params => { params.then = true }, params => { params.else = true }), { n: 2 })
.then(activation => assert.deepStrictEqual(activation.response.result, { value: true, then: true }))
})
it('condition = false, nosave option', function () {
return invoke(composer.if_nosave('isEven', params => { params.then = true }, params => { params.else = true }), { n: 3 })
.then(activation => assert.deepStrictEqual(activation.response.result, { value: false, else: true }))
})
it('too many arguments', function () {
try {
invoke(composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement', 'TripleAndIncrement'))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Too many arguments'))
}
})
})
describe('while', function () {
it('a few iterations', function () {
return invoke(composer.while('isNotOne', ({ n }) => ({ n: n - 1 })), { n: 4 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 1 }))
})
it('no iteration', function () {
return invoke(composer.while(() => false, ({ n }) => ({ n: n - 1 })), { n: 1 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 1 }))
})
it('nosave option', function () {
return invoke(composer.while_nosave(({ n }) => ({ n, value: n !== 1 }), ({ n }) => ({ n: n - 1 })), { n: 4 })
.then(activation => assert.deepStrictEqual(activation.response.result, { value: false, n: 1 }))
})
it('too many arguments', function () {
try {
invoke(composer.while('isNotOne', ({ n }) => ({ n: n - 1 }), ({ n }) => ({ n: n - 1 })), { n: 4 })
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Too many arguments'))
}
})
})
describe('dowhile', function () {
it('a few iterations', function () {
return invoke(composer.dowhile(({ n }) => ({ n: n - 1 }), 'isNotOne'), { n: 4 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 1 }))
})
it('one iteration', function () {
return invoke(composer.dowhile(({ n }) => ({ n: n - 1 }), () => false), { n: 1 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 0 }))
})
it('nosave option', function () {
return invoke(composer.dowhile_nosave(({ n }) => ({ n: n - 1 }), ({ n }) => ({ n, value: n !== 1 })), { n: 4 })
.then(activation => assert.deepStrictEqual(activation.response.result, { value: false, n: 1 }))
})
it('too many arguments', function () {
try {
invoke(composer.dowhile(({ n }) => ({ n: n - 1 }), 'isNotOne', ({ n }) => ({ n: n - 1 })), { n: 4 })
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Too many arguments'))
}
})
})
describe('try', function () {
it('no error', function () {
return invoke(composer.try(() => true, error => ({ message: error.error })))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: true }))
})
it('error', function () {
return invoke(composer.try(() => ({ error: 'foo' }), error => ({ message: error.error })))
.then(activation => assert.deepStrictEqual(activation.response.result, { message: 'foo' }))
})
it('try must throw', function () {
return invoke(composer.try(composer.task(null), error => ({ message: error.error })), { error: 'foo' })
.then(activation => assert.deepStrictEqual(activation.response.result, { message: 'foo' }))
})
it('while must throw', function () {
return invoke(composer.try(composer.while(composer.literal(false), null), error => ({ message: error.error })), { error: 'foo' })
.then(activation => assert.deepStrictEqual(activation.response.result, { message: 'foo' }))
})
it('if must throw', function () {
return invoke(composer.try(composer.if(composer.literal(false), null), error => ({ message: error.error })), { error: 'foo' })
.then(activation => assert.deepStrictEqual(activation.response.result, { message: 'foo' }))
})
it('retain', function () {
return invoke(composer.retain(composer.try(() => ({ p: 4 }), null)), { n: 3 })
.then(activation => assert.deepStrictEqual(activation.response.result, { params: { n: 3 }, result: { p: 4 } }))
})
it('too many arguments', function () {
try {
invoke(composer.try('isNotOne', 'isNotOne', 'isNotOne'))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Too many arguments'))
}
})
})
describe('finally', function () {
it('no error', function () {
return invoke(composer.finally(() => true, params => ({ params })))
.then(activation => assert.deepStrictEqual(activation.response.result, { params: { value: true } }))
})
it('error', function () {
return invoke(composer.finally(() => ({ error: 'foo' }), params => ({ params })))
.then(activation => assert.deepStrictEqual(activation.response.result, { params: { error: 'foo' } }))
})
it('too many arguments', function () {
try {
invoke(composer.finally('isNotOne', 'isNotOne', 'isNotOne'))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Too many arguments'))
}
})
})
describe('let', function () {
it('one variable', function () {
return invoke(composer.let({ x: 42 }, () => x))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
})
it('masking', function () {
return invoke(composer.let({ x: 42 }, composer.let({ x: 69 }, () => x)))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: 69 }))
})
it('two variables', function () {
return invoke(composer.let({ x: 42 }, composer.let({ y: 69 }, () => x + y)))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: 111 }))
})
it('two variables combined', function () {
return invoke(composer.let({ x: 42, y: 69 }, () => x + y))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: 111 }))
})
it('scoping', function () {
return invoke(composer.let({ x: 42 }, composer.let({ x: 69 }, () => x), ({ value }) => value + x))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: 111 }))
})
it('invalid argument', function () {
try {
invoke(composer.let(invoke))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Invalid argument'))
}
})
})
describe('mask', function () {
it('let/let/mask', function () {
return invoke(composer.let({ x: 42 }, composer.let({ x: 69 }, composer.mask(() => x))))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
})
it('let/mask/let', function () {
return invoke(composer.let({ x: 42 }, composer.mask(composer.let({ x: 69 }, () => x))))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: 69 }))
})
it('let/let/try/mask', function () {
return invoke(composer.let({ x: 42 }, composer.let({ x: 69 },
composer.try(composer.mask(() => x), () => { }))))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
})
it('let/let/let/mask', function () {
return invoke(composer.let({ x: 42 }, composer.let({ x: 69 },
composer.let({ x: -1 }, composer.mask(() => x)))))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: 69 }))
})
it('let/let/let/mask/mask', function () {
return invoke(composer.let({ x: 42 }, composer.let({ x: 69 },
composer.let({ x: -1 }, composer.mask(composer.mask(() => x))))))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
})
it('let/let/mask/let/mask', function () {
return invoke(composer.let({ x: 42 }, composer.let({ x: 69 },
composer.mask(composer.let({ x: -1 }, composer.mask(() => x))))))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
})
})
describe('retain', function () {
it('base case', function () {
return invoke(composer.retain('TripleAndIncrement'), { n: 3 })
.then(activation => assert.deepStrictEqual(activation.response.result, { params: { n: 3 }, result: { n: 10 } }))
})
it('throw error', function () {
return invoke(composer.retain(() => ({ error: 'foo' })), { n: 3 })
.then(() => assert.fail(), activation => assert.deepStrictEqual(activation.error.response.result, { error: 'foo' }))
})
it('catch error', function () {
return invoke(composer.retain_catch(() => ({ error: 'foo' })), { n: 3 })
.then(activation => assert.deepStrictEqual(activation.response.result, { params: { n: 3 }, result: { error: 'foo' } }))
})
})
describe('merge', function () {
it('base case', function () {
return invoke(composer.merge('TripleAndIncrement'), { n: 3, p: 4 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 10, p: 4 }))
})
})
describe('repeat', function () {
it('a few iterations', function () {
return invoke(composer.repeat(3, 'DivideByTwo'), { n: 8 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 1 }))
})
it('invalid argument', function () {
try {
invoke(composer.repeat('foo'))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Invalid argument'))
}
})
})
describe('retry', function () {
it('success', function () {
return invoke(composer.let({ x: 2 }, composer.retry(2, () => x-- > 0 ? { error: 'foo' } : 42)))
.then(activation => assert.deepStrictEqual(activation.response.result, { value: 42 }))
})
it('failure', function () {
return invoke(composer.let({ x: 2 }, composer.retry(1, () => x-- > 0 ? { error: 'foo' } : 42)))
.then(() => assert.fail(), activation => assert.deepStrictEqual(activation.error.response.result.error, 'foo'))
})
it('invalid argument', function () {
try {
invoke(composer.retry('foo'))
assert.fail()
} catch (error) {
assert.ok(error.message.startsWith('Invalid argument'))
}
})
})
})
})
describe('compositions', function () {
describe('collatz', function () {
it('composition must return { n: 1 }', function () {
return invoke(composer.while('isNotOne', composer.if('isEven', 'DivideByTwo', 'TripleAndIncrement')), { n: 5 })
.then(activation => assert.deepStrictEqual(activation.response.result, { n: 1 }))
})
})
})
})