From 91c1590453343361d15690d74892d9de036c5ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 28 Aug 2017 16:46:56 -0400 Subject: [PATCH 1/3] add get-body logic for 'figure' item key - where if 'figure' is present, get-body only parse the figure value - this is important for mimicking request using path to data/layout coupled to some other options (e.g. width, height, format) --- src/app/runner/get-body.js | 44 ++++++++++++++++++++++++++++++-------- test/unit/runner_test.js | 12 +++++++++++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/app/runner/get-body.js b/src/app/runner/get-body.js index 723b2422..19d3e4a7 100644 --- a/src/app/runner/get-body.js +++ b/src/app/runner/get-body.js @@ -1,5 +1,6 @@ const fs = require('fs') const isUrl = require('is-url') +const isPlainObj = require('is-plain-obj') const request = require('request') /** @@ -9,19 +10,44 @@ const request = require('request') * - body */ function getBody (item, cb) { - if (fs.existsSync(item)) { - fs.readFile(item, 'utf-8', cb) - } else if (fs.existsSync(item + '.json')) { - fs.readFile(item + '.json', 'utf-8', cb) - } else if (isUrl(item)) { - request.get(item, (err, res, body) => { + let p + let done + + // if item is object and has 'figure' key, + // only parse its 'figure' value and accumulate it with item + // to form body object + if (isPlainObj(item) && item.figure) { + p = item.figure + done = (err, _figure) => { + let figure + + try { + figure = JSON.parse(_figure) + } catch (e) { + return cb(e) + } + + const body = Object.assign({}, item, {figure: figure}) + cb(err, body) + } + } else { + p = item + done = cb + } + + if (fs.existsSync(p)) { + fs.readFile(p, 'utf-8', done) + } else if (fs.existsSync(p + '.json')) { + fs.readFile(p + '.json', 'utf-8', done) + } else if (isUrl(p)) { + request.get(p, (err, res, body) => { if (err) { - return cb(err) + return done(err) } - cb(null, body) + done(null, body) }) } else { - cb(null, item) + done(null, item) } } diff --git a/test/unit/runner_test.js b/test/unit/runner_test.js index ebca43a6..749a166f 100644 --- a/test/unit/runner_test.js +++ b/test/unit/runner_test.js @@ -113,6 +113,18 @@ tap.test('getBody:', t => { }) }) + t.test('should accept and parse nested *figure* path', t => { + getBody({ + figure: paths.pkg, + format: 'png' + }, (err, body) => { + t.equal(err, null, 'error') + t.type(body.figure, 'object') + t.equal(body.format, 'png', 'other stuff in item') + t.end() + }) + }) + t.end() }) From 5416fcac697908b5d143a134405b0beaac7443e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 28 Aug 2017 16:47:15 -0400 Subject: [PATCH 2/3] fixup pipeToStdOut logic --- bin/plotly-graph-exporter_electron.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/plotly-graph-exporter_electron.js b/bin/plotly-graph-exporter_electron.js index 0534efbd..328bd68f 100644 --- a/bin/plotly-graph-exporter_electron.js +++ b/bin/plotly-graph-exporter_electron.js @@ -29,7 +29,7 @@ if (!fs.existsSync(argv.outputDir)) { getStdin().then((txt) => { const hasStdin = !!txt - const pipeToStdOut = hasStdin && !argv.output && !argv.outputDir + const pipeToStdOut = hasStdin && !argv.output const showLogs = !pipeToStdOut && (DEBUG || argv.verbose) const input = hasStdin ? argv._.concat([txt]) : argv._ const getItemName = makeGetItemName(input) From c8d54831f799e00efbf80b88ff08e8bbed8c7438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 28 Aug 2017 16:52:21 -0400 Subject: [PATCH 3/3] add `write` callback to runner app options - to allow runner apps to add `write` step (e.g. fs.writeFile(outPath, info.body) inside each task, ensure that the app quits when all async logic is completed. - after-export-all exit code mimick process exit code (i.e. 0 for success, 1 for failure) --- bin/plotly-graph-exporter_electron.js | 24 ++++--- src/app/runner/coerce-opts.js | 4 ++ src/app/runner/constants.js | 5 +- src/app/runner/index.js | 5 +- src/app/runner/run.js | 63 ++++++++++++------- .../integration/plotly-graph-exporter_test.js | 2 +- test/unit/runner_test.js | 18 +++--- 7 files changed, 73 insertions(+), 48 deletions(-) diff --git a/bin/plotly-graph-exporter_electron.js b/bin/plotly-graph-exporter_electron.js index 328bd68f..ce3f593b 100644 --- a/bin/plotly-graph-exporter_electron.js +++ b/bin/plotly-graph-exporter_electron.js @@ -34,8 +34,21 @@ getStdin().then((txt) => { const input = hasStdin ? argv._.concat([txt]) : argv._ const getItemName = makeGetItemName(input) + const write = (info, _, done) => { + const itemName = getItemName(info) + const outPath = path.resolve(argv.outputDir, `${itemName}.${info.format}`) + + if (pipeToStdOut) { + str(info.body) + .pipe(process.stdout.on('drain', done)) + } else { + fs.writeFile(outPath, info.body, done) + } + } + const app = plotlyExporter.run({ input: input, + write: write, debug: DEBUG, parallelLimit: argv.parallelLimit, component: { @@ -56,19 +69,10 @@ getStdin().then((txt) => { app.on('after-export', (info) => { const itemName = getItemName(info) - const outPath = path.resolve(argv.outputDir, `${itemName}.${info.format}`) if (showLogs) { console.log(`exported ${itemName}, in ${info.processingTime} ms`) } - - if (pipeToStdOut) { - str(info.body).pipe(process.stdout) - } else { - fs.writeFile(outPath, info.body, (err) => { - if (err) console.warn(err) - }) - } }) app.on('export-error', (info) => { @@ -90,7 +94,7 @@ getStdin().then((txt) => { const msg = `\ndone with code ${info.code} in ${timeStr} - ${info.msg}` - if (info.code === 200) { + if (info.code === 0) { if (showLogs) { console.log(msg) } diff --git a/src/app/runner/coerce-opts.js b/src/app/runner/coerce-opts.js index 404b751b..26b49309 100644 --- a/src/app/runner/coerce-opts.js +++ b/src/app/runner/coerce-opts.js @@ -51,6 +51,10 @@ function coerceOpts (_opts = {}) { throw new Error('no valid input given') } + opts.write = typeof _opts.write === 'function' + ? _opts.write + : false + opts.input = input return opts diff --git a/src/app/runner/constants.js b/src/app/runner/constants.js index da138984..d4c9dc05 100644 --- a/src/app/runner/constants.js +++ b/src/app/runner/constants.js @@ -1,8 +1,9 @@ module.exports = { statusMsg: { - 200: 'all task(s) completed', + 0: 'all task(s) completed', + 1: 'failed or incomplete task(s)', 422: 'json parse error', - 500: 'incomplete task(s)' + 501: 'failed export' }, dflt: { parallelLimit: 4 diff --git a/src/app/runner/index.js b/src/app/runner/index.js index 2ea7e241..ff3d3665 100644 --- a/src/app/runner/index.js +++ b/src/app/runner/index.js @@ -34,6 +34,7 @@ function createApp (_opts) { win.on('closed', () => { win = null + index.destroy() }) createIndex(opts.component, opts, (_index) => { @@ -46,10 +47,6 @@ function createApp (_opts) { }) }) - process.on('exit', () => { - index.destroy() - }) - return app } diff --git a/src/app/runner/run.js b/src/app/runner/run.js index 677bf2d6..16d9c19a 100644 --- a/src/app/runner/run.js +++ b/src/app/runner/run.js @@ -1,4 +1,5 @@ const uuid = require('uuid/v4') +const isNumeric = require('fast-isnumeric') const parallelLimit = require('run-parallel-limit') const createTimer = require('../../util/create-timer') @@ -25,8 +26,9 @@ function run (app, win, ipcMain, opts) { const totalTimer = createTimer() let pending = input.length + let failed = 0 - const tasks = input.map((item, i) => (done) => { + const tasks = input.map((item, i) => (cb) => { const timer = createTimer() const id = uuid() @@ -38,15 +40,30 @@ function run (app, win, ipcMain, opts) { id: id } - const errorOut = (code) => { - fullInfo.msg = fullInfo.msg || STATUS_MSG[code] || '' + // task callback wrapper: + // - emits 'export-error' if given error code or error obj/msg + // - emits 'after-export' if no argument is given + const done = (err) => { + fullInfo.pending = --pending + fullInfo.processingTime = timer.end() + + if (err) { + failed++ - app.emit('export-error', Object.assign( - {code: code}, - fullInfo - )) + if (isNumeric(err)) { + fullInfo.code = err + } else { + fullInfo.code = 501 + fullInfo.error = err + } + + fullInfo.msg = fullInfo.msg || STATUS_MSG[fullInfo.code] || '' + app.emit('export-error', fullInfo) + } else { + app.emit('after-export', fullInfo) + } - return done() + cb() } // setup parse callback @@ -54,7 +71,7 @@ function run (app, win, ipcMain, opts) { Object.assign(fullInfo, parseInfo) if (errorCode) { - return errorOut(errorCode) + return done(errorCode) } win.webContents.send(comp.name, id, fullInfo, compOpts) @@ -65,14 +82,14 @@ function run (app, win, ipcMain, opts) { Object.assign(fullInfo, convertInfo) if (errorCode) { - return errorOut(errorCode) + return done(errorCode) } - fullInfo.pending = --pending - fullInfo.processingTime = timer.end() - - app.emit('after-export', fullInfo) - done() + if (opts.write) { + opts.write(fullInfo, compOpts, done) + } else { + done() + } } // setup convert on render message -> emit 'after-export' @@ -80,7 +97,7 @@ function run (app, win, ipcMain, opts) { Object.assign(fullInfo, renderInfo) if (errorCode) { - return errorOut(errorCode) + return done(errorCode) } comp._module.convert(fullInfo, compOpts, reply) @@ -91,14 +108,14 @@ function run (app, win, ipcMain, opts) { let body if (err) { - return errorOut(422) + return done(422) } if (typeof _body === 'string') { try { body = JSON.parse(_body) } catch (e) { - return errorOut(422) + return done(422) } } else { body = _body @@ -109,16 +126,18 @@ function run (app, win, ipcMain, opts) { }) parallelLimit(tasks, opts.parallelLimit, (err) => { - const code = (err || pending !== 0) ? 500 : 200 + const exitCode = (err || pending > 0 || failed > 0) ? 1 : 0 app.emit('after-export-all', { - code: code, - msg: STATUS_MSG[code], + code: exitCode, + msg: STATUS_MSG[exitCode], totalProcessingTime: totalTimer.end() }) // do not close window to look for unlogged console errors - if (!opts.debug) app.quit() + if (!opts.debug) { + app.exit(exitCode) + } }) } diff --git a/test/integration/plotly-graph-exporter_test.js b/test/integration/plotly-graph-exporter_test.js index 3d286719..4a582624 100644 --- a/test/integration/plotly-graph-exporter_test.js +++ b/test/integration/plotly-graph-exporter_test.js @@ -59,7 +59,7 @@ tap.test('should print export info on success', t => { const matches = [ /^exported fig/, - /done with code 200/ + /done with code 0/ ] let i = 0 diff --git a/test/unit/runner_test.js b/test/unit/runner_test.js index 749a166f..9f89a1b6 100644 --- a/test/unit/runner_test.js +++ b/test/unit/runner_test.js @@ -153,7 +153,7 @@ tap.test('run:', t => { cases.forEach(c => { t.test(`(case ${c}`, t => { const app = new EventEmitter() - app.quit = t.end + app.exit = t.end app.once('after-export', t.fail) app.once('export-error', (info) => { @@ -162,7 +162,7 @@ tap.test('run:', t => { }) app.once('after-export-all', (info) => { - t.equal(info.code, 500, 'code') + t.equal(info.code, 1, 'code') }) _run(app, c) @@ -178,7 +178,7 @@ tap.test('run:', t => { cases.forEach(c => { t.test(`(case ${c}`, t => { const app = new EventEmitter() - app.quit = t.end + app.exit = t.end app.once('export-error', t.fail) app.once('after-export', (info) => { @@ -190,7 +190,7 @@ tap.test('run:', t => { app.once('after-export-all', (info) => { t.equal(Object.keys(info).length, 3, '# of keys') - t.equal(info.code, 200, 'code') + t.equal(info.code, 0, 'code') t.equal(info.msg, 'all task(s) completed', 'msg') t.type(info.totalProcessingTime, 'number') }) @@ -218,7 +218,7 @@ tap.test('run:', t => { // return some dummy error code sinon.stub(opts.component._module, c).yields(555) - app.quit = () => { + app.exit = () => { opts.component._module[c].restore() t.end() } @@ -228,7 +228,7 @@ tap.test('run:', t => { }) app.once('after-export-all', (info) => { - t.equal(info.code, 500, 'code') + t.equal(info.code, 1, 'code') }) run(app, win, ipc, opts) @@ -247,7 +247,7 @@ tap.test('run:', t => { const app = new EventEmitter() app.once('after-export', t.fail) - app.quit = t.end + app.exit = t.end const opts = coerceOpts({ component: 'plotly-graph', @@ -259,7 +259,7 @@ tap.test('run:', t => { }) app.once('after-export-all', (info) => { - t.equal(info.code, 500, 'code') + t.equal(info.code, 1, 'code') }) run(app, win, ipc, opts) @@ -270,7 +270,7 @@ tap.test('run:', t => { t.test('should not quit in debug mode', t => { const app = new EventEmitter() - app.quit = t.fail + app.exit = t.fail const opts = coerceOpts({ component: 'plotly-graph',