diff --git a/.gitignore b/.gitignore index 3ede111..b2a5b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ spec/support/templates/fooTemplate/bar/cfg/cfg.txt # logs *.log + +# python +*.pyc diff --git a/packages/ant-cli/spec/bin/ant.spec.js b/packages/ant-cli/spec/bin/ant.spec.js index 4d80261..c1f5d07 100644 --- a/packages/ant-cli/spec/bin/ant.spec.js +++ b/packages/ant-cli/spec/bin/ant.spec.js @@ -773,14 +773,15 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .addFunction = jest.fn(async (name, func, runtime, type, isGlobal) => { + .addFunction = jest.fn(async (name, func, runtime, version, type, isGlobal) => { expect(name).toBe('myfunc'); expect(func).toBe('path/to/myfunc'); expect(runtime).toBe('nodejs'); + expect(version).toBe('6'); expect(type).toBe('lib'); expect(isGlobal).toBe(false); }); - antCli._yargs.parse('function add myfunc path/to/myfunc nodejs'); + antCli._yargs.parse('function add myfunc path/to/myfunc nodejs 6'); }); test('should work with global flag', done => { @@ -797,14 +798,15 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .addFunction = jest.fn(async (name, path, runtime, type, isGlobal) => { + .addFunction = jest.fn(async (name, path, runtime, version, type, isGlobal) => { expect(name).toBe('myfunc'); expect(path).toBe('/path/to/myfunc'); expect(runtime).toBe('nodejs'); + expect(version).toBe('6'); expect(type).toBe('lib'); expect(isGlobal).toBe(true); }); - antCli._yargs.parse('function add myfunc /path/to/myfunc nodejs --global'); + antCli._yargs.parse('function add myfunc /path/to/myfunc nodejs 6 --global'); }); test('should work with template', done => { @@ -821,7 +823,7 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .addFunction = jest.fn(async (name, path, runtime, type, isGlobal, template) => { + .addFunction = jest.fn(async (name, path, runtime, version, type, isGlobal, template) => { expect(name).toBe('myfunc'); expect(template).toBe('/path/to/my/template'); }); @@ -1062,13 +1064,14 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .addRuntime = jest.fn(async (name, bin, extensions, isGlobal) => { + .addRuntime = jest.fn(async (name, version, bin, extensions, isGlobal) => { expect(name).toBe('myruntime'); + expect(version).toBe('10'); expect(bin).toBe(path.resolve(process.cwd(), bin)); - expect(extensions).toEqual(['nodejs', 'python']); + expect(extensions).toEqual(['foo', 'bar']); expect(isGlobal).toBe(false); }); - antCli._yargs.parse('runtime add myruntime path/to/myruntime nodejs python'); + antCli._yargs.parse('runtime add myruntime 10 path/to/myruntime foo bar'); }); test('should work with global flag', done => { @@ -1085,14 +1088,15 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .addRuntime = jest.fn(async (name, bin, extensions, isGlobal) => { + .addRuntime = jest.fn(async (name, version, bin, extensions, isGlobal) => { expect(name).toBe('myruntime'); + expect(version).toBe('10'); expect(bin).toBe('/path/to/myruntime'); console.log(extensions); expect(extensions).toEqual(['nodejs', 'python']); expect(isGlobal).toBe(true); }); - antCli._yargs.parse('runtime add myruntime /path/to/myruntime nodejs python --global'); + antCli._yargs.parse('runtime add myruntime 10 /path/to/myruntime nodejs python --global'); }); test('should handle any errors', done => { @@ -1113,7 +1117,7 @@ ant.js --help plugin add`) .addRuntime = jest.fn(async () => { throw new Error('Mocked error'); }); - antCli._yargs.parse('runtime add myruntime /path/to/myruntime nodejs python'); + antCli._yargs.parse('runtime add myruntime 10 /path/to/myruntime nodejs python'); }); }); @@ -1132,11 +1136,12 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .removeRuntime = jest.fn(async (name, isGlobal) => { + .removeRuntime = jest.fn(async (name, version, isGlobal) => { expect(name).toBe('myruntime'); + expect(version).toBe('5'); expect(isGlobal).toBe(false); }); - antCli._yargs.parse('runtime remove myruntime'); + antCli._yargs.parse('runtime remove myruntime 5'); }); test('should work with global flag', done => { @@ -1153,11 +1158,12 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .removeRuntime = jest.fn(async (name, isGlobal) => { + .removeRuntime = jest.fn(async (name, version, isGlobal) => { expect(name).toBe('myruntime'); + expect(version).toBe('5'); expect(isGlobal).toBe(true); }); - antCli._yargs.parse('runtime remove myruntime --global'); + antCli._yargs.parse('runtime remove myruntime 5 --global'); }); test('should handle any errors', done => { @@ -1178,7 +1184,7 @@ ant.js --help plugin add`) .removeRuntime = jest.fn(async () => { throw new Error('Mocked error'); }); - antCli._yargs.parse('runtime remove myruntime'); + antCli._yargs.parse('runtime remove myruntime 5'); }); }); diff --git a/packages/ant-util-tests/functions/objectLibFunction.js b/packages/ant-util-tests/functions/objectLibFunction.js new file mode 100644 index 0000000..c4a6e9e --- /dev/null +++ b/packages/ant-util-tests/functions/objectLibFunction.js @@ -0,0 +1,15 @@ +/** + * @fileoverview Object lib function for testing purposes. + */ + +const { Observable } = require('rxjs'); + +module.exports = () => { + return Observable.create( + subscriber => { + subscriber.next({ foo: 'bar' }); + subscriber.complete(); + return () => {}; + } + ); +}; diff --git a/packages/ant/lib/config/Config.js b/packages/ant/lib/config/Config.js index c741eb4..d73762f 100644 --- a/packages/ant/lib/config/Config.js +++ b/packages/ant/lib/config/Config.js @@ -283,9 +283,11 @@ template remove command should do nothing`); * Adds a {@link BinFunction} or {@link LibFunction} into this configuration. * * @param {!BinFunction|LibFunction} antFunction The function to be added + * @param {Boolean} specifyRuntimeVersion Flag indicating the runtime version + * should be specified * @returns {Config} This configuration instance. */ - addFunction(antFunction) { + addFunction(antFunction, specifyRuntimeVersion = false) { assert(antFunction, 'Param "antFunction" is required'); assert((antFunction instanceof BinFunction || antFunction instanceof LibFunction), 'Param "antFunction" must be an instance of BinFunction or LibFunction'); @@ -318,7 +320,9 @@ function add command will OVERRIDE the current function`); attributes.items.push(new Pair(new Scalar('bin'), new Scalar(bin))); } else { attributes.items.push(new Pair(new Scalar('handler'), new Scalar(handler))); - attributes.items.push(new Pair(new Scalar('runtime'), new Scalar(runtime.name))); + attributes.items.push(new Pair(new Scalar('runtime'), new Scalar( + specifyRuntimeVersion ? `${runtime.name} ${runtime.version}` : runtime.name + ))); } console.log(`Function "${name}" successfully added on configuration file ${this._path}`); // Document has changed, resets the cached JSON @@ -371,21 +375,21 @@ function remove command should do nothing`); assert(runtime, 'Param "runtime" is required'); assert(runtime instanceof Runtime, 'Param "runtime" must be an instance of Runtime'); - const { name, bin, extensions } = runtime; + const { name, bin, extensions, version } = runtime; // Ensure the "runtimes" root element exists, // and it is a Pair<"runtimes", Map> const runtimes = this._ensureRootCollectionNode('runtimes', Map); - - if (this._filterNodeFromCollectionByKey(runtimes, name)) { - console.log(`Runtime "${name}" already found on the configuration file. \ + const entryName = `${name} ${version}`; + if (this._filterNodeFromCollectionByKey(runtimes, entryName)) { + console.log(`Runtime "${entryName}" already found on the configuration file. \ runtime add command will OVERRIDE the current runtime`); } - logger.log(`Adding runtime ${name} into configuration file ${this._path}`); + logger.log(`Adding runtime ${entryName} into configuration file ${this._path}`); // Creates the brand new Runtime node and adds its to the runtimes Map. runtimes.items.push(new Pair( - new Scalar(name), + new Scalar(entryName), this._createAttributeMap({ bin, extensions }) )); @@ -398,28 +402,35 @@ runtime add command will OVERRIDE the current runtime`); * Removes an {@link Runtime} from this configuration. * * @param {!String} runtime The name of the {@link Runtime} to be removed + * @param {!String} version The version of the {@link Runtime} to be removed * @returns {Config} This configuration instance. */ - removeRuntime(runtime) { + removeRuntime(runtime, version) { assert(runtime, 'Could not remove runtime: param "runtime" is required'); assert( typeof runtime === 'string', 'Could not remove runtime: param "runtime" should be String' ); + assert(version, 'Could not remove runtime: param "version" is required'); + assert( + typeof version === 'string', + 'Could not remove runtime: param "version" should be String' + ); const runtimes = this._findRootCollectionNode('runtimes', Map); if (!runtimes) { console.log('No "runtimes" was found on configuration file. runtime \ remove command should do nothing'); return this; } - if(this._filterNodeFromCollectionByKey(runtimes, runtime)) { - console.log(`Runtime "${runtime}" successfully removed from \ + const entryName = `${runtime} ${version}`; + if(this._filterNodeFromCollectionByKey(runtimes, entryName)) { + console.log(`Runtime "${entryName}" successfully removed from \ configuration file ${this._path}`); // Document has changed, resets the cached JSON this._cachedJson = null; } else { - console.log(`Runtime "${runtime}" was not found on configuration file. \ + console.log(`Runtime "${entryName}" was not found on configuration file. \ runtime remove command should do nothing`); } return this; @@ -668,21 +679,22 @@ provider "${providerName}"` } return Object.keys(functions).map(name => { const func = functions[name]; - const { bin, handler, runtime } = func; + const { bin, handler, runtime, args } = func; try { if (bin) { return new BinFunction(runtimeController.ant, name, bin); } else if (handler) { let runtimeInstance; if (runtime) { - runtimeInstance = runtimeController.getRuntime(runtime); + const [name, version] = runtime.split(' '); + runtimeInstance = runtimeController.getRuntime(name, version); if (!runtimeInstance) { - throw new AntError(`Runtime ${runtime} was not found`); + throw new AntError(`Runtime ${name} ${version} was not found`); } } else { runtimeInstance = runtimeController.defaultRuntime; } - return new LibFunction(runtimeController.ant, name, handler, runtimeInstance); + return new LibFunction(runtimeController.ant, name, handler, runtimeInstance, args); } throw new AntError(`Function type unknown: ${JSON.stringify(func)}`); } catch (e) { @@ -705,14 +717,14 @@ configuration file', e); return []; } return Object.keys(runtimes).map( - name => new Runtime(ant, name, runtimes[name].bin, runtimes[name].extensions) + key => new Runtime(ant, key.split(' ')[0], runtimes[key].bin, runtimes[key].extensions, undefined, key.split(' ')[1]) ); } /** - * Parses the {@link Runtime} from the configuration file "defaultRuntime" string. + * Parses the {@link Runtime} from the configuration file "runtime" string. * - * @param {Object} defaultRuntime The "defaultRuntime" object from the configuration file + * @param {String} defaultRuntime The "runtime" value from the configuration file * @param {!RuntimeController} runtimeController The {@link RuntimeController} * used to find the default {@link Runtime} * @returns {Runtime} The {@link Runtime} instance, given the default runtime name diff --git a/packages/ant/lib/functions/LibFunction.js b/packages/ant/lib/functions/LibFunction.js index ea3d6a1..0b731b2 100644 --- a/packages/ant/lib/functions/LibFunction.js +++ b/packages/ant/lib/functions/LibFunction.js @@ -25,7 +25,7 @@ class LibFunction extends AntFunction { * @throws {AssertionError} If "ant", "name", "handler" or "runtime" params * are not valid. */ - constructor(ant, name, handler, runtime) { + constructor(ant, name, handler, runtime, args) { super(ant, name); assert( @@ -51,6 +51,13 @@ class LibFunction extends AntFunction { * @private */ this._runtime = runtime; + + /** + * Contains the fixed arguments that will be used when running the function. + * @type {Array} + * @private + */ + this._args = args || []; } /** @@ -71,6 +78,15 @@ class LibFunction extends AntFunction { return this._runtime; } + /** + * Contains the function fixed execution argumnets. + * @type {Array} + * @readonly + */ + get args() { + return this._args; + } + /** * Runs the function. It can receive different arguments depending on the * function instance. @@ -80,16 +96,22 @@ class LibFunction extends AntFunction { run() { logger.log(`Running lib function ${this.name}...`); + const args = JSON.stringify( + this._args.concat(Array.from(arguments)) + ); try { return this._runtime.run([ this._handler, - JSON.stringify(Array.from(arguments)) + args ]).pipe(map(data => { // JSON fails to parse 'undefined', but not '"undefined"' try { return JSON.parse(data); } catch (e) { - return undefined; + if (typeof data === 'string' && data.trim() === 'undefined') { + return undefined; + } + return data; } })); } catch (e) { diff --git a/packages/ant/lib/functions/runtimes/Runtime.js b/packages/ant/lib/functions/runtimes/Runtime.js index 320f82f..b096330 100644 --- a/packages/ant/lib/functions/runtimes/Runtime.js +++ b/packages/ant/lib/functions/runtimes/Runtime.js @@ -4,6 +4,7 @@ const assert = require('assert'); const BinFunction = require('../BinFunction'); +const semver = require('semver'); /** * @class ant/Runtime @@ -21,16 +22,22 @@ class Runtime extends BinFunction { * runtime supports to execute. * @param {String} template The path to the file to be used as template * when creating new functions with this runtime + * @param {!String} version The runtime version supported + * @param {Boolean} isDefault Flag indicating it should be set as default * @throws {AssertionError} If "ant", "name", "bin" or "extensions" params are * not valid. */ - constructor(ant, name, bin, extensions, template) { + constructor(ant, name, bin, extensions, template, version, isDefault) { super(ant, name, bin); assert( !extensions || extensions instanceof Array, 'Could not initialize Runtime: param "extensions" should be Array' ); + assert( + typeof version === 'string' && version.trim() !== '', + 'Could not initialize Runtime: param "version" should be non empty String' + ); /** * Contains the extensions that this runtime supports to execute. @@ -46,6 +53,20 @@ class Runtime extends BinFunction { * @private */ this._template = template; + + /** + * The runtime version supported. + * @type {String} + * @private + */ + this._version = semver.major(semver.coerce(version)).toString(); + + /** + * Flag indicating it should be set as default + * @type {Boolean} + * @private + */ + this._isDefault = isDefault; } /** @@ -65,6 +86,22 @@ class Runtime extends BinFunction { get template() { return this._template; } + + /** + * Returns the runtime version supported. + * @type {String} + */ + get version() { + return this._version; + } + + /** + * Returns the flag indicating it should be set as default. + * @type {Boolean} + */ + get isDefault() { + return this._isDefault; + } } module.exports = Runtime; diff --git a/packages/ant/lib/functions/runtimes/RuntimeController.js b/packages/ant/lib/functions/runtimes/RuntimeController.js index ff084bc..ac8291c 100644 --- a/packages/ant/lib/functions/runtimes/RuntimeController.js +++ b/packages/ant/lib/functions/runtimes/RuntimeController.js @@ -4,6 +4,7 @@ const assert = require('assert'); const Runtime = require('./Runtime'); +const semver = require('semver'); /** * @class ant/RuntimeController @@ -87,7 +88,6 @@ class RuntimeController { runtimes instanceof Array, 'Could not load runtimes: param "runtimes" should be Array' ); - for (const runtime of runtimes) { assert( runtime instanceof Runtime, @@ -100,29 +100,50 @@ be an instance of Runtime` `Could not load runtime ${runtime.name}: the framework used to \ initialize the runtime is different to this controller's` ); - this._runtimes.set( - runtime.name, - runtime - ); + let runtimeByVersion = this._runtimes.get(runtime.name); + if (!runtimeByVersion) { + runtimeByVersion = new Map(); + // If this is the first runtime by its name, + // set it as the default + runtimeByVersion.set('default', runtime); + this._runtimes.set(runtime.name, runtimeByVersion); + } else if (runtime.isDefault){ + // If isDefault is true, overrides the actual default runtime + runtimeByVersion.set('default', runtime); + } + runtimeByVersion.set(runtime.version, runtime); } } /** * Contains the loaded runtimes. - * @type {Runtime[]} + * @type {Map>} * @readonly */ get runtimes() { - return Array.from(this._runtimes.values()); + return this._runtimes; } /** * Gets a specific runtime by its name. * @param {String} name The name of the runtime to be gotten. + * @param {String} version The target runtime version. * @return {Runtime} The runtime object. */ - getRuntime(name) { - return this._runtimes.get(name) || null; + getRuntime(name, version) { + assert(typeof name === 'string', 'Could not get runtime. "name" should be String'); + const runtimeByVersion = this._runtimes.get(name); + if (!runtimeByVersion || runtimeByVersion.length === 0) { + return null; + } + // If version was not provided, returns the default + // runtime by its name + if (!version) { + return runtimeByVersion.get('default'); + } + assert(typeof version === 'string' && version.length, 'Could not get runtime. "version" \ +should be non-empty String'); + return runtimeByVersion.get(semver.major(semver.coerce(version)).toString()) || null; } /** diff --git a/packages/ant/package.json b/packages/ant/package.json index 51ce64c..7202030 100644 --- a/packages/ant/package.json +++ b/packages/ant/package.json @@ -55,6 +55,7 @@ "@back4app/ant-serverless": "^0.0.17", "@back4app/ant-util": "^0.0.17", "@back4app/ant-util-rxjs": "^0.0.17", + "semver": "^5.5.1", "fs-extra": "^7.0.0", "mustache": "^3.0.0", "rxjs": "^6.3.2", diff --git a/packages/ant/spec/lib/Ant.spec.js b/packages/ant/spec/lib/Ant.spec.js index edbfc8e..7dab62e 100644 --- a/packages/ant/spec/lib/Ant.spec.js +++ b/packages/ant/spec/lib/Ant.spec.js @@ -191,60 +191,63 @@ Template category value is not an object!' test('should load runtimes from config', () => { const runtimes = { - Bin: { - bin: '/path/to/bin', + 'Foo 0': { + bin: '/path/to/foo', extensions: ['js', 'py'] }, - Lib: { - bin: '/path/to/lib', + 'Bar 1': { + bin: '/path/to/bar', extensions: ['cpp', 'c', 'h'] } }; const ant = new Ant({ runtimes }); - const binRuntime = ant.runtimeController.getRuntime('Bin'); + const binRuntime = ant.runtimeController.getRuntime('Foo'); expect(binRuntime).toBeInstanceOf(Runtime); - expect(binRuntime.name).toBe('Bin'); - expect(binRuntime.bin).toBe(runtimes.Bin.bin); - expect(binRuntime.extensions).toBe(runtimes.Bin.extensions); - - const libRuntime = ant.runtimeController.getRuntime('Lib'); - expect(libRuntime.name).toBe('Lib'); - expect(libRuntime.bin).toBe(runtimes.Lib.bin); - expect(libRuntime.extensions).toBe(runtimes.Lib.extensions); + expect(binRuntime.name).toBe('Foo'); + expect(binRuntime.bin).toBe(runtimes['Foo 0'].bin); + expect(binRuntime.extensions).toBe(runtimes['Foo 0'].extensions); + expect(binRuntime.version).toBe('0'); + + const libRuntime = ant.runtimeController.getRuntime('Bar', '1'); + expect(libRuntime.name).toBe('Bar'); + expect(libRuntime.bin).toBe(runtimes['Bar 1'].bin); + expect(libRuntime.extensions).toBe(runtimes['Bar 1'].extensions); + expect(libRuntime.version).toBe('1'); }); test('should load default runtime from config', () => { const runtimes = { - Bin: { - bin: '/path/to/bin', + 'Foo 0': { + bin: '/path/to/foo', extensions: ['js', 'py'] }, - Lib: { - bin: '/path/to/lib', + 'Bar 1': { + bin: '/path/to/bar', extensions: ['cpp', 'c', 'h'] } }; - const runtime = 'Bin'; + const runtime = 'Foo'; const ant = new Ant({ runtimes, runtime }); const fooRuntime = ant.runtimeController.defaultRuntime; expect(fooRuntime).toBeInstanceOf(Runtime); - expect(fooRuntime.name).toBe('Bin'); - expect(fooRuntime.bin).toBe(runtimes.Bin.bin); - expect(fooRuntime.extensions).toBe(runtimes.Bin.extensions); + expect(fooRuntime.name).toBe('Foo'); + expect(fooRuntime.bin).toBe(runtimes['Foo 0'].bin); + expect(fooRuntime.extensions).toBe(runtimes['Foo 0'].extensions); + expect(fooRuntime.version).toBe('0'); }); test('should load default runtime from global config', () => { const runtimes = { - Bin: { - bin: '/path/to/bin', + 'Foo 0': { + bin: '/path/to/foo', extensions: ['js', 'py'] }, - Lib: { - bin: '/path/to/lib', + 'Bar 1': { + bin: '/path/to/bar', extensions: ['cpp', 'c', 'h'] } }; - const defaultRuntimeName = 'Bin'; + const defaultRuntimeName = 'Foo'; jest.spyOn(Ant.prototype, '_getGlobalConfig').mockImplementation(() => { return { runtimes, @@ -254,9 +257,10 @@ Template category value is not an object!' try { const ant = new Ant(); const defaultRuntime = ant.runtimeController.defaultRuntime; - expect(defaultRuntime.name).toBe('Bin'); - expect(defaultRuntime.bin).toBe('/path/to/bin'); - expect(defaultRuntime.extensions).toEqual([ 'js', 'py' ]); + expect(defaultRuntime.name).toBe(defaultRuntimeName); + expect(defaultRuntime.bin).toBe(runtimes['Foo 0'].bin); + expect(defaultRuntime.extensions).toEqual(runtimes['Foo 0'].extensions); + expect(defaultRuntime.version).toBe('0'); } finally { jest.restoreAllMocks(); } diff --git a/packages/ant/spec/lib/config/Config.spec.js b/packages/ant/spec/lib/config/Config.spec.js index 0e210a9..f6aa05a 100644 --- a/packages/ant/spec/lib/config/Config.spec.js +++ b/packages/ant/spec/lib/config/Config.spec.js @@ -684,7 +684,7 @@ configuration. template remove command should do nothing'); config.addFunction( new LibFunction(ant, 'LibFunc', '/myhandler', - new Runtime(ant, 'MyRuntime', '/my/runtime') + new Runtime(ant, 'MyRuntime', '/my/runtime', [], undefined, '1') ) ); functions = config.config.functions; @@ -697,6 +697,26 @@ configuration. template remove command should do nothing'); runtime: 'MyRuntime' } }); + + config.addFunction( + new LibFunction(ant, 'LibFuncVersionDefined', '/myhandler', + new Runtime(ant, 'MyRuntime', '/my/runtime', [], undefined, '1') + ), true + ); + functions = config.config.functions; + expect(functions).toEqual({ + BinFunc: { + bin: '/my/bin' + }, + LibFunc: { + handler: '/myhandler', + runtime: 'MyRuntime' + }, + LibFuncVersionDefined: { + handler: '/myhandler', + runtime: 'MyRuntime 1' + } + }); }); test('should override if function already exists', () => { @@ -737,7 +757,7 @@ found on the configuration file. function add command will OVERRIDE the current const config = new Config({}); config.addFunction(new BinFunction(ant, 'BinFunc', '/my/bin')); config.addFunction(new LibFunction(ant, 'LibFunc', '/myhandler', - new Runtime(ant, 'MyRuntime', 'my/runtime') + new Runtime(ant, 'MyRuntime', 'my/runtime', [], undefined, '1') )); config.removeFunction('BinFunc'); const { functions } = config.config; @@ -774,10 +794,10 @@ found on the configuration file. function add command will OVERRIDE the current test('should add a runtime', () => { const ant = new Ant(); const config = new Config({}); - const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ]); + const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ], undefined, '1'); expect(config.addRuntime(runtime)).toBe(config); expect(config.config.runtimes).toEqual({ - runtime: { + 'runtime 1': { bin: '/my/bin', extensions: ['js'] } @@ -789,13 +809,13 @@ found on the configuration file. function add command will OVERRIDE the current console.log = jest.fn(); const ant = new Ant(); const config = new Config({}); - const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ]); + const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ], undefined, '1'); config.addRuntime(runtime); - config.addRuntime(new Runtime(ant, 'runtime', '/alternative/bin', [ 'py' ])); - expect(console.log).toHaveBeenCalledWith('Runtime "runtime" already \ + config.addRuntime(new Runtime(ant, 'runtime', '/alternative/bin', [ 'py' ], undefined, '1')); + expect(console.log).toHaveBeenCalledWith('Runtime "runtime 1" already \ found on the configuration file. runtime add command will OVERRIDE the current runtime'); expect(config.config.runtimes).toEqual({ - runtime: { + 'runtime 1': { bin: '/alternative/bin', extensions: [ 'py' ] } @@ -807,16 +827,16 @@ found on the configuration file. runtime add command will OVERRIDE the current r test('should remove a runtime', () => { const ant = new Ant(); const config = new Config({}); - const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ]); + const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ], undefined, '1'); config.addRuntime(runtime); - config.removeRuntime('runtime'); + config.removeRuntime('runtime', '1'); expect(config.config.runtimes).toEqual({}); }); test('should do nothing because "runtimes" does not exists', () => { console.log = jest.fn(); const config = new Config({}); - config.removeRuntime('runtime'); + config.removeRuntime('runtime', '1'); expect(console.log).toHaveBeenCalledWith('No "runtimes" was found \ on configuration file. runtime remove command should do nothing'); }); @@ -825,10 +845,10 @@ on configuration file. runtime remove command should do nothing'); console.log = jest.fn(); const ant = new Ant(); const config = new Config({}); - const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ]); + const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ], undefined, '1'); config.addRuntime(runtime); - config.removeRuntime('foo'); - expect(console.log).toHaveBeenCalledWith('Runtime "foo" was not \ + config.removeRuntime('foo', '1'); + expect(console.log).toHaveBeenCalledWith('Runtime "foo 1" was not \ found on configuration file. runtime remove command should do nothing'); }); }); @@ -962,12 +982,13 @@ Template category value is not an object!' }, Foo: { handler: '/foo/bar', - runtime: 'python' + runtime: 'python', + args: ['--foo', '--bar'] } }; const runtimeController = { getRuntime: jest.fn().mockImplementation( - runtime => new Runtime(ant, runtime, 'foo') + runtime => new Runtime(ant, runtime, 'foo', [], undefined, '1') ), ant: new Ant() }; @@ -984,6 +1005,7 @@ Template category value is not an object!' expect(func).toBeInstanceOf(LibFunction); expect(func.name).toBe('Foo'); expect(func.handler).toBe(functions.Foo.handler); + expect(func.args).toBe(functions.Foo.args); expect(runtimeController.getRuntime.mock.calls.length).toBe(2); expect(runtimeController.getRuntime.mock.calls[0][0]).toBe(functions.MyLib.runtime); @@ -1028,11 +1050,11 @@ Template category value is not an object!' describe('ParseConfigRuntimes', () => { test('should parse runtimes from config', () => { const runtimes = { - Runtime: { + 'Runtime 1': { bin: '/my/runtime', extensions: ['py'] }, - Node: { + 'Node 2': { bin: '/node', extensions: ['js'] }, @@ -1043,20 +1065,24 @@ Template category value is not an object!' expect(results[0]).toBeInstanceOf(Runtime); expect(results[0].name).toBe('Runtime'); - expect(results[0].bin).toBe(runtimes.Runtime.bin); - expect(results[0].extensions).toBe(runtimes.Runtime.extensions); + expect(results[0].version).toBe('1'); + expect(results[0].bin).toBe(runtimes['Runtime 1'].bin); + expect(results[0].extensions).toBe(runtimes['Runtime 1'].extensions); expect(results[1]).toBeInstanceOf(Runtime); expect(results[1].name).toBe('Node'); - expect(results[1].bin).toBe(runtimes.Node.bin); - expect(results[1].extensions).toBe(runtimes.Node.extensions); + expect(results[1].version).toBe('2'); + expect(results[1].bin).toBe(runtimes['Node 2'].bin); + expect(results[1].extensions).toBe(runtimes['Node 2'].extensions); }); }); describe('ParseConfigDefaultRuntime', () => { test('should parse default runtime from config', () => { const myDefaultRuntime = 'myDefaultRuntime'; - const runtimeStub = new Runtime(new Ant(), 'runtimeStub', '/my/runtime/stub', ['foo', 'bar']); + const runtimeStub = new Runtime( + new Ant(), 'runtimeStub', '/my/runtime/stub', ['foo', 'bar'], undefined, '1' + ); const getRuntimeMock = jest.fn(name => { expect(name).toBe(myDefaultRuntime); return runtimeStub; diff --git a/packages/ant/spec/lib/functions/FunctionController.spec.js b/packages/ant/spec/lib/functions/FunctionController.spec.js index 11239c9..664359f 100644 --- a/packages/ant/spec/lib/functions/FunctionController.spec.js +++ b/packages/ant/spec/lib/functions/FunctionController.spec.js @@ -124,7 +124,7 @@ describe('lib/functions/FunctionController.js', () => { const function1 = new AntFunction(antWithFunctions, 'ant'); const function2 = new BinFunction(antWithFunctions, 'bin', '/path/to/bin'); const function3 = new LibFunction(antWithFunctions, 'lib', '/path/to/lib', - new Runtime(antWithFunctions, 'runtime', '/path/to/runtime') + new Runtime(antWithFunctions, 'runtime', '/path/to/runtime', [], undefined, '7') ); antWithFunctions.functionController.loadFunctions([function1, function2, function3]); diff --git a/packages/ant/spec/lib/functions/LibFunction.spec.js b/packages/ant/spec/lib/functions/LibFunction.spec.js index 4fc786a..4d46e01 100644 --- a/packages/ant/spec/lib/functions/LibFunction.spec.js +++ b/packages/ant/spec/lib/functions/LibFunction.spec.js @@ -3,7 +3,7 @@ */ const path = require('path'); -const { Observable } = require('rxjs'); +const { Observable, of } = require('rxjs'); const { toArray } = require('rxjs/operators'); const Ant = require('../../../lib/Ant'); const LibFunction = require('../../../lib/functions/LibFunction'); @@ -19,7 +19,10 @@ const ant = new Ant(); const fooRuntime = new Runtime( ant, 'fooRuntime', - path.resolve(utilPath, 'functions/fooRuntime.js') + path.resolve(utilPath, 'functions/fooRuntime.js'), + [], + undefined, + '1' ); const libFunction = new LibFunction( @@ -78,7 +81,10 @@ describe('lib/functions/LibFunction.js', () => { const runtime = new Runtime( ant, 'fooBinFunction', - 'it/will/fail' + 'it/will/fail', + [], + undefined, + '1' ); runtime.run = () => { throw new Error('Some error'); }; expect(() => { @@ -90,5 +96,26 @@ describe('lib/functions/LibFunction.js', () => { )).run(); }).toThrowError('Could not run lib function fooLibFunction'); }); + + test('should run and handle multiple types result', async () => { + // Mocks the runtime.run function + const mockRuntime = new Runtime(ant, 'mockRuntime', 'bin', [], undefined, '1'); + const values = [ + 1, + 'a', + true, + { foo: { + bar: false + }}, + ['lorem', 'ipsum'], + undefined, + null + ]; + mockRuntime.run = jest.fn(() => of(...values)); + const typeslibAntFunction = new LibFunction(ant, 'foo', 'bar', mockRuntime); + const runReturn = typeslibAntFunction.run(); + expect(await runReturn.pipe(toArray()).toPromise()) + .toEqual(values); + }); }); }); diff --git a/packages/ant/spec/lib/functions/runtimes/RuntimeController.spec.js b/packages/ant/spec/lib/functions/runtimes/RuntimeController.spec.js index 1890a82..10c1e00 100644 --- a/packages/ant/spec/lib/functions/runtimes/RuntimeController.spec.js +++ b/packages/ant/spec/lib/functions/runtimes/RuntimeController.spec.js @@ -19,10 +19,9 @@ describe('lib/functions/runtimes/RuntimeController.js', () => { test('should load plugins\' runtimes', () => { const antWithRuntimes = new Ant(); - - const runtime1 = new Runtime(antWithRuntimes, 'runtime1', '/foo/bin'); - const runtime2 = new Runtime(antWithRuntimes, 'runtime2', '/foo/bin'); - const runtime2v2 = new Runtime(antWithRuntimes, 'runtime2', '/foo/bin'); + const runtime1 = new Runtime(antWithRuntimes, 'runtime1', '/foo/bin', [], undefined, '1'); + const runtime2 = new Runtime(antWithRuntimes, 'runtime2', '/foo/bin', [], undefined, '2'); + const runtime2v2 = new Runtime(antWithRuntimes, 'runtime2', '/foo/bin', [], undefined, '3'); /** * Represents a {@link Plugin} with runtimes for testing purposes. @@ -36,17 +35,12 @@ describe('lib/functions/runtimes/RuntimeController.js', () => { } antWithRuntimes.pluginController.loadPlugins([PluginWithRuntimes]); - expect(antWithRuntimes.runtimeController.runtimes) - .toEqual(expect.any(Array)); - expect( - antWithRuntimes.runtimeController.runtimes[0].name - ).toEqual('Node'); - expect( - antWithRuntimes.runtimeController.runtimes[1] - ).toEqual(runtime1); - expect( - antWithRuntimes.runtimeController.runtimes[2] - ).toEqual(runtime2v2); + const { runtimes } = antWithRuntimes.runtimeController; + expect(runtimes).toEqual(expect.any(Map)); + expect(runtimes.get('Node').get('default')).toBeDefined(); + expect(runtimes.get('runtime1').get('default')).toEqual(runtime1); + expect(runtimes.get('runtime2').get('default')).toEqual(runtime2); + expect(runtimes.get('runtime2').get('3')).toEqual(runtime2v2); }); test('should fail if "ant" param is not passed', () => { @@ -83,7 +77,8 @@ should be Ant' 'myCustomRuntime', '/foo/bin', ['extension'], - '/foo/template' + '/foo/template', + '1' ); const runtimes = [myCustomRuntime]; const runtimeController = new RuntimeController(ant, runtimes); @@ -92,6 +87,34 @@ should be Ant' expect(loadedRuntime.template).toEqual('/foo/template'); }); + test('should load runtimes and set a new default', () => { + const myCustomRuntime = new Runtime( + ant, + 'myCustomRuntime', + '/foo/bin', + ['extension'], + '/foo/template', + '1' + ); + const myNewDefault = new Runtime( + ant, + 'myCustomRuntime', + '/bar/bin', + ['newextension'], + '/bar/template', + '1', + true + ); + const runtimes = [myCustomRuntime]; + const runtimeController = new RuntimeController(ant, runtimes); + let loadedRuntime = runtimeController.getRuntime(myCustomRuntime.name); + expect(loadedRuntime).toEqual(myCustomRuntime); + + runtimeController.loadRuntimes([myNewDefault]); + loadedRuntime = runtimeController.getRuntime(myCustomRuntime.name); + expect(loadedRuntime).toEqual(myNewDefault); + }); + describe('RuntimeController.ant', () => { test('should be readonly', () => { expect(runtimeController.ant).toEqual(ant); @@ -101,9 +124,90 @@ should be Ant' }); describe('RuntimeController.getRuntime', () => { - test('should return null if runtime not found', () => { - expect(runtimeController.getRuntime('NotExistent')) + test('should return null if runtime list is empty', () => { + const runtimeController = new RuntimeController(ant); + expect(runtimeController.getRuntime('any runtime')) .toEqual(null); }); + + test('should return null if runtime was not found', () => { + const runtimes = [ + new Runtime( + ant, 'myCustomRuntime', '/foo/bin', ['extension'], '/foo/template', '1.0.0' + ), + new Runtime( + ant, 'myCustomRuntime', '/foo/bin2', ['extension'], null, '0.0.1' + ), + new Runtime( + ant, 'myCustomRuntime', '/foo/bin3', ['extension'], null, '2.0.1' + ) + ]; + const runtimeController = new RuntimeController(ant, runtimes); + expect(runtimeController.getRuntime('foo')).toBeNull(); + }); + + test('should fail due to invalid version param', () => { + try { + runtimeController.getRuntime('name', 1.2); + } catch (err) { + expect(err.message).toBe('Could not get runtime. "version" \ +should be non-empty String'); + } + }); + + test('should return the default runtime', () => { + const myCustomRuntime = new Runtime( + ant, + 'myCustomRuntime', + '/foo/bin', + ['extension'], + '/foo/template', + '1.0.0' + ); + const runtimes = [ + myCustomRuntime, + new Runtime( + ant, 'myCustomRuntime', '/foo/bin2', ['extension'], null, '0.0.1' + ), + new Runtime( + ant, 'myCustomRuntime', '/foo/bin3', ['extension'], null, '2.0.1' + ) + ]; + const runtimeController = new RuntimeController(ant, runtimes); + const loadedRuntime = runtimeController.getRuntime(myCustomRuntime.name); + expect(loadedRuntime).toEqual(myCustomRuntime); + }); + + test('should return given a version', () => { + const myCustomRuntime = new Runtime( + ant, + 'myCustomRuntime', + '/foo/bin', + ['extension'], + '/foo/template', + '1.0' + ); + const runtimes = [ myCustomRuntime ]; + const runtimeController = new RuntimeController(ant, runtimes); + const loadedRuntime = runtimeController.getRuntime(myCustomRuntime.name, '1'); + expect(loadedRuntime).toEqual(myCustomRuntime); + }); + + test('should return null due to version out of range', () => { + const runtimes = [ + new Runtime( + ant, 'myCustomRuntime', '/foo/bin1', ['extension'], null, '1.1' + ), + new Runtime( + ant, 'myCustomRuntime', '/foo/bin2', ['extension'], null, '2.1' + ), + new Runtime( + ant, 'myCustomRuntime', '/foo/bin3', ['extension'], null, '3.0.2' + ) + ]; + const runtimeController = new RuntimeController(ant, runtimes); + const loadedRuntime = runtimeController.getRuntime('myCustomRuntime', '4'); + expect(loadedRuntime).toBeNull(); + }); }); }); diff --git a/packages/ant/spec/lib/plugins/PluginController.spec.js b/packages/ant/spec/lib/plugins/PluginController.spec.js index f3ab3a8..c4d76ca 100644 --- a/packages/ant/spec/lib/plugins/PluginController.spec.js +++ b/packages/ant/spec/lib/plugins/PluginController.spec.js @@ -159,8 +159,8 @@ class PluginWithNotValidFunction extends Plugin { } } -const runtime1 = new Runtime(ant, 'runtime1', '/foo/path'); -const runtime2 = new Runtime(ant, 'runtime2', '/foo/path'); +const runtime1 = new Runtime(ant, 'runtime1', '/foo/path', [], undefined, '1'); +const runtime2 = new Runtime(ant, 'runtime2', '/foo/path', [], undefined, '2'); /** * Represents a {@link Plugin} with runtimes for testing purposes. diff --git a/plugins/ant-core/functions/javaRuntime.js b/plugins/ant-core/functions/javaRuntime.js new file mode 100755 index 0000000..f4a05dc --- /dev/null +++ b/plugins/ant-core/functions/javaRuntime.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +/** + * @fileoverview Java runtime for Ant Framework. + */ +const { spawn, spawnSync } = require('child_process'); +const { parse } = require('path'); +const { argv, stdout, stderr } = process; + +/** + * Flushes the stdout and stderr and exits the process. + * + * @param {Number} code The status code + */ +const exit = code => { + // On "close", waits stdout and stderr to flush + // and then exits. If it is already done, + // the cb is called anyway + stdout.end(() => { + stderr.end(() => { + process.exit(code); + }); + }); +}; + +let javaFile = argv[2]; +const { dirname, name: fileName, ext } = parse(javaFile); + +let args = []; +const options = {}; +if (ext === '.jar') { + args.push('-cp'); +} else if (ext === '.java') { + const { error, status, stderr: javacStderr, stdout: javacStdout } = spawnSync('javac', [javaFile]); + if (error) { + stdout.write(javacStdout); + stderr.write(javacStderr); + exit(status); + } + options.cwd = dirname; + javaFile = fileName; +} +args.push(javaFile); + +// Filters null or undefined arguments and non serializable items +args = args.concat(JSON.parse(argv[3])) + .filter(arg => { + if (arg === undefined || arg === null) { + return false; + } + try { + JSON.stringify(arg); + return true; + } catch (err) { + return false; + } + }) + .map(arg => typeof arg === 'string' ? arg : JSON.stringify(arg)); + +const javaProgram = spawn('java', args, options); +javaProgram.stdout.on('data', data => stdout.write(data.toString())); +javaProgram.stderr.on('data', data => stderr.write(data.toString())); +javaProgram.on('close', exit); diff --git a/plugins/ant-core/functions/python3Runtime.py b/plugins/ant-core/functions/python3Runtime.py new file mode 100755 index 0000000..f3256d9 --- /dev/null +++ b/plugins/ant-core/functions/python3Runtime.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +import sys +import json +from importlib.machinery import SourceFileLoader +import traceback + +if __name__ == "__main__": + executable = sys.argv[1] + args = json.loads(sys.argv[2]) + + # Loads the user function and runs it, providing the + # arguments received from Ant + user_module = SourceFileLoader('antframework', executable).load_module() + + # By default, we invoke the "main" function + try: + user_module.main(args) + except Exception as error: + sys.stdout.write(traceback.format_exc()) diff --git a/plugins/ant-core/functions/pythonRuntime.py b/plugins/ant-core/functions/pythonRuntime.py new file mode 100755 index 0000000..2f3c614 --- /dev/null +++ b/plugins/ant-core/functions/pythonRuntime.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import sys +import json +import imp +import traceback + +if __name__ == "__main__": + executable = sys.argv[1] + args = json.loads(sys.argv[2], "utf-8") + + # Loads the user function and runs it, providing the + # arguments received from Ant + user_module = imp.load_source('antframework', executable) + + # By default, we invoke the "main" function + try: + user_module.main(args) + except Exception as error: + sys.stdout.write(traceback.format_exc()) diff --git a/plugins/ant-core/lib/Core.js b/plugins/ant-core/lib/Core.js index c02d76d..714a993 100644 --- a/plugins/ant-core/lib/Core.js +++ b/plugins/ant-core/lib/Core.js @@ -51,7 +51,32 @@ class Core extends Plugin { 'Node', path.resolve(__dirname, '../functions/nodeRuntime.js'), ['js'], - path.resolve(__dirname, '../templates/function/node.js.mustache') + path.resolve(__dirname, '../templates/function/node.js.mustache'), + '10' + ), + new Runtime( + this._ant, + 'Java', + path.resolve(__dirname, '../functions/javaRuntime.js'), + ['jar', 'java'], + path.resolve(__dirname, '../templates/function/java.java.mustache'), + '8' + ), + new Runtime( + this._ant, + 'Python', + path.resolve(__dirname, '../functions/pythonRuntime.py'), + ['py'], + path.resolve(__dirname, '../templates/function/python.py.mustache'), + '2' + ), + new Runtime( + this._ant, + 'Python', + path.resolve(__dirname, '../functions/python3Runtime.py'), + ['py'], + path.resolve(__dirname, '../templates/function/python3.py.mustache'), + '3' ) ]; } @@ -230,37 +255,43 @@ using template "${argv.template}"` 'function ', 'Manage functions of Ant framework', yargs => { yargs.command( - 'add [function] [runtime]', + 'add [function] [runtime] [runtimeVersion]', 'Adds/overrides a function', yargs => { yargs.positional('name', { describe: 'The name of the function', - string: true + type: 'string' }).positional('function', { describe: 'The path to the function', - string: true + type: 'string' }).positional('runtime', { describe: 'The runtime to run the function', - string: true, + type: 'string', + }).positional('runtimeVersion', { + describe: 'The runtime version to run the function', + type: 'string', + require: false }).option('global', { alias: 'g', describe: 'Adds the function into global configuration file', - boolean: true, + type: 'boolean', nargs: 0, default: false }).option('type', { alias: 'f', describe: 'Specifies which type of function will be added', choices: ['lib', 'bin'], - default: 'lib' + default: 'lib', + type: 'string' }).option('template', { alias: 't', describe: 'The template to render the function in case no source \ -file is found at the given path' +file is found at the given path', + type: 'string' }); }, - async ({ name, function: func, runtime, type, configPath, global, template }) => { + async ({ name, function: func, runtime, runtimeVersion, type, configPath, global, template }) => { try { - await this.addFunction(name, func, runtime, type, configPath || global, template); + await this.addFunction(name, func, runtime, runtimeVersion, type, configPath || global, template); process.exit(0); } catch (e) { yargsHelper.handleErrorMessage(e.message, e, 'function add'); @@ -341,14 +372,17 @@ file is found at the given path' 'runtime ', 'Manage runtimes of Ant framework', yargs => { yargs.command( - 'add [extensions..]', + 'add [extensions..]', 'Adds new runtime', yargs => { yargs.positional('name', { describe: 'The name of the runtime', - string: true + type: 'string' + }).positional('runtimeVersion', { + describe: 'The version of the runtime', + type: 'string' }).positional('bin', { describe: 'The path to the runtime', - string: true + type: 'string' }).positional('extensions', { describe: 'The extensions supported by the runtime', array: true @@ -360,37 +394,40 @@ file is found at the given path' default: false }); }, - async ({ name, bin, extensions, configPath, global }) => { + async ({ name, runtimeVersion, bin, extensions, configPath, global }) => { try { // If bin is relative, we must resolve it with our current working // directory before saving it into the configuration file if (bin && typeof bin === 'string' && !bin.startsWith('/')) { bin = path.resolve(process.cwd(), bin); } - await this.addRuntime(name, bin, extensions, configPath || global); + await this.addRuntime(name, runtimeVersion, bin, extensions, configPath || global); process.exit(0); } catch (e) { yargsHelper.handleErrorMessage(e.message, e, 'runtime add'); } } ).command( - 'remove [--global]', + 'remove [--global]', 'Removes a runtime', yargs => { yargs.positional('name', { describe: 'The name of the runtime to be removed', - string: true + type: 'string' + }).positional('runtimeVersion', { + describe: 'The version of the runtime to be removed', + type: 'string' }).option('global', { alias: 'g', describe: 'Removes runtime from global configuration file', - boolean: true, + type: 'boolean', nargs: 0, default: false }); }, async (argv) => { try { - const { name, configPath, global } = argv; - await this.removeRuntime(name, configPath || global); + const { name, runtimeVersion, configPath, global } = argv; + await this.removeRuntime(name, runtimeVersion, configPath || global); process.exit(0); } catch (e) { yargsHelper.handleErrorMessage(e.message, e, 'runtime remove'); @@ -534,14 +571,14 @@ file is found at the given path' case 'add': command = 'runtime add'; if (msg.includes('Not enough non-option arguments')) { - msg = 'Runtime add command requires name and bin arguments'; + msg = 'Runtime add command requires name, runtimeVersion and bin arguments'; createError = true; } break; case 'remove': command = 'runtime remove'; if (msg.includes('Not enough non-option arguments')) { - msg = 'Runtime remove command requires name argument'; + msg = 'Runtime remove command requires name and runtimeVersion arguments'; createError = true; } break; @@ -717,6 +754,7 @@ Considering "${template}" as the template files path.`); * @param {!String} name The name of the function to be added * @param {String} func The path to the function * @param {String} runtime The name of the runtime that will run the function + * @param {String} version The runtime version * @param {String} type The type of the AntFunction that will be added * @param {String|Boolean} config The configuration file path whose function * will be added; or a flag indicating this change should be done on the @@ -724,7 +762,7 @@ Considering "${template}" as the template files path.`); * @param {String} template The name or path to the template under the category * "Function" to render the function source file when it does not exists */ - async addFunction(name, func, runtime, type = 'lib', config, template) { + async addFunction(name, func, runtime, version, type = 'lib', config, template) { config = Core._getConfig(config); assert(!template || typeof template === 'string', 'Param "template" must be a String'); if (template) { @@ -742,8 +780,8 @@ Considering "${template}" as the template files path.`); /* eslint-disable no-case-declarations */ let runtimeInstance; if (runtime) { - runtimeInstance = this.ant.runtimeController.getRuntime(runtime); - assert(runtimeInstance, `Runtime "${runtime}" was not found`); + runtimeInstance = this.ant.runtimeController.getRuntime(runtime, version); + assert(runtimeInstance, `Runtime "${runtime}${version ? ` ${version}` : ''}" was not found`); } else { runtimeInstance = this.ant.runtimeController.defaultRuntime; } @@ -765,7 +803,7 @@ Considering "${template}" as the template files path.`); const runtimeTemplate = this.ant.templateController.getTemplate('Function', runtime); template = runtimeTemplate || runtimeInstance.template; } - config.addFunction(new LibFunction(this.ant, name, func, runtimeInstance)); + config.addFunction(new LibFunction(this.ant, name, func, runtimeInstance), !!version); break; case 'bin': config.addFunction(new BinFunction(this.ant, name, func)); @@ -809,7 +847,7 @@ Considering "${template}" as the template files path.`); const additionalInfo = func instanceof BinFunction ? `: ${func.bin}` : func instanceof LibFunction - ? `: ${func.handler} ${func.runtime.name}` + ? `: ${func.handler} ${func.runtime.name} ${func.runtime.version}` : ''; console.log(`${func.constructor.name} ${func.name}${additionalInfo}`); }); @@ -836,16 +874,17 @@ Considering "${template}" as the template files path.`); * Adds a runtime into the configuration file and saves it. * * @param {!String} name The name of the runtime to be added + * @param {!String} version The version of the runtime * @param {!String} bin The absolute path to the runtime * @param {Array} extensions The extensions supported by the runtime * @param {String|Boolean} config The configuration file path whose runtime * will be added; or a flag indicating this change should be done on the * global configuration (if true), or local configuration (if false). */ - async addRuntime(name, bin, extensions, config) { + async addRuntime(name, version, bin, extensions, config) { config = Core._getConfig(config); return config.addRuntime(new Runtime( - this.ant, name, bin, extensions + this.ant, name, bin, extensions, undefined, version )).save(); } @@ -853,13 +892,14 @@ Considering "${template}" as the template files path.`); * Removes a runtime from the configuration file and saves it. * * @param {!String} name The name of the runtime to be removed + * @param {!String} version The version of the runtime to be removed * @param {String|Boolean} config The configuration file path whose runtime will be removed; * or a flag indicating this change should be done on the global configuration (if true), * or local configuration (if false). */ - async removeRuntime(name, config) { + async removeRuntime(name, version, config) { config = Core._getConfig(config); - return config.removeRuntime(name).save(); + return config.removeRuntime(name, version).save(); } /** @@ -869,9 +909,30 @@ Considering "${template}" as the template files path.`); async listRuntimes() { const runtimes = this.ant.runtimeController.runtimes; console.log('Listing all runtimes available \ -( [extensions]):'); - runtimes.forEach(({ name, bin, extensions }) => { - console.log(`${name} ${bin}${extensions ? ` ${extensions.join(' ')}` : ''}`); +([default] [extensions] [template]):'); + const defaultRuntimes = new Set(); // Needed to avoid printing the default runtime twice + Array.from(runtimes.values()).forEach(runtimeByVersion => { + for(const [key, runtimeInstance] of runtimeByVersion.entries()) { + if (defaultRuntimes.has(runtimeInstance)) { + continue; + } + const isDefault = key === 'default'; + if (isDefault) { + defaultRuntimes.add(runtimeInstance); + } + + // Building the console.log content + const { name, bin, extensions, version, template } = runtimeInstance; + let runtime = isDefault ? `default ${name}` : name; + runtime += ` ${version} ${bin}`; + if (extensions && extensions.length) { + runtime += ` [${extensions.join(', ')}]`; + } + if (template) { + runtime += ` ${template}`; + } + console.log(runtime); + } }); } diff --git a/plugins/ant-core/spec/functions/python3Runtime.spec.js b/plugins/ant-core/spec/functions/python3Runtime.spec.js new file mode 100644 index 0000000..45b66a4 --- /dev/null +++ b/plugins/ant-core/spec/functions/python3Runtime.spec.js @@ -0,0 +1,91 @@ +/** + * @fileoverview Tests for python3Runtime.py file. + */ + +const path = require('path'); +const { Ant, LibFunction } = require('@back4app/ant'); +const Core = require('../../lib/Core'); +const { toArray } = require('rxjs/operators'); + +const ant = new Ant(); + +const PYTHON_FUNCS_PATH = path.resolve(__dirname, '../support/functions/python'); +const stringResultFunction = path.resolve(PYTHON_FUNCS_PATH, 'stringResultFunction.py'); +const integerResultFunction = path.resolve(PYTHON_FUNCS_PATH, 'integerResultFunction.py'); +const objectResultFunction = path.resolve(PYTHON_FUNCS_PATH, 'objectResultFunction.py'); +const listResultFunction = path.resolve(PYTHON_FUNCS_PATH, 'listResultFunction.py'); +const raiseExceptionFunction = path.resolve(PYTHON_FUNCS_PATH, 'raiseExceptionFunction.py'); +const echoParamFunction = path.resolve(PYTHON_FUNCS_PATH, 'v3/echoParamFunction.py'); + +describe('pythonRuntime.py', () => { + describe('should execute a function and', () => { + const assertFunctionExecution = async (funcPath, expected, params, flattenParams) => { + const core = new Core(ant); + const python = core.ant.runtimeController.getRuntime('Python', '3'); + const func = new LibFunction(new Ant(), 'func', funcPath, python); + const observable = flattenParams ? func.run(...params) : func.run(params); + const result = flattenParams ? await observable.pipe(toArray()).toPromise() : await observable.toPromise(); + expect(result).toEqual(expected); + }; + + test('return a string', async () => { + await assertFunctionExecution(stringResultFunction, 'foo'); + }); + + test('return an integer', async () => { + await assertFunctionExecution(integerResultFunction, 1); + }); + + test('return an object', async () => { + await assertFunctionExecution(objectResultFunction, { 'foo' : 'bar' }); + }); + + test('return a list', async () => { + await assertFunctionExecution(listResultFunction, [ + 'foo', + 1, + { 'foo': 'bar' } + ]); + }); + + test('handle the raised exception', async () => { + await assertFunctionExecution( + raiseExceptionFunction, + expect.stringContaining('Exception: Mocked error') + ); + }); + + test('echo a string param', async () => { + await assertFunctionExecution(echoParamFunction, 'param', 'param'); + }); + + test('echo an integer param', async () => { + await assertFunctionExecution(echoParamFunction, 1, 1); + }); + + test('echo an object param', async () => { + await assertFunctionExecution( + echoParamFunction, + { 'foo' : 'bar' }, + { 'foo' : 'bar' } + ); + }); + + test('echo a list param', async () => { + await assertFunctionExecution( + echoParamFunction, + ['foo', 1, { 'foo' : 'bar' }], + ['foo', 1, { 'foo' : 'bar' }] + ); + }); + + test('echo multiple params', async () => { + await assertFunctionExecution( + echoParamFunction, + ['foo', 1, { 'foo' : 'bar' }], + ['foo', 1, { 'foo' : 'bar' }], + true + ); + }); + }); +}); diff --git a/plugins/ant-core/spec/functions/pythonRuntime.spec.js b/plugins/ant-core/spec/functions/pythonRuntime.spec.js new file mode 100644 index 0000000..df294a9 --- /dev/null +++ b/plugins/ant-core/spec/functions/pythonRuntime.spec.js @@ -0,0 +1,91 @@ +/** + * @fileoverview Tests for pythonRuntime.py file. + */ + +const path = require('path'); +const { Ant, LibFunction } = require('@back4app/ant'); +const Core = require('../../lib/Core'); +const { toArray } = require('rxjs/operators'); + +const ant = new Ant(); + +const PYTHON_FUNCS_PATH = path.resolve(__dirname, '../support/functions/python'); +const stringResultFunction = path.resolve(PYTHON_FUNCS_PATH, 'stringResultFunction.py'); +const integerResultFunction = path.resolve(PYTHON_FUNCS_PATH, 'integerResultFunction.py'); +const objectResultFunction = path.resolve(PYTHON_FUNCS_PATH, 'objectResultFunction.py'); +const listResultFunction = path.resolve(PYTHON_FUNCS_PATH, 'listResultFunction.py'); +const raiseExceptionFunction = path.resolve(PYTHON_FUNCS_PATH, 'raiseExceptionFunction.py'); +const echoParamFunction = path.resolve(PYTHON_FUNCS_PATH, 'echoParamFunction.py'); + +describe('pythonRuntime.py', () => { + describe('should execute a function and', () => { + const assertFunctionExecution = async (funcPath, expected, params, flattenParams) => { + const core = new Core(ant); + const python = core.ant.runtimeController.getRuntime('Python', '2'); + const func = new LibFunction(new Ant(), 'func', funcPath, python); + const observable = flattenParams ? func.run(...params) : func.run(params); + const result = flattenParams ? await observable.pipe(toArray()).toPromise() : await observable.toPromise(); + expect(result).toEqual(expected); + }; + + test('return a string', async () => { + await assertFunctionExecution(stringResultFunction, 'foo'); + }); + + test('return an integer', async () => { + await assertFunctionExecution(integerResultFunction, 1); + }); + + test('return an object', async () => { + await assertFunctionExecution(objectResultFunction, { 'foo' : 'bar' }); + }); + + test('return a list', async () => { + await assertFunctionExecution(listResultFunction, [ + 'foo', + 1, + { 'foo': 'bar' } + ]); + }); + + test('handle the raised exception', async () => { + await assertFunctionExecution( + raiseExceptionFunction, + expect.stringContaining('Exception: Mocked error') + ); + }); + + test('echo a string param', async () => { + await assertFunctionExecution(echoParamFunction, 'param', 'param'); + }); + + test('echo an integer param', async () => { + await assertFunctionExecution(echoParamFunction, 1, 1); + }); + + test('echo an object param', async () => { + await assertFunctionExecution( + echoParamFunction, + { 'foo' : 'bar' }, + { 'foo' : 'bar' } + ); + }); + + test('echo a list param', async () => { + await assertFunctionExecution( + echoParamFunction, + ['foo', 1, { 'foo' : 'bar' }], + ['foo', 1, { 'foo' : 'bar' }] + ); + }); + + test('echo multiple params', async () => { + await assertFunctionExecution( + echoParamFunction, + ['foo', 1, { 'foo' : 'bar' }], + ['foo', 1, { 'foo' : 'bar' }], + true + ); + }); + }); +}); diff --git a/plugins/ant-core/spec/lib/Core.spec.js b/plugins/ant-core/spec/lib/Core.spec.js index 3b3c453..e8a359a 100644 --- a/plugins/ant-core/spec/lib/Core.spec.js +++ b/plugins/ant-core/spec/lib/Core.spec.js @@ -1028,7 +1028,7 @@ describe('lib/Core.js', () => { }); jest.spyOn(Config.prototype, 'save'); const core = new Core(ant); - await core.addFunction(name, func, null, 'bin'); + await core.addFunction(name, func, null, null, 'bin'); expect(getLocalConfigPath).toHaveBeenCalled(); expect(Config.prototype.save).toHaveBeenCalled(); }); @@ -1045,7 +1045,7 @@ describe('lib/Core.js', () => { }; jest.spyOn(Core, '_getConfig').mockImplementation(() => configMock); const core = new Core(ant); - await core.addFunction(name, func, null, 'bin', true); + await core.addFunction(name, func, null, null, 'bin', true); expect(Core._getConfig).toHaveBeenCalledWith(true); expect(configMock.addFunction).toHaveBeenCalled(); expect(configMock.save).toHaveBeenCalled(); @@ -1055,7 +1055,7 @@ describe('lib/Core.js', () => { const ant = new Ant(); const name = 'myFunc'; const func = '/path/to/func'; - const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime'); + const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime', [], undefined, '1'); const configFilePath = path.resolve(outPath, 'ant.yml'); fs.ensureFileSync(configFilePath); const getLocalConfigPath = jest.spyOn(Config, 'GetLocalConfigPath') @@ -1072,9 +1072,9 @@ describe('lib/Core.js', () => { }); const save = jest.spyOn(Config.prototype, 'save'); const core = new Core(ant); - await core.addFunction(name, func, runtimeInstance.name, 'lib'); + await core.addFunction(name, func, runtimeInstance.name, runtimeInstance.version, 'lib'); expect(getLocalConfigPath).toHaveBeenCalled(); - expect(getRuntime).toHaveBeenCalledWith(runtimeInstance.name); + expect(getRuntime).toHaveBeenCalledWith(runtimeInstance.name, runtimeInstance.version); expect(save).toHaveBeenCalled(); }); @@ -1082,7 +1082,7 @@ describe('lib/Core.js', () => { const ant = new Ant(); const name = 'myFunc'; const func = '/path/to/func'; - const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime'); + const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime', [], undefined, '1'); const configFilePath = path.resolve(outPath, 'ant.yml'); fs.ensureFileSync(configFilePath); const getLocalConfigPath = jest.spyOn(Config, 'GetLocalConfigPath') @@ -1106,7 +1106,7 @@ describe('lib/Core.js', () => { test('should add LibFunction with default runtime and no defined path', async () => { const ant = new Ant(); const name = 'myFunc'; - const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime', ['foo']); + const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime', ['foo'], undefined, '1'); const configFilePath = path.resolve(outPath, 'ant.yml'); fs.ensureFileSync(configFilePath); const getLocalConfigPath = jest.spyOn(Config, 'GetLocalConfigPath') @@ -1151,7 +1151,7 @@ describe('lib/Core.js', () => { const name = 'myFunc'; const core = new Core(ant); fs.ensureFileSync(path.resolve(outPath, 'ant.yml')); - const fooRuntime = new Runtime(ant, 'Foo', '/bin/foo', ['js']); + const fooRuntime = new Runtime(ant, 'Foo', '/bin/foo', ['js'], undefined, '1'); ant.runtimeController.loadRuntimes([fooRuntime]); const originalRender = Template.prototype.render; const render = Template.prototype.render = jest.fn(); @@ -1180,7 +1180,7 @@ describe('lib/Core.js', () => { const name = 'myFunc'; const core = new Core(ant); fs.ensureFileSync(path.resolve(outPath, 'ant.yml')); - const fooRuntime = new Runtime(ant, 'Foo', '/bin/foo', ['js']); + const fooRuntime = new Runtime(ant, 'Foo', '/bin/foo', ['js'], undefined, '1'); ant.runtimeController.loadRuntimes([fooRuntime]); const templateMocked = new Template('Function', 'myTemplate', '/myTemplate/path'); @@ -1188,7 +1188,7 @@ describe('lib/Core.js', () => { ant.templateController.loadTemplates([templateMocked]); jest.spyOn(fs, 'existsSync').mockImplementation(() => false); const funcPath = path.resolve(outPath, 'foo/bar/myFunc.js'); - await core.addFunction(name, funcPath, 'Foo', undefined, undefined, 'myTemplate'); + await core.addFunction(name, funcPath, 'Foo', undefined, undefined, undefined, 'myTemplate'); expect(templateMocked.render).toHaveBeenCalledWith( funcPath, expect.any(Object) @@ -1233,7 +1233,7 @@ describe('lib/Core.js', () => { fs.ensureFileSync(path.resolve(outPath, 'ant.yml')); jest.spyOn(fs, 'existsSync').mockImplementation(() => false); try { - await core.addFunction(name, null, null, undefined, undefined, '/my/invalid/path'); + await core.addFunction(name, null, null, undefined, undefined, undefined, '/my/invalid/path'); } catch (err) { expect(err.message).toBe('Param "template" is not a valid path: /my/invalid/path'); } @@ -1244,9 +1244,9 @@ describe('lib/Core.js', () => { const core = new Core(ant); fs.ensureFileSync(path.resolve(outPath, 'ant.yml')); try { - await core.addFunction(null, null, 'should not find me'); + await core.addFunction(null, null, 'should not find me', '1'); } catch (err) { - expect(err.message).toBe('Runtime "should not find me" was not found'); + expect(err.message).toBe('Runtime "should not find me 1" was not found'); } }); @@ -1256,9 +1256,8 @@ describe('lib/Core.js', () => { const ant = new Ant(); const name = 'myFunc'; const func = '/path/to/func'; - const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime'); const core = new Core(ant); - expect(core.addFunction(name, func, runtimeInstance.name, 'foo')) + expect(core.addFunction(name, func, null, null, 'foo')) .rejects.toThrowError('AntFunction type "foo" is unknown'); }); @@ -1396,12 +1395,12 @@ describe('lib/Core.js', () => { }); describe('function ls command', () => { - test('should print templates', async () => { + test('should print functions', async () => { console.log = jest.fn(); const functions = [ new AntFunction(ant, 'ant', () => {}), new BinFunction(ant, 'foo', '/path/to/foo'), - new LibFunction(ant, 'bar', '/path/to/bar', new Runtime(ant, 'barRuntime', '/path/to/runtime')) + new LibFunction(ant, 'bar', '/path/to/bar', new Runtime(ant, 'barRuntime', '/path/to/runtime', [], undefined, '1')) ]; ant.functionController.getAllFunctions = jest.fn().mockImplementation(() => functions); const core = new Core(ant); @@ -1411,7 +1410,7 @@ describe('lib/Core.js', () => { ( [: (| )]):'); expect(console.log.mock.calls[1][0]).toBe('AntFunction ant'); expect(console.log.mock.calls[2][0]).toBe('BinFunction foo: /path/to/foo'); - expect(console.log.mock.calls[3][0]).toBe('LibFunction bar: /path/to/bar barRuntime'); + expect(console.log.mock.calls[3][0]).toBe('LibFunction bar: /path/to/bar barRuntime 1'); }); test('should handle error message', done => { @@ -1586,6 +1585,7 @@ describe('lib/Core.js', () => { describe('runtime add command', () => { test('should add runtime and save locally', async () => { const name = 'runtime'; + const version = '1'; const bin = '/my/runtime'; const extensions = [ 'js' ]; const configFilePath = path.resolve(outPath, 'ant.yml'); @@ -1598,12 +1598,13 @@ describe('lib/Core.js', () => { jest.spyOn(Config.prototype, 'addRuntime') .mockImplementation(runtime => { expect(runtime.name).toBe(name); + expect(runtime.version).toBe(version); expect(runtime.bin).toBe(bin); expect(runtime.extensions).toBe(extensions); return configMock; }); const core = new Core(ant); - await core.addRuntime(name, bin, extensions); + await core.addRuntime(name, version, bin, extensions); expect(getLocalConfigPath).toHaveBeenCalled(); expect(configMock.save).toHaveBeenCalled(); }); @@ -1611,7 +1612,8 @@ describe('lib/Core.js', () => { test('should add runtime and save locally v2', (done) => { const antCli = new AntCli(); const name = 'runtime'; - const bin = 'my/runtime'; + const version = '1'; + const bin = '/my/runtime'; const extensions = 'js'; const configFilePath = path.resolve(outPath, 'ant.yml'); fs.ensureFileSync(configFilePath); @@ -1623,11 +1625,12 @@ describe('lib/Core.js', () => { jest.spyOn(Config.prototype, 'addRuntime') .mockImplementation(runtime => { expect(runtime.name).toBe(name); + expect(runtime.version).toBe(version); expect(runtime.bin).toEqual(expect.stringContaining(bin)); expect(runtime.extensions).toEqual([extensions]); return configMock; }); - antCli._yargs.parse(`runtime add ${name} ${bin} ${extensions}`); + antCli._yargs.parse(`runtime add ${name} ${version} ${bin} ${extensions}`); process.exit = jest.fn((code) => { expect(code).toEqual(0); expect(getLocalConfigPath).toHaveBeenCalled(); @@ -1664,11 +1667,13 @@ describe('lib/Core.js', () => { test('should add runtime and save globally', async () => { const name = 'runtime'; + const version = '1'; const bin = '/my/runtime'; const extensions = [ 'js' ]; const configMock = { addRuntime: jest.fn(runtime => { expect(runtime.name).toBe(name); + expect(runtime.version).toBe(version); expect(runtime.bin).toBe(bin); expect(runtime.extensions).toBe(extensions); return configMock; @@ -1677,7 +1682,7 @@ describe('lib/Core.js', () => { }; jest.spyOn(Core, '_getConfig').mockImplementation(() => configMock); const core = new Core(ant); - await core.addRuntime(name, bin, extensions, true); + await core.addRuntime(name, version, bin, extensions, true); expect(Core._getConfig).toHaveBeenCalledWith(true); expect(configMock.addRuntime).toHaveBeenCalled(); expect(configMock.save).toHaveBeenCalled(); @@ -1688,7 +1693,7 @@ describe('lib/Core.js', () => { process.argv = ['runtime', 'add']; process.exit = jest.fn(code => { expect(handleErrorMessage).toHaveBeenCalledWith( - 'Runtime add command requires name and bin arguments', null, 'runtime add' + 'Runtime add command requires name, runtimeVersion and bin arguments', null, 'runtime add' ); expect(code).toEqual(1); done(); @@ -1696,12 +1701,25 @@ describe('lib/Core.js', () => { new Core(ant)._yargsFailed('Not enough non-option arguments'); }); - test('should show friendly error when bin was not passed', done => { + test('should show friendly error when version was not passed', done => { const handleErrorMessage = jest.spyOn(yargsHelper, 'handleErrorMessage'); process.argv = ['runtime', 'add', 'myruntime']; process.exit = jest.fn((code) => { expect(handleErrorMessage).toHaveBeenCalledWith( - 'Runtime add command requires name and bin arguments', null, 'runtime add' + 'Runtime add command requires name, runtimeVersion and bin arguments', null, 'runtime add' + ); + expect(code).toEqual(1); + done(); + }); + new Core(ant)._yargsFailed('Not enough non-option arguments'); + }); + + test('should show friendly error when bin was not passed', done => { + const handleErrorMessage = jest.spyOn(yargsHelper, 'handleErrorMessage'); + process.argv = ['runtime', 'add', 'myruntime 123']; + process.exit = jest.fn((code) => { + expect(handleErrorMessage).toHaveBeenCalledWith( + 'Runtime add command requires name, runtimeVersion and bin arguments', null, 'runtime add' ); expect(code).toEqual(1); done(); @@ -1720,6 +1738,7 @@ describe('lib/Core.js', () => { describe('runtime remove command', () => { test('should remove runtime and save locally', async () => { const name = 'myRuntime'; + const version = '1'; const configFilePath = path.resolve(outPath, 'ant.yml'); fs.ensureFileSync(configFilePath); const getLocalConfigPath = jest.spyOn(Config, 'GetLocalConfigPath') @@ -1727,15 +1746,16 @@ describe('lib/Core.js', () => { const removeRuntime = jest.spyOn(Config.prototype, 'removeRuntime'); const save = jest.spyOn(Config.prototype, 'save'); const core = new Core(ant); - await core.removeRuntime(name); + await core.removeRuntime(name, version); expect(getLocalConfigPath).toHaveBeenCalled(); - expect(removeRuntime).toHaveBeenCalled(); + expect(removeRuntime).toHaveBeenCalledWith(name, version); expect(save).toHaveBeenCalled(); }); test('should handle error message', async () => { const antCli = new AntCli(); const name = 'myRuntime'; + const version = '1'; const configFilePath = path.resolve(outPath, 'ant.yml'); fs.ensureFileSync(configFilePath); const getLocalConfigPath = jest.spyOn(Config, 'GetLocalConfigPath') @@ -1748,7 +1768,7 @@ describe('lib/Core.js', () => { yargsHelper, 'handleErrorMessage' ); - antCli._yargs.parse(`runtime remove ${name}`); + antCli._yargs.parse(`runtime remove ${name} ${version}`); process.exit = jest.fn(code => { expect(code).toEqual(1); expect(getLocalConfigPath).toHaveBeenCalled(); @@ -1760,18 +1780,20 @@ describe('lib/Core.js', () => { test('should remove runtime and save globally', async () => { const name = 'myRuntime'; + const version = '1'; const configMock = { - removeRuntime: jest.fn().mockImplementation(runtimeName => { + removeRuntime: jest.fn().mockImplementation((runtimeName, runtimeVersion) => { expect(runtimeName).toBe(name); + expect(runtimeVersion).toBe(version); return configMock; }), save: jest.fn() }; jest.spyOn(Core, '_getConfig').mockImplementation(() => configMock); const core = new Core(ant); - await core.removeRuntime(name, true); + await core.removeRuntime(name, version, true); expect(Core._getConfig).toHaveBeenCalledWith(true); - expect(configMock.removeRuntime).toHaveBeenCalled(); + expect(configMock.removeRuntime).toHaveBeenCalledWith(name, version); expect(configMock.save).toHaveBeenCalled(); }); @@ -1780,7 +1802,20 @@ describe('lib/Core.js', () => { process.argv = ['runtime', 'remove']; process.exit = jest.fn((code) => { expect(handleErrorMessage).toHaveBeenCalledWith( - 'Runtime remove command requires name argument', null, 'runtime remove' + 'Runtime remove command requires name and runtimeVersion arguments', null, 'runtime remove' + ); + expect(code).toEqual(1); + done(); + }); + new Core(ant)._yargsFailed('Not enough non-option arguments'); + }); + + test('should show friendly error when runtimeVersion was not passed', async done => { + const handleErrorMessage = jest.spyOn(yargsHelper, 'handleErrorMessage'); + process.argv = ['runtime', 'remove', 'node']; + process.exit = jest.fn((code) => { + expect(handleErrorMessage).toHaveBeenCalledWith( + 'Runtime remove command requires name and runtimeVersion arguments', null, 'runtime remove' ); expect(code).toEqual(1); done(); @@ -1800,43 +1835,62 @@ describe('lib/Core.js', () => { test('should print runtimes', async () => { console.log = jest.fn(); const runtimes = [ - new Runtime(ant, 'foo', '/path/to/foo', ['foo', 'js']), - new Runtime(ant, 'bar', '/path/to/bar', ['bar']), - new Runtime(ant, 'lorem', '/ipsum') + new Runtime(ant, 'foo', '/path/to/foo', ['foo', 'js'], '/foo/template', '4.0.0'), + new Runtime(ant, 'bar', '/path/to/bar', ['bar'], undefined, '3.2.1'), + new Runtime(ant, 'lorem', '/ipsum', [], undefined, '1'), + new Runtime(ant, 'lorem', '/ipsum', [], undefined, '2') ]; ant.runtimeController._runtimes = new Map(); ant.runtimeController.loadRuntimes(runtimes); const core = new Core(ant); await core.listRuntimes(); - expect(console.log.mock.calls.length).toBe(4); + expect(console.log.mock.calls.length).toBe(5); expect(console.log.mock.calls[0][0]).toBe('Listing all runtimes available \ -( [extensions]):'); - expect(console.log.mock.calls[1][0]).toBe('foo /path/to/foo foo js'); - expect(console.log.mock.calls[2][0]).toBe('bar /path/to/bar bar'); - expect(console.log.mock.calls[3][0]).toBe('lorem /ipsum'); +([default] [extensions] [template]):'); + expect(console.log.mock.calls[1][0]).toBe('default foo 4 /path/to/foo [foo, js] /foo/template'); + expect(console.log.mock.calls[2][0]).toBe('default bar 3 /path/to/bar [bar]'); + expect(console.log.mock.calls[3][0]).toBe('default lorem 1 /ipsum'); + expect(console.log.mock.calls[4][0]).toBe('lorem 2 /ipsum'); }); test('should print runtimes v2', (done) => { console.log = jest.fn(); const antCli = new AntCli(); const runtimes = [ - new Runtime(antCli._ant, 'foo', '/path/to/foo', ['foo', 'js']), - new Runtime(antCli._ant, 'bar', '/path/to/bar', ['bar']), - new Runtime(antCli._ant, 'lorem', '/ipsum') + new Runtime(antCli._ant, 'foo', '/path/to/foo', ['foo', 'js'], '/foo/template', '4.0.0'), + new Runtime(antCli._ant, 'bar', '/path/to/bar', ['bar'], undefined, '3.2.1'), + new Runtime(antCli._ant, 'lorem', '/ipsum', [], undefined, '1'), + new Runtime(antCli._ant, 'lorem', '/ipsum', [], undefined, '2') ]; antCli._ant.runtimeController._runtimes = new Map(); antCli._ant.runtimeController.loadRuntimes(runtimes); - antCli._yargs.parse('runtime ls'); + process.exit = jest.fn(code => { - expect(code).toEqual(1); - expect(console.log.mock.calls.length).toBe(4); + expect(code).toBe(0); + expect(console.log.mock.calls.length).toBe(5); expect(console.log.mock.calls[0][0]).toBe('Listing all runtimes available \ -( [extensions]):'); - expect(console.log.mock.calls[1][0]).toBe('foo /path/to/foo foo js'); - expect(console.log.mock.calls[2][0]).toBe('bar /path/to/bar bar'); - expect(console.log.mock.calls[3][0]).toBe('lorem /ipsum'); +([default] [extensions] [template]):'); + expect(console.log.mock.calls[1][0]).toBe('default foo 4 /path/to/foo [foo, js] /foo/template'); + expect(console.log.mock.calls[2][0]).toBe('default bar 3 /path/to/bar [bar]'); + expect(console.log.mock.calls[3][0]).toBe('default lorem 1 /ipsum'); + expect(console.log.mock.calls[4][0]).toBe('lorem 2 /ipsum'); done(); }); + antCli._yargs.parse('runtime ls'); + }); + + test('should not show "runtime ls" friendly error when error is unknown', () => { + const handleErrorMessage = jest.spyOn(yargsHelper, 'handleErrorMessage'); + const error = new Error('Mocked error'); + const antCli = new AntCli(); + antCli._ant.pluginController.getPlugin('Core').listRuntimes = jest.fn(async () => { + throw error; + }); + process.exit = jest.fn(code => { + expect(code).toBe(1); + expect(handleErrorMessage).toHaveBeenCalledWith(error.message, error, 'runtime ls'); + }); + antCli._yargs.parse('runtime ls'); }); }); }); diff --git a/plugins/ant-core/spec/support/functions/python/echoParamFunction.py b/plugins/ant-core/spec/support/functions/python/echoParamFunction.py new file mode 100644 index 0000000..097ff76 --- /dev/null +++ b/plugins/ant-core/spec/support/functions/python/echoParamFunction.py @@ -0,0 +1,17 @@ +import sys +import json +import time + +def write(arg): + sys.stdout.write(arg) + sys.stdout.flush() + time.sleep(0.01) + +def main(args): + for arg in args: + if type(arg) is int: + write(repr(arg)) + elif isinstance(arg, basestring): + write(arg) + elif type(arg) is dict or type(arg) is list: + write(json.dumps(arg)) diff --git a/plugins/ant-core/spec/support/functions/python/integerResultFunction.py b/plugins/ant-core/spec/support/functions/python/integerResultFunction.py new file mode 100644 index 0000000..9cf98c9 --- /dev/null +++ b/plugins/ant-core/spec/support/functions/python/integerResultFunction.py @@ -0,0 +1,5 @@ +import sys + +def main(args): + sys.stdout.write(repr(1)) + sys.stdout.flush() diff --git a/plugins/ant-core/spec/support/functions/python/listResultFunction.py b/plugins/ant-core/spec/support/functions/python/listResultFunction.py new file mode 100644 index 0000000..278c161 --- /dev/null +++ b/plugins/ant-core/spec/support/functions/python/listResultFunction.py @@ -0,0 +1,10 @@ +import sys +import json + +def main(args): + sys.stdout.write(json.dumps([ + 'foo', + 1, + { 'foo': 'bar' } + ])) + sys.stdout.flush() diff --git a/plugins/ant-core/spec/support/functions/python/objectResultFunction.py b/plugins/ant-core/spec/support/functions/python/objectResultFunction.py new file mode 100644 index 0000000..ca08bb8 --- /dev/null +++ b/plugins/ant-core/spec/support/functions/python/objectResultFunction.py @@ -0,0 +1,6 @@ +import sys +import json + +def main(args): + sys.stdout.write(json.dumps({ 'foo': 'bar' })) + sys.stdout.flush() diff --git a/plugins/ant-core/spec/support/functions/python/raiseExceptionFunction.py b/plugins/ant-core/spec/support/functions/python/raiseExceptionFunction.py new file mode 100644 index 0000000..3d3d9f0 --- /dev/null +++ b/plugins/ant-core/spec/support/functions/python/raiseExceptionFunction.py @@ -0,0 +1,2 @@ +def main(args): + raise Exception('Mocked error') diff --git a/plugins/ant-core/spec/support/functions/python/stringResultFunction.py b/plugins/ant-core/spec/support/functions/python/stringResultFunction.py new file mode 100644 index 0000000..3c463dc --- /dev/null +++ b/plugins/ant-core/spec/support/functions/python/stringResultFunction.py @@ -0,0 +1,5 @@ +import sys + +def main(args): + sys.stdout.write('foo') + sys.stdout.flush() diff --git a/plugins/ant-core/spec/support/functions/python/v3/echoParamFunction.py b/plugins/ant-core/spec/support/functions/python/v3/echoParamFunction.py new file mode 100644 index 0000000..bde628e --- /dev/null +++ b/plugins/ant-core/spec/support/functions/python/v3/echoParamFunction.py @@ -0,0 +1,17 @@ +import sys +import json +import time + +def write(arg): + sys.stdout.write(arg) + sys.stdout.flush() + time.sleep(0.01) + +def main(args): + for arg in args: + if type(arg) is int: + write(repr(arg)) + elif type(arg) is str: + write(arg) + elif type(arg) is dict or type(arg) is list: + write(json.dumps(arg)) diff --git a/plugins/ant-core/templates/function/java.java.mustache b/plugins/ant-core/templates/function/java.java.mustache new file mode 100644 index 0000000..5d744b7 --- /dev/null +++ b/plugins/ant-core/templates/function/java.java.mustache @@ -0,0 +1,69 @@ +import java.lang.StringBuffer; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An example class which invokes a RESTful API and returns its content + * to the Ant Framework GraphQL API. + */ +public class {{name}} { + /** + * The args[0] represents the field parameters from the GraphQL query. + */ + public static void main(String[] args) throws IOException { + /** + * The parameters are stored in a stringified JSON object. + * So, in order to make use of them, we need to parse it. + */ + String cityName = parseJson(args[0]); + try { + // Opening connection to the RESTful API + String url = "http://api.openweathermap.org/data/2.5/weather?q=" + + URLEncoder.encode(cityName, "UTF-8") + + "&appid=464ddf23d2c714ee8ecbc5b39f1f7eae"; + HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); + con.setRequestMethod("GET"); + + // Retrieving the response content + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + String inputLine; + StringBuffer response = new StringBuffer(); + + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + + // Prints the response to the stdout, which Ant is watching. + System.out.print(response.toString()); + } catch (Exception e) { + // If any exception occurs, we should also let Ant be aware of it. + System.out.print(e.toString()); + } + } + + /** + * Since this file is only for testing purposes and we aren't importing any + * external libs, we parse the JSON using a regex. + * + * In this example, we are expecting the "city" parameter. So + * all we need to do here is retrieve it from the JSON and return + * its value, which is a String. + */ + public static String parseJson(jsonAsString) { + String cityJsonRegex = "(?:\\\"city\\\":\\\")(.*?)(?:\\\")"; + Pattern pattern = Pattern.compile(cityJsonRegex); + Matcher matcher = pattern.matcher(jsonAsString); + if (matcher.find()) { + String cityName = matcher.group().split(":")[1].replaceAll("\\\"", ""); + return cityName; + } + return null; + } +} diff --git a/plugins/ant-core/templates/function/python.py.mustache b/plugins/ant-core/templates/function/python.py.mustache new file mode 100644 index 0000000..25ab9a1 --- /dev/null +++ b/plugins/ant-core/templates/function/python.py.mustache @@ -0,0 +1,33 @@ +import urllib +import urllib2 +import sys + +# This is a sample Python 2 function that sends a GET +# request to the Wikipedia server, and finds articles +# by a "term" provided through args. + +# Args is an array, where its first element is a JSON +# representing the field parameters from the GraphQL query +# executed by Ant. +def main(args): + graphQLArgs = args[0] + # Builds the URL with the querystring + data = { + 'srsearch': graphQLArgs['term'], + 'action': 'query', + 'list': 'search', + 'format': 'json', + 'utf8': '' + } + querystring = urllib.urlencode(data) + url = 'http://en.wikipedia.org/w/api.php?' + querystring + + # Sends the GET request and catches the response + request = urllib2.Request(url) + response = urllib2.urlopen(request) + + # Extracts the response and writes it back to stdout, which + # Ant is watching. + response = response.read() + sys.stdout.write(response) + sys.stdout.flush() diff --git a/plugins/ant-core/templates/function/python3.py.mustache b/plugins/ant-core/templates/function/python3.py.mustache new file mode 100644 index 0000000..401cd82 --- /dev/null +++ b/plugins/ant-core/templates/function/python3.py.mustache @@ -0,0 +1,33 @@ +import urllib.request +import urllib.parse +import sys + +# This is a sample Python 3 function that sends a GET +# request to the Wikipedia server, and finds articles +# by a "term" provided through args. + +# Args is an array, where its first element is a JSON +# representing the field parameters from the GraphQL query +# executed by Ant. +def main(args): + graphQLArgs = args[0] + # Builds the URL with the querystring + data = { + 'srsearch': graphQLArgs['term'], + 'action': 'query', + 'list': 'search', + 'format': 'json', + 'utf8': '' + } + querystring = urllib.parse.urlencode(data) + url = 'http://en.wikipedia.org/w/api.php?' + querystring + + # Sends the GET request and catches the response + request = urllib.request.Request(url) + response = urllib.request.urlopen(request) + + # Extracts the response and writes it back to stdout, which + # Ant is watching. + response = response.read().decode('utf-8') + sys.stdout.write(response) + sys.stdout.flush() diff --git a/plugins/ant-graphql/lib/GraphQL.js b/plugins/ant-graphql/lib/GraphQL.js index cd13e7a..171265c 100644 --- a/plugins/ant-graphql/lib/GraphQL.js +++ b/plugins/ant-graphql/lib/GraphQL.js @@ -17,6 +17,7 @@ const mock = require('../functions/mock'); const resolve = require('../functions/resolve'); const subscribe = require('../functions/subscribe'); +const { stdout, stderr } = process; const defaultServerPath = path.dirname( require.resolve('@back4app/ant-graphql-express') ); @@ -249,7 +250,7 @@ directory "${cwd}"` this._serverProcess.stdout.on('data', (data) => { data = data.toString(); - console.log(`Server => ${data}`); + stdout.write(`Server => ${data}`); const successMessage = 'GraphQL API server listening for requests on '; @@ -269,7 +270,7 @@ directory "${cwd}"` }); this._serverProcess.stderr.on('data', (data) => { - console.error(`Server => ${data}`); + stderr.write(`Server => ${data}`); }); const promise = new Promise((resolve, reject) => { diff --git a/plugins/ant-graphql/spec/lib/GraphQL.spec.js b/plugins/ant-graphql/spec/lib/GraphQL.spec.js index ace2dc8..6009076 100644 --- a/plugins/ant-graphql/spec/lib/GraphQL.spec.js +++ b/plugins/ant-graphql/spec/lib/GraphQL.spec.js @@ -73,9 +73,7 @@ describe('lib/GraphQL.js', () => { }); test('should fail if server crashes', async () => { - const originalError = console.error; - console.error = jest.fn(); - expect.hasAssertions(); + const write = jest.spyOn(process.stderr, 'write').mockImplementation(() => {}); const bin = path.resolve( utilPath, 'templates/crashServerTemplate/server.js' @@ -92,10 +90,11 @@ describe('lib/GraphQL.js', () => { expect(e.message).toEqual( expect.stringContaining('Server process closed with code "1"') ); - expect(console.error).toHaveBeenCalledWith(expect.stringContaining( + expect(write).toHaveBeenCalledWith(expect.stringContaining( 'Crashed' )); - console.error = originalError; + } finally { + jest.restoreAllMocks(); } }); @@ -177,10 +176,10 @@ describe('lib/GraphQL.js', () => { expect.hasAssertions(); const originalExec = childProcess.exec; childProcess.exec = jest.fn(); - const originalError = console.error; - const originalLog = console.log; - console.error = jest.fn(); - console.log = jest.fn(); + const originalErrWrite = process.stderr.write; + process.stderr.write = jest.fn(); + const originalOutWrite = process.stdout.write; + process.stdout.write = jest.fn(); const model = path.resolve( utilPath, 'configs/graphQLPluginConfig/model.graphql' @@ -192,14 +191,14 @@ describe('lib/GraphQL.js', () => { const server = { bin }; const graphQL = new GraphQL(ant, { model, server }); await graphQL.startService(); - expect(console.error).toHaveBeenCalledWith( + expect(process.stderr.write).toHaveBeenCalledWith( expect.stringContaining('Some server error') ); - expect(console.log).toHaveBeenCalledWith( + expect(process.stdout.write).toHaveBeenCalledWith( expect.stringContaining('Some other log') ); - console.error = originalError; - console.log = originalLog; + process.stderr.write = originalErrWrite; + process.stdout.write = originalOutWrite; childProcess.exec = originalExec; }); diff --git a/plugins/ant-graphql/spec/lib/directives/DirectiveController.spec.js b/plugins/ant-graphql/spec/lib/directives/DirectiveController.spec.js index 2331f9d..04d3cc8 100644 --- a/plugins/ant-graphql/spec/lib/directives/DirectiveController.spec.js +++ b/plugins/ant-graphql/spec/lib/directives/DirectiveController.spec.js @@ -13,7 +13,7 @@ const DirectiveController = require( const ant = new Ant(); const fooFunction = new AntFunction(ant, 'fooFunction'); const barFunction = new LibFunction(ant, 'barFunction', '/my/handler', - new Runtime(ant, 'libRuntime', '/lib/runtime', ['js']) + new Runtime(ant, 'libRuntime', '/lib/runtime', ['js'], undefined, '1') ); const handler = '/foo/handler'; const runtime = 'Node'; diff --git a/plugins/ant-serverless/spec/lib/Serverless.spec.js b/plugins/ant-serverless/spec/lib/Serverless.spec.js index 75f3475..360a8cc 100644 --- a/plugins/ant-serverless/spec/lib/Serverless.spec.js +++ b/plugins/ant-serverless/spec/lib/Serverless.spec.js @@ -79,7 +79,7 @@ describe('lib/Serverless.js', () => { try { fs.ensureDirSync(basePath); } finally { - (new Template( + await (new Template( 'service', 'FooService', path.resolve( @@ -266,7 +266,7 @@ describe('lib/Serverless.js', () => { try { fs.ensureDirSync(basePath); } finally { - (new Template( + await (new Template( 'service', 'FooService', path.resolve(