Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 36 additions & 24 deletions lib/ShellCommand.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,64 @@
const assert = require('node:assert/strict')
const { exec } = require('child_process')
const { spawn } = require('child_process')

// @todo #0:30m decompose
// runSilent go to ShellCommand
// run go to ShellCommandLogged
module.exports = class ShellCommand {

static withText (cmd) {
return new ShellCommand ({ cmd, exec })
return new ShellCommand ({ cmd, spawn })
}

constructor(o) {
this.cwd = o.cwd
this.cmd = o.cmd
this.exec = o.exec
this.spawn = o.spawn
}

async run () {
let cwdLabel = this.cwd ? ` (${this.cwd})` : ''
this.log(`${cwdLabel} > ${await this.print ()}`)
let result = await this.runSilent()
if (result) {
this.log(result.trim ())
}
return result
return this.runSilent({log_flow: true})
}

async runSilent () {
async runSilent (options = {}) {
const {log_flow} = options
if (global.FUZZ) {
// @todo #120:1h move to injected ShellCommandFuzz
return global.FUZZ_SHELL_REPLY
}
return new Promise((ok, fail) => {
const [the_cmd, tail] = this.cmd.split(' || true')
const is_always_ok = tail === ''
const [cmd, ...args] = the_cmd.split(' ')

return new Promise((resolve, reject) => {
this.exec(this.cmd, {cwd: this.cwd}, (error, stdout, stderr) => {
if (error) {
let label = error.message
if (label && /^Command failed: /i.test (label) && stdout) {
label = stdout
}
reject (new Error (label))
return
}
if (stderr) {
resolve(stderr)
return
const env = { ...process.env,
TERM: 'xterm-256color'
}
const o = {env}
const subprocess = this.spawn(cmd, args, o)

let stdout = '', stderr = ''
subprocess.stdout.on('data', (data) => {
if (log_flow) process.stdout.write(data)
stdout = stdout + data
})

subprocess.stderr.on('data', (data) => {
if (log_flow) process.stdout.write(data)
stderr = stderr + data
})

subprocess.on('exit', (code) => {
if (code === 0 || is_always_ok) {
ok(stdout.toString().trim())
} else {
fail(new Error(stderr.toString().trim()))
}
resolve (stdout.trim ())
})

subprocess.on('error', (error) => {
fail(new Error(error.toString().trim()))
})
})
}
Expand Down
36 changes: 32 additions & 4 deletions tests/ShellCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@ const ShellCommand = require ('../lib/ShellCommand')


describe('random input', () => {

const makeSpawnMock = (o = {}) => {

const {stdout, stderr, close} = o

return (command, args, options) => ({
stdout: {
on: (event, callback) => {
if (event === 'data') {
return callback(`${stdout}\n`)
}
},
},
stderr: {
on: (event, callback) => {
if (event === 'data') {
return callback(`${stderr}\n`)
}
},
},
on: (event, callback) => {
if (event === 'exit') {
return callback(o.exit)
}
},
})
}

it ('exit 1', async (t) => {
assert.rejects(async () => {
await ShellCommand.withText ('exit 1').run ()
Expand All @@ -15,13 +43,13 @@ describe('random input', () => {
})

it ('exec ok', async (t) => {
const exec = (cmd, o, callback) => callback ('', '', '')
assert.strictEqual(await (new ShellCommand ({cmd: 'git fetch', exec}).runSilent ()), '')
const spawn = makeSpawnMock ({stdout: '', stderr: '', exit: 0})
assert.strictEqual(await (new ShellCommand ({cmd: 'git fetch', spawn}).runSilent ()), '')
})

it ('exec fail', async (t) => {
const exec = (cmd, o, callback) => callback (new Error ('Command failed: '), 'not a repo', '')
assert.rejects(new ShellCommand ({cmd: 'git fetch', exec}).runSilent (), {message: 'not a repo'})
const spawn = makeSpawnMock ({stdout: '', stderr: 'not a repo', exit: -127})
assert.rejects(new ShellCommand ({cmd: 'git fetch', spawn}).runSilent (), {message: 'not a repo'})
})

it ('git status fuzz', async (t) => {
Expand Down
Loading