mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Cancel running subtests when their parent test times out
node:test changes, matching Node 24.22:
- When a test's { timeout } elapses, its subtests that are still running
are cancelled, the innermost first. Each reports its duration so far and
fails with "test did not finish before its parent and was cancelled",
before the parent's result.
- A cancelled subtest's body keeps running unobserved: when it settles it
reports nothing and leaves the runner's state alone. Before, it reported
its result after the run's summary.
The timeout-subtests fixture compares the results, counts and failing
section with Node, plain and with SCRIPTC_SAN=1.
This commit is contained in:
@@ -3,3 +3,4 @@
|
||||
- **TestContext values:** `t.filePath` is the entry file's path when the program was compiled. `t.attempt` is `0`, since a test runs once. `t.passed` is `false` while the body runs and the test's outcome in its `afterEach` and `t.after` hooks. `t.workerId` is `Number(process.env.NODE_TEST_WORKER_ID) || undefined`, as in Node. `t.tags` is `[]`, since the `tags` test option throws `SC2020`; the array is not frozen as Node's is.
|
||||
- **Suite and hook contexts:** a `describe` body may take its `SuiteContext`, which has `name`, `fullName`, `filePath`, `attempt`, `passed` and `diagnostic`. Hooks may take their context, as in Node: the suite's for `before` and `after`, the test's for `beforeEach`, `afterEach`, `t.before` and `t.after`. The `SuiteContext` `signal` and `log` members, and hook `done` callbacks, throw `SC2020`.
|
||||
- **Module-level hooks:** `before` hooks outside any suite run at their `before()` call, and the first top-level test waits for the async ones; `after` hooks outside any suite run after the last top-level test. Both get the root's context, named `<root>`. As in Node, a throwing root `before` fails each top-level test with its error, without running its body, and cancels the tests and suites inside each top-level suite. A throwing root `after` reports the entry file as a failed result, and the exit code is unchanged.
|
||||
- **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node. Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports. A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started.
|
||||
|
||||
@@ -125,6 +125,7 @@ struct ScrTestCtx {
|
||||
struct ScrMockTimers *mock_timers; /* t.mock.timers, made on first use; freed at exit */
|
||||
bool passed; /* t.passed: set when the body passes, as Node's pass() does */
|
||||
bool listed; /* a suite listed in the failing section (a failing root before) */
|
||||
double start_ms; /* when the test started running; NaN before */
|
||||
};
|
||||
|
||||
static SCR_TL ScrTestCtx *scr_test_root = NULL;
|
||||
@@ -230,6 +231,7 @@ static ScrTestCtx *scr_test_node_new(ScrStr *name /*borrowed*/, int mode,
|
||||
t->plan = -1;
|
||||
t->timeout = INFINITY;
|
||||
t->concurrency = 1;
|
||||
t->start_ms = NAN;
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_testctx_live++;
|
||||
#endif
|
||||
@@ -597,6 +599,23 @@ static void scr_test_cancel_unrun(ScrTestCtx *c) {
|
||||
scr_test_report_result(c);
|
||||
}
|
||||
|
||||
/* A test that timed out (Node): its subtests that are still running are
|
||||
* cancelled, innermost first, with their durations so far. Their bodies
|
||||
* keep running unobserved and report nothing when they settle. */
|
||||
static void scr_test_cancel_running(ScrTestCtx *t) {
|
||||
for (size_t i = 0; i < t->nchildren; i++) {
|
||||
ScrTestCtx *c = t->children[i];
|
||||
if (c->is_suite || c->state != SCR_TS_PENDING || isnan(c->start_ms)) continue;
|
||||
scr_test_cancel_running(c);
|
||||
static const char msg[] = "test did not finish before its parent and was cancelled";
|
||||
scr_throw_str(scr_str_new(msg, sizeof msg - 1));
|
||||
c->err = scr_exc_take();
|
||||
c->dur_ms = scr_now_ms() - c->start_ms;
|
||||
c->state = SCR_TS_CANCELLED;
|
||||
scr_test_report_result(c);
|
||||
}
|
||||
}
|
||||
|
||||
static void scr_test_fail_unrun(ScrTestCtx *c, ScrCaught *err) {
|
||||
double start = scr_now_ms();
|
||||
c->err = scr_caught_retain(err);
|
||||
@@ -689,6 +708,7 @@ static void scr_test_run_node(ScrTestCtx *t) {
|
||||
* afterEach chain. */
|
||||
ScrTestCtx *prev_parent = scr_test_parent_get();
|
||||
scr_test_parent_set(t);
|
||||
t->start_ms = start;
|
||||
if (t->concurrency > 1) scr_test_defer++;
|
||||
bool hooked = scr_test_run_each_hooks(t, SCR_HOOK_BEFORE_EACH, true);
|
||||
/* A concurrent subtest resumes after its beforeEach hooks as many turns
|
||||
@@ -703,10 +723,18 @@ static void scr_test_run_node(ScrTestCtx *t) {
|
||||
failed = true;
|
||||
} else if (t->cb) {
|
||||
scr_test_call(t->cb, t->flags, t, t);
|
||||
if (t->state != SCR_TS_PENDING) {
|
||||
/* Its parent timed out and cancelled it while the body ran: the
|
||||
* result is reported, and the runner has moved on. */
|
||||
if (scr_exc_pending()) scr_exc_clear();
|
||||
if (t->concurrency > 1) scr_test_defer--;
|
||||
return;
|
||||
}
|
||||
if (scr_exc_pending()) {
|
||||
t->err = scr_exc_take();
|
||||
failed = true;
|
||||
}
|
||||
if (failed && t->timed_out) scr_test_cancel_running(t);
|
||||
}
|
||||
/* Children a passing body registered without running (bare test() in
|
||||
* the body): run them now, before the parent completes — Node holds
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// @exit: 1
|
||||
// A test that times out cancels its subtests that are still running, the
|
||||
// innermost first, with "test did not finish before its parent and was
|
||||
// cancelled". Subtests that finished keep their results, and the
|
||||
// cancelled bodies report nothing when they settle later.
|
||||
import { test } from "node:test";
|
||||
|
||||
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
test("parent", { timeout: 40 }, async (t) => {
|
||||
await t.test("finished first", () => {});
|
||||
await t.test("running", async (t) => {
|
||||
await t.test("inner done", () => {});
|
||||
await t.test("inner running", async () => {
|
||||
await sleep(300);
|
||||
throw new Error("never reported");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("next", async () => {
|
||||
await sleep(400);
|
||||
});
|
||||
Reference in New Issue
Block a user