diff --git a/dev_deps.ts b/dev_deps.ts index 1006343..d782738 100644 --- a/dev_deps.ts +++ b/dev_deps.ts @@ -4,4 +4,9 @@ export { assertObjectMatch, assertRejects, assertStringIncludes, -} from "https://deno.land/std@0.130.0/testing/asserts.ts"; +} from "https://deno.land/std@0.139.0/testing/asserts.ts"; + +export { + assertSpyCalls, + spy, +} from "https://deno.land/std@0.148.0/testing/mock.ts"; diff --git a/src/runtime/process.ts b/src/runtime/process.ts index be32c03..a1d6e81 100644 --- a/src/runtime/process.ts +++ b/src/runtime/process.ts @@ -4,6 +4,8 @@ import { ProcessError } from "./process_error.ts"; import { ProcessOutput } from "./process_output.ts"; import { $ } from "./shell.ts"; +export type RetryCallback = (error: ProcessError) => boolean | Promise; + export interface ProcessOptions { // deno-lint-ignore ban-types errorContext?: Function; @@ -18,13 +20,13 @@ export class Process implements Promise { #stdout: Deno.RunOptions["stdout"] = $.stdout; #stderr: Deno.RunOptions["stderr"] = $.stderr; #baseError: ProcessError; - #maxRetries = 0; #retries = 0; #timeout = 0; #timers: Array = []; #delay = 500; #throwErrors = true; #isKilled = false; + #shouldRetry?: RetryCallback; constructor(cmd: string, { errorContext }: ProcessOptions = {}) { this.#cmd = cmd; @@ -113,8 +115,11 @@ export class Process implements Promise { return this.#resolve().then(({ stderr }) => stderr); } - retry(retries: number): this { - this.#maxRetries = retries; + retry(retries: number | RetryCallback): this { + this.#shouldRetry = typeof retries === "number" + ? (error) => error.retries < retries + : (error) => retries(error); + return this; } @@ -205,14 +210,26 @@ export class Process implements Promise { }); if (!status.success) { - output = ProcessError.merge( + const error = ProcessError.merge( this.#baseError, new ProcessError(output), ); - if (this.#throwErrors || this.#retries < this.#maxRetries) { - throw output; + if (await this.#shouldRetry?.(error)) { + if (this.#delay) { + await new Promise((resolve) => + this.#timers.push(setTimeout(resolve, this.#delay)) + ); + } + this.#close(); + this.#proc = null; + this.#retries++; + + return this.#run(); + } else if (this.#throwErrors) { + throw error; } + output = error; } this.#close(); this.#closeTimer(); @@ -220,19 +237,6 @@ export class Process implements Promise { return output; } catch (error) { this.#close(); - - if (this.#retries < this.#maxRetries) { - this.#retries++; - this.#proc = null; - - if (this.#delay) { - await new Promise((resolve) => - this.#timers.push(setTimeout(resolve, this.#delay)) - ); - } - - return this.#run(); - } this.#closeTimer(); throw error; diff --git a/test.ts b/test.ts index b31fe24..09c81a2 100644 --- a/test.ts +++ b/test.ts @@ -4,7 +4,9 @@ import { assert, assertEquals, assertRejects, + assertSpyCalls, assertStringIncludes, + spy, } from "./dev_deps.ts"; import { $, $e, $o, cd, path, ProcessError } from "./mod.ts"; @@ -171,6 +173,17 @@ Deno.test({ }, }); +Deno.test({ + name: "$ should retry command with custom callback if it fails", + async fn() { + const retrySpy = spy(({ retries }: ProcessError) => retries < 3); + const result = await $`exit 1`.noThrow.delay(100).retry(retrySpy); + assertEquals(result.retries, 3); + assertEquals(result.status.code, 1); + assertSpyCalls(retrySpy, 4); + }, +}); + Deno.test({ name: "$ should resolve stdout promise", async fn() {