Commit b4d1fb8
Changed files (8)
src/core.ts
@@ -20,7 +20,15 @@ import { Readable, Writable } from 'node:stream'
import { inspect } from 'node:util'
import { chalk, which } from './goods.js'
import { log } from './log.js'
-import { exitCodeInfo, noop, psTree, quote, substitute } from './util.js'
+import {
+ Duration,
+ exitCodeInfo,
+ noop,
+ parseDuration,
+ psTree,
+ quote,
+ substitute,
+} from './util.js'
export type Shell = (
pieces: TemplateStringsArray,
@@ -113,7 +121,6 @@ export const $ = new Proxy<Shell & Options>(
type Resolve = (out: ProcessOutput) => void
type IO = StdioPipe | StdioNull
-export type Duration = number | `${number}s` | `${number}ms`
export class ProcessPromise extends Promise<ProcessOutput> {
child?: ChildProcess
@@ -300,15 +307,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
}
timeout(d: Duration, signal = 'SIGTERM') {
- if (typeof d == 'number') {
- this._timeout = d
- } else if (/\d+s/.test(d)) {
- this._timeout = +d.slice(0, -1) * 1000
- } else if (/\d+ms/.test(d)) {
- this._timeout = +d.slice(0, -2)
- } else {
- throw new Error(`Unknown timeout duration: "${d}".`)
- }
+ this._timeout = parseDuration(d)
this._timeoutSignal = signal
return this
}
src/experimental.ts
@@ -12,22 +12,48 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import assert from 'node:assert'
import { $ } from './core.js'
import { sleep } from './goods.js'
+import { Duration, parseDuration } from './util.js'
-// Retries a command a few times. Will return after the first
-// successful attempt, or will throw after specifies attempts count.
-export function retry(count = 5, delay = 0) {
- return async function (cmd: TemplateStringsArray, ...args: any[]) {
- while (count-- > 0)
- try {
- return await $(cmd, ...args)
- } catch (p) {
- if (count === 0) throw p
- if (delay) await sleep(delay)
+export async function retry<T>(count: number, callback: () => T): Promise<T>
+export async function retry<T>(
+ count: number,
+ duration: Duration,
+ callback?: () => T
+): Promise<T>
+export async function retry<T>(
+ count: number,
+ a: Duration | (() => T),
+ b?: () => T
+): Promise<T> {
+ const total = count
+ let cb: () => T
+ let delay = 0
+ if (typeof a == 'function') {
+ cb = a
+ } else {
+ delay = parseDuration(a)
+ assert(b)
+ cb = b
+ }
+ while (count-- > 0) {
+ try {
+ return await cb()
+ } catch (err) {
+ if ($.verbose) {
+ console.error(
+ chalk.bgRed.white(' FAIL '),
+ `Attempt: ${total - count}/${total}` +
+ (delay > 0 ? `; next in ${delay}ms` : '')
+ )
}
- return
+ if (count == 0) throw err
+ if (delay) await sleep(delay)
+ }
}
+ throw 'unreachable'
}
/**
src/index.ts
@@ -18,7 +18,6 @@ export {
$,
Shell,
Options,
- Duration,
ProcessPromise,
ProcessOutput,
within,
@@ -43,6 +42,8 @@ export {
export { log, formatCmd, LogEntry } from './log.js'
+export { Duration } from './util.js'
+
/**
* @deprecated Use $.nothrow() instead.
*/
src/repl.ts
@@ -20,7 +20,7 @@ import chalk from 'chalk'
import { ProcessOutput } from './core.js'
export function startRepl() {
- (global as any).ZX_VERBOSE = false
+ ;(global as any).ZX_VERBOSE = false
const r = repl.start({
prompt: chalk.greenBright.bold('❯ '),
useGlobal: true,
src/util.ts
@@ -120,3 +120,17 @@ export function stdin() {
})()
}
}
+
+export type Duration = number | `${number}s` | `${number}ms`
+
+export function parseDuration(d: Duration) {
+ if (typeof d == 'number') {
+ return d
+ } else if (/\d+s/.test(d)) {
+ return +d.slice(0, -1) * 1000
+ } else if (/\d+ms/.test(d)) {
+ return +d.slice(0, -2)
+ } else {
+ throw new Error(`Unknown duration: "${d}".`)
+ }
+}
test/experimental.test.js
@@ -14,31 +14,38 @@
import { suite } from 'uvu'
import * as assert from 'uvu/assert'
-import { retry } from '../build/experimental.js'
import '../build/globals.js'
const test = suite('experimental')
$.verbose = false
+function zx(script) {
+ return $`node build/cli.js --experimental --eval ${script}`
+}
+
test('retry works', async () => {
- let exitCode = 0
- let now = Date.now()
- try {
- await retry(5, 50)`exit 123`
- } catch (p) {
- exitCode = p.exitCode
- }
- assert.is(exitCode, 123)
+ const now = Date.now()
+ let p = await zx(`
+ try {
+ await retry(5, 50, () => $\`exit 123\`)
+ } catch (e) {
+ echo('exitCode:', e.exitCode)
+ }
+ await retry(5, () => $\`exit 0\`)
+ echo('success')
+`)
+ assert.match(p.toString(), 'exitCode: 123')
+ assert.match(p.toString(), 'success')
assert.ok(Date.now() >= now + 50 * (5 - 1))
})
test('spinner works', async () => {
- let out = await $`node build/cli.js --experimental <<'EOF'
- let stop = startSpinner('waiting')
- await sleep(1000)
- stop()
-EOF`
+ let out = await zx(`
+ let stop = startSpinner('waiting')
+ await sleep(1000)
+ stop()
+ `)
assert.match(out.stderr, 'waiting')
})
test/index.test.js
@@ -486,10 +486,4 @@ test('timeout() expiration works', async () => {
assert.is(signal, undefined)
})
-test('timeout() with string durations works', async () => {
- assert.is($`:`.timeout('2s')._timeout, 2000)
- assert.is($`:`.timeout('500ms')._timeout, 500)
- assert.throws(() => $`:`.timeout('100'))
-})
-
test.run()
test/util.test.js
@@ -14,7 +14,14 @@
import { suite } from 'uvu'
import * as assert from 'uvu/assert'
-import { exitCodeInfo, randomId, noop, isString, quote } from '../build/util.js'
+import {
+ exitCodeInfo,
+ randomId,
+ noop,
+ isString,
+ quote,
+ parseDuration,
+} from '../build/util.js'
const test = suite('util')
@@ -43,4 +50,11 @@ test('quote()', () => {
assert.ok(quote(`'\f\n\r\t\v\0`) === `$'\\'\\f\\n\\r\\t\\v\\0'`)
})
+test('duration parsing works', () => {
+ assert.is(parseDuration(1000), 1000)
+ assert.is(parseDuration('2s'), 2000)
+ assert.is(parseDuration('500ms'), 500)
+ assert.throws(() => parseDuration('100'))
+})
+
test.run()