From 58e72c7f215c2222b586028a432a87693bc21abc Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Mon, 27 Sep 2021 12:49:04 -0700 Subject: [PATCH 1/5] fix: exit with non-zero code when test command fails --- src/Helpers.js | 6 +--- src/sde-build.js | 12 ++++++-- src/sde-compare.js | 77 ++++++++++++++++++++++++---------------------- src/sde-compile.js | 2 +- src/sde-test.js | 44 ++++++++++++++------------ 5 files changed, 77 insertions(+), 64 deletions(-) diff --git a/src/Helpers.js b/src/Helpers.js index 12a6c826..8c9897b4 100644 --- a/src/Helpers.js +++ b/src/Helpers.js @@ -206,11 +206,7 @@ export let ensureDir = (dir, defaultDir, modelDirname) => { return dirName } export let fileExists = pathname => { - let exists = fs.existsSync(pathname) - if (!exists) { - console.error(`${pathname} not found`) - } - return exists + return fs.existsSync(pathname) } export let linkCSourceFiles = (modelDirname, buildDirname) => { let cDirname = path.join(new URL('.', import.meta.url).pathname, 'c') diff --git a/src/sde-build.js b/src/sde-build.js index 26c7c337..a43da371 100644 --- a/src/sde-build.js +++ b/src/sde-build.js @@ -19,9 +19,15 @@ export let handler = argv => { build(argv.model, argv) } export let build = async (model, opts) => { - opts.genc = true - await generate(model, opts) - compile(model, opts) + try { + opts.genc = true + await generate(model, opts) + compile(model, opts) + } catch (e) { + // Exit with a non-zero error code if any step failed + console.error(`ERROR: ${e.message}\n`) + process.exit(1) + } } export default { diff --git a/src/sde-compare.js b/src/sde-compare.js index f440b3cd..94bc8de1 100644 --- a/src/sde-compare.js +++ b/src/sde-compare.js @@ -28,50 +28,55 @@ export let handler = argv => { compare(argv.vensimlog, argv.sdelog, argv) } export let compare = async (vensimfile, sdefile, opts) => { + if (!fileExists(vensimfile)) { + throw new Error(`Vensim DAT file not found: ${vensimfile}`) + } + if (!fileExists(sdefile)) { + throw new Error(`SDEverywhere DAT file not found: ${sdefile}`) + } + + let vensimLog = await readDat(vensimfile) + let sdeLog = await readDat(sdefile) + + if (vensimLog.size === 0) { + throw new Error(`Vensim DAT file did not contain data: ${vensimfile}`) + } + if (sdeLog.size === 0) { + throw new Error(`SDEverywhere DAT file did not contain data: ${sdefile}`) + } + if (opts.precision) { ε = opts.precision } - if (fileExists(vensimfile) && fileExists(sdefile)) { - let vensimLog = await readDat(vensimfile) - let sdeLog = await readDat(sdefile) - if (vensimLog.size > 0 && sdeLog.size > 0) { - let noDATDifference = true - for (let varName of vensimLog.keys()) { - let sdeValues = sdeLog.get(varName) - // Ignore variables that are not found in the SDE log file. - if (sdeValues && (!opts.name || varName === opts.name)) { - let vensimValue = undefined - let vensimValues = vensimLog.get(varName) - // Filter on time t, the key in the values list. - for (let t of vensimValues.keys()) { - if (!opts.times || R.find(time => isEqual(time, t), opts.times)) { - // In Vensim log files, const vars only have one value at the initial time. - if (vensimValues.size > 1 || !vensimValue) { - vensimValue = vensimValues.get(t) - } - let sdeValue = sdeValues.get(t) - let diff = difference(sdeValue, vensimValue) - if (diff > ε) { - let diffPct = (diff * 100).toFixed(6) - pr(`${varName} time=${t.toFixed(2)} vensim=${vensimValue} sde=${sdeValue} diff=${diffPct}%`) - noDATDifference = false - } - } + + let noDATDifference = true + for (let varName of vensimLog.keys()) { + let sdeValues = sdeLog.get(varName) + // Ignore variables that are not found in the SDE log file. + if (sdeValues && (!opts.name || varName === opts.name)) { + let vensimValue = undefined + let vensimValues = vensimLog.get(varName) + // Filter on time t, the key in the values list. + for (let t of vensimValues.keys()) { + if (!opts.times || R.find(time => isEqual(time, t), opts.times)) { + // In Vensim log files, const vars only have one value at the initial time. + if (vensimValues.size > 1 || !vensimValue) { + vensimValue = vensimValues.get(t) + } + let sdeValue = sdeValues.get(t) + let diff = difference(sdeValue, vensimValue) + if (diff > ε) { + let diffPct = (diff * 100).toFixed(6) + pr(`${varName} time=${t.toFixed(2)} vensim=${vensimValue} sde=${sdeValue} diff=${diffPct}%`) + noDATDifference = false } } } - if (noDATDifference) { - pr(`Data were the same for ${vensimfile} and ${sdefile}`) - } - } else { - if (vensimLog.size === 0) { - console.error(`${vensimfile} did not contain Vensim data`) - } - if (sdeLog.size === 0) { - console.error(`${sdefile} did not contain Vensim data`) - } } } + if (noDATDifference) { + pr(`Data were the same for ${vensimfile} and ${sdefile}`) + } } let isZero = value => { return Math.abs(value) < ε diff --git a/src/sde-compile.js b/src/sde-compile.js index d36709e5..8d06cbb7 100644 --- a/src/sde-compile.js +++ b/src/sde-compile.js @@ -14,7 +14,7 @@ export let handler = argv => { compile(argv.model, argv) } export let compile = (model, opts) => { - let { modelDirname, modelName, modelPathname } = modelPathProps(model) + let { modelDirname, modelName } = modelPathProps(model) // Ensure the build directory exists. let buildDirname = buildDir(opts.builddir, modelDirname) // Link SDEverywhere C source files into the build directory. diff --git a/src/sde-test.js b/src/sde-test.js index f9f52e92..2fbaaf78 100644 --- a/src/sde-test.js +++ b/src/sde-test.js @@ -32,26 +32,32 @@ export let handler = argv => { test(argv.model, argv) } export let test = async (model, opts) => { - // Run the model and save output to an SDE log file. - let { modelDirname, modelName } = modelPathProps(model) - let logPathname - if (opts.outfile) { - logPathname = opts.outfile - } else { - let outputDirname = outputDir(opts.outfile, modelDirname) - logPathname = path.join(outputDirname, `${modelName}.txt`) - opts.outfile = logPathname + try { + // Run the model and save output to an SDE log file. + let { modelDirname, modelName } = modelPathProps(model) + let logPathname + if (opts.outfile) { + logPathname = opts.outfile + } else { + let outputDirname = outputDir(opts.outfile, modelDirname) + logPathname = path.join(outputDirname, `${modelName}.txt`) + opts.outfile = logPathname + } + await run(model, opts) + // Convert the TSV log file to a DAT file in the same directory. + opts.dat = true + await log(logPathname, opts) + // Assume there is a Vensim-created DAT file named {modelName}.dat in the model directory. + // Compare it to the SDE DAT file. + let vensimPathname = path.join(modelDirname, `${modelName}.dat`) + let p = path.parse(logPathname) + let sdePathname = path.format({ dir: p.dir, name: p.name, ext: '.dat' }) + await compare(vensimPathname, sdePathname, opts) + } catch (e) { + // Exit with a non-zero error code if any step failed + console.error(`ERROR: ${e.message}\n`) + process.exit(1) } - await run(model, opts) - // Convert the TSV log file to a DAT file in the same directory. - opts.dat = true - await log(logPathname, opts) - // Assume there is a Vensim-created DAT file named {modelName}.dat in the model directory. - // Compare it to the SDE DAT file. - let vensimPathname = path.join(modelDirname, `${modelName}.dat`) - let p = path.parse(logPathname) - let sdePathname = path.format({ dir: p.dir, name: p.name, ext: '.dat' }) - await compare(vensimPathname, sdePathname, opts) } export default { From deba1859e1127e3ee7a3416fbc7695817fcc8ede Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Mon, 27 Sep 2021 13:17:31 -0700 Subject: [PATCH 2/5] fix: check exit code of exec'd command a different way, and add more error handling --- src/Helpers.js | 7 ++++--- src/sde-exec.js | 7 +++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Helpers.js b/src/Helpers.js index 8c9897b4..e7019607 100644 --- a/src/Helpers.js +++ b/src/Helpers.js @@ -245,9 +245,10 @@ export let execCmd = cmd => { // Run a command line silently in the "sh" shell. Print error output on error. let exitCode = 0 let result = sh.exec(cmd, { silent: true }) - if (sh.error()) { - console.log(result.stderr) - exitCode = 1 + if (result.code !== 0) { + console.log(result.stdout) + console.error(result.stderr) + exitCode = result.code } return exitCode } diff --git a/src/sde-exec.js b/src/sde-exec.js index 659fc575..54e6ae5c 100644 --- a/src/sde-exec.js +++ b/src/sde-exec.js @@ -1,6 +1,6 @@ import path from 'path' import moment from 'moment' -import { modelPathProps, buildDir, outputDir, execCmd } from './Helpers.js' +import { modelPathProps, buildDir, outputDir, execCmd, fileExists } from './Helpers.js' export let command = 'exec [options] ' export let describe = 'execute the model and capture its output to a file' @@ -20,7 +20,7 @@ export let handler = argv => { exec(argv.model, argv) } export let exec = (model, opts) => { - let { modelDirname, modelName, modelPathname } = modelPathProps(model) + let { modelDirname, modelName } = modelPathProps(model) // Ensure the build and output directories exist. let buildDirname = buildDir(opts.builddir, modelDirname) let outputDirname = outputDir(opts.outfile, modelDirname) @@ -35,6 +35,9 @@ export let exec = (model, opts) => { let exitCode = execCmd(`${modelCmd} >${outputPathname}`) if (exitCode > 0) { process.exit(exitCode) + } else if (!fileExists(outputPathname)) { + console.error(`ERROR: Failed to write model output to ${outputPathname}`) + process.exit(1) } return 0 } From 5f243222aa95c831da483b97c5e031c5466208e8 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Mon, 27 Sep 2021 13:35:54 -0700 Subject: [PATCH 3/5] Revert "fix: check exit code of exec'd command a different way, and add more error handling" This reverts commit deba1859e1127e3ee7a3416fbc7695817fcc8ede. --- src/Helpers.js | 7 +++---- src/sde-exec.js | 7 ++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Helpers.js b/src/Helpers.js index e7019607..8c9897b4 100644 --- a/src/Helpers.js +++ b/src/Helpers.js @@ -245,10 +245,9 @@ export let execCmd = cmd => { // Run a command line silently in the "sh" shell. Print error output on error. let exitCode = 0 let result = sh.exec(cmd, { silent: true }) - if (result.code !== 0) { - console.log(result.stdout) - console.error(result.stderr) - exitCode = result.code + if (sh.error()) { + console.log(result.stderr) + exitCode = 1 } return exitCode } diff --git a/src/sde-exec.js b/src/sde-exec.js index 54e6ae5c..659fc575 100644 --- a/src/sde-exec.js +++ b/src/sde-exec.js @@ -1,6 +1,6 @@ import path from 'path' import moment from 'moment' -import { modelPathProps, buildDir, outputDir, execCmd, fileExists } from './Helpers.js' +import { modelPathProps, buildDir, outputDir, execCmd } from './Helpers.js' export let command = 'exec [options] ' export let describe = 'execute the model and capture its output to a file' @@ -20,7 +20,7 @@ export let handler = argv => { exec(argv.model, argv) } export let exec = (model, opts) => { - let { modelDirname, modelName } = modelPathProps(model) + let { modelDirname, modelName, modelPathname } = modelPathProps(model) // Ensure the build and output directories exist. let buildDirname = buildDir(opts.builddir, modelDirname) let outputDirname = outputDir(opts.outfile, modelDirname) @@ -35,9 +35,6 @@ export let exec = (model, opts) => { let exitCode = execCmd(`${modelCmd} >${outputPathname}`) if (exitCode > 0) { process.exit(exitCode) - } else if (!fileExists(outputPathname)) { - console.error(`ERROR: Failed to write model output to ${outputPathname}`) - process.exit(1) } return 0 } From 6c23d50e2454e2462973f8252ebdd7e05df99b49 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Mon, 27 Sep 2021 13:38:04 -0700 Subject: [PATCH 4/5] fix: use simpler check for exec result code --- src/Helpers.js | 6 +++--- src/sde-exec.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Helpers.js b/src/Helpers.js index 8c9897b4..470ec981 100644 --- a/src/Helpers.js +++ b/src/Helpers.js @@ -245,9 +245,9 @@ export let execCmd = cmd => { // Run a command line silently in the "sh" shell. Print error output on error. let exitCode = 0 let result = sh.exec(cmd, { silent: true }) - if (sh.error()) { - console.log(result.stderr) - exitCode = 1 + if (result.code !== 0) { + console.error(result.stderr) + exitCode = result.code } return exitCode } diff --git a/src/sde-exec.js b/src/sde-exec.js index 659fc575..6dbe57b8 100644 --- a/src/sde-exec.js +++ b/src/sde-exec.js @@ -20,7 +20,7 @@ export let handler = argv => { exec(argv.model, argv) } export let exec = (model, opts) => { - let { modelDirname, modelName, modelPathname } = modelPathProps(model) + let { modelDirname, modelName } = modelPathProps(model) // Ensure the build and output directories exist. let buildDirname = buildDir(opts.builddir, modelDirname) let outputDirname = outputDir(opts.outfile, modelDirname) From f9588546f2d07d455f359a3e14eb288ffea8d6ad Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Mon, 27 Sep 2021 13:38:27 -0700 Subject: [PATCH 5/5] fix: wait for 'finish' event before resolving promise --- src/sde-log.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/sde-log.js b/src/sde-log.js index ec7473da..70d0ac90 100644 --- a/src/sde-log.js +++ b/src/sde-log.js @@ -57,6 +57,11 @@ let exportDat = async (logPathname, datPathname) => { let stream = fs.createWriteStream(datPathname, 'utf8') let iVarKey = 0 let finished = () => {} + stream.on('finish', () => { + // Resolve the promise after `stream.end()` has been called and all data has been flushed + // to the output file + finished() + }) let writeContinuation = () => { while (iVarKey < varKeys.length) { let varKey = varKeys[iVarKey++] @@ -74,8 +79,9 @@ let exportDat = async (logPathname, datPathname) => { } } } + // End the stream after the last line is emitted; this will trigger a `finish` event after + // all data has been flushed to the output file stream.end() - finished() } return new Promise(resolve => { // Start the write on the next event loop tick so we can return immediately.