diff options
-rw-r--r-- | cli/js/40_testing.js | 335 | ||||
-rw-r--r-- | cli/lsp/testing/execution.rs | 24 | ||||
-rw-r--r-- | cli/tests/integration/test_tests.rs | 21 | ||||
-rw-r--r-- | cli/tests/testdata/test/ops_sanitizer_missing_details.out | 7 | ||||
-rw-r--r-- | cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests.out | 14 | ||||
-rw-r--r-- | cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests_no_trace.out | 14 | ||||
-rw-r--r-- | cli/tests/testdata/test/ops_sanitizer_unstable.out | 7 | ||||
-rw-r--r-- | cli/tests/testdata/test/parallel_output.out | 53 | ||||
-rw-r--r-- | cli/tests/testdata/test/resource_sanitizer.out | 9 | ||||
-rw-r--r-- | cli/tests/testdata/test/sigint_with_hanging_test.out | 10 | ||||
-rw-r--r-- | cli/tests/testdata/test/sigint_with_hanging_test.ts | 15 | ||||
-rw-r--r-- | cli/tests/testdata/test/steps/failing_steps.out | 61 | ||||
-rw-r--r-- | cli/tests/testdata/test/steps/invalid_usage.out | 110 | ||||
-rw-r--r-- | cli/tests/testdata/test/steps/invalid_usage.ts | 2 | ||||
-rw-r--r-- | cli/tests/unit_node/process_test.ts | 84 | ||||
-rw-r--r-- | cli/tools/test.rs | 457 |
16 files changed, 644 insertions, 579 deletions
diff --git a/cli/js/40_testing.js b/cli/js/40_testing.js index bcb5990a9..74fbb8da3 100644 --- a/cli/js/40_testing.js +++ b/cli/js/40_testing.js @@ -142,7 +142,8 @@ function assertOps(fn) { const pre = core.metrics(); const preTraces = new Map(core.opCallTraces); try { - await fn(desc); + const innerResult = await fn(desc); + if (innerResult) return innerResult; } finally { // Defer until next event loop turn - that way timeouts and intervals // cleared can actually be removed from resource table, otherwise @@ -150,9 +151,6 @@ function assertOps(fn) { await opSanitizerDelay(); await opSanitizerDelay(); } - - if (shouldSkipSanitizers(desc)) return; - const post = core.metrics(); const postTraces = new Map(core.opCallTraces); @@ -161,7 +159,7 @@ function assertOps(fn) { const dispatchedDiff = post.opsDispatchedAsync - pre.opsDispatchedAsync; const completedDiff = post.opsCompletedAsync - pre.opsCompletedAsync; - if (dispatchedDiff === completedDiff) return; + if (dispatchedDiff === completedDiff) return null; const details = []; for (const key in post.ops) { @@ -215,19 +213,7 @@ function assertOps(fn) { ); } } - - let msg = `Test case is leaking async ops. - - - ${ArrayPrototypeJoin(details, "\n - ")}`; - - if (!core.isOpCallTracingEnabled()) { - msg += - `\n\nTo get more details where ops were leaked, run again with --trace-ops flag.`; - } else { - msg += "\n"; - } - - throw assert(false, msg); + return { failed: { leakedOps: [details, core.isOpCallTracingEnabled()] } }; }; } @@ -372,12 +358,8 @@ function assertResources(fn) { /** @param desc {TestDescription | TestStepDescription} */ return async function resourceSanitizer(desc) { const pre = core.resources(); - await fn(desc); - - if (shouldSkipSanitizers(desc)) { - return; - } - + const innerResult = await fn(desc); + if (innerResult) return innerResult; const post = core.resources(); const allResources = new Set([ @@ -404,14 +386,10 @@ function assertResources(fn) { ArrayPrototypePush(details, detail); } } - - const message = `Test case is leaking ${details.length} resource${ - details.length === 1 ? "" : "s" - }: - - - ${details.join("\n - ")} -`; - assert(details.length === 0, message); + if (details.length == 0) { + return null; + } + return { failed: { leakedResources: details } }; }; } @@ -429,9 +407,8 @@ function assertExit(fn, isTest) { }); try { - await fn(...new SafeArrayIterator(params)); - } catch (err) { - throw err; + const innerResult = await fn(...new SafeArrayIterator(params)); + if (innerResult) return innerResult; } finally { setExitHandler(null); } @@ -441,79 +418,52 @@ function assertExit(fn, isTest) { function assertTestStepScopes(fn) { /** @param desc {TestDescription | TestStepDescription} */ return async function testStepSanitizer(desc) { - preValidation(); - // only report waiting after pre-validation - if (canStreamReporting(desc) && "parent" in desc) { - stepReportWait(desc); - } - await fn(MapPrototypeGet(testStates, desc.id).context); - testStepPostValidation(desc); - - function preValidation() { - const runningStepDescs = getRunningStepDescs(); - const runningStepDescsWithSanitizers = ArrayPrototypeFilter( - runningStepDescs, - (d) => usesSanitizer(d), - ); - - if (runningStepDescsWithSanitizers.length > 0) { - throw new Error( - "Cannot start test step while another test step with sanitizers is running.\n" + - runningStepDescsWithSanitizers - .map((d) => ` * ${getFullName(d)}`) - .join("\n"), - ); - } - - if (usesSanitizer(desc) && runningStepDescs.length > 0) { - throw new Error( - "Cannot start test step with sanitizers while another test step is running.\n" + - runningStepDescs.map((d) => ` * ${getFullName(d)}`).join("\n"), - ); - } - - function getRunningStepDescs() { - const results = []; - let childDesc = desc; - while (childDesc.parent != null) { - const state = MapPrototypeGet(testStates, childDesc.parent.id); - for (const siblingDesc of state.children) { - if (siblingDesc.id == childDesc.id) { - continue; - } - const siblingState = MapPrototypeGet(testStates, siblingDesc.id); - if (!siblingState.finalized) { - ArrayPrototypePush(results, siblingDesc); - } + function getRunningStepDescs() { + const results = []; + let childDesc = desc; + while (childDesc.parent != null) { + const state = MapPrototypeGet(testStates, childDesc.parent.id); + for (const siblingDesc of state.children) { + if (siblingDesc.id == childDesc.id) { + continue; + } + const siblingState = MapPrototypeGet(testStates, siblingDesc.id); + if (!siblingState.completed) { + ArrayPrototypePush(results, siblingDesc); } - childDesc = childDesc.parent; } - return results; + childDesc = childDesc.parent; } + return results; } - }; -} + const runningStepDescs = getRunningStepDescs(); + const runningStepDescsWithSanitizers = ArrayPrototypeFilter( + runningStepDescs, + (d) => usesSanitizer(d), + ); -function testStepPostValidation(desc) { - // check for any running steps - for (const childDesc of MapPrototypeGet(testStates, desc.id).children) { - if (MapPrototypeGet(testStates, childDesc.id).status == "pending") { - throw new Error( - "There were still test steps running after the current scope finished execution. Ensure all steps are awaited (ex. `await t.step(...)`).", - ); + if (runningStepDescsWithSanitizers.length > 0) { + return { + failed: { + overlapsWithSanitizers: runningStepDescsWithSanitizers.map( + getFullName, + ), + }, + }; } - } - // check if an ancestor already completed - let currentDesc = desc.parent; - while (currentDesc != null) { - if (MapPrototypeGet(testStates, currentDesc.id).finalized) { - throw new Error( - "Parent scope completed before test step finished execution. Ensure all steps are awaited (ex. `await t.step(...)`).", - ); + if (usesSanitizer(desc) && runningStepDescs.length > 0) { + return { + failed: { hasSanitizersAndOverlaps: runningStepDescs.map(getFullName) }, + }; } - currentDesc = currentDesc.parent; - } + await fn(MapPrototypeGet(testStates, desc.id).context); + for (const childDesc of MapPrototypeGet(testStates, desc.id).children) { + if (!MapPrototypeGet(testStates, childDesc.id).completed) { + return { failed: "incompleteSteps" }; + } + } + }; } function pledgePermissions(permissions) { @@ -573,18 +523,14 @@ function withPermissions(fn, permissions) { * @typedef {{ * context: TestContext, * children: TestStepDescription[], - * finalized: boolean, + * completed: boolean, * }} TestState * * @typedef {{ * context: TestContext, * children: TestStepDescription[], - * finalized: boolean, - * status: "pending" | "ok" | ""failed" | ignored", - * error: unknown, - * elapsed: number | null, - * reportedWait: boolean, - * reportedResult: boolean, + * completed: boolean, + * failed: boolean, * }} TestStepState * * @typedef {{ @@ -701,13 +647,6 @@ function test( // Delete this prop in case the user passed it. It's used to detect steps. delete testDesc.parent; - testDesc.fn = wrapTestFnWithSanitizers(testDesc.fn, testDesc); - if (testDesc.permissions) { - testDesc.fn = withPermissions( - testDesc.fn, - testDesc.permissions, - ); - } testDesc.origin = getTestOrigin(); const jsError = core.destructureError(new Error()); testDesc.location = { @@ -724,7 +663,7 @@ function test( MapPrototypeSet(testStates, testDesc.id, { context: createTestContext(testDesc), children: [], - finalized: false, + completed: false, }); } @@ -832,28 +771,20 @@ async function runTest(desc) { if (desc.ignore) { return "ignored"; } - + let testFn = wrapTestFnWithSanitizers(desc.fn, desc); + if (!("parent" in desc) && desc.permissions) { + testFn = withPermissions( + testFn, + desc.permissions, + ); + } try { - await desc.fn(desc); - const failCount = failedChildStepsCount(desc); - return failCount === 0 ? "ok" : { - "failed": core.destructureError( - new Error( - `${failCount} test step${failCount === 1 ? "" : "s"} failed.`, - ), - ), - }; + const result = await testFn(desc); + if (result) return result; + const failedSteps = failedChildStepsCount(desc); + return failedSteps === 0 ? "ok" : { failed: { failedSteps } }; } catch (error) { - return { - "failed": core.destructureError(error), - }; - } finally { - const state = MapPrototypeGet(testStates, desc.id); - state.finalized = true; - // ensure the children report their result - for (const childDesc of state.children) { - stepReportResult(childDesc); - } + return { failed: { jsError: core.destructureError(error) } }; } } @@ -1094,6 +1025,11 @@ async function runTests({ const earlier = DateNow(); const result = await runTest(desc); const elapsed = DateNow() - earlier; + const state = MapPrototypeGet(testStates, desc.id); + state.completed = true; + for (const childDesc of state.children) { + stepReportResult(childDesc, { failed: "incomplete" }, 0); + } ops.op_dispatch_test_event({ result: [desc.id, result, elapsed], }); @@ -1153,7 +1089,7 @@ async function runBenchmarks() { function getFullName(desc) { if ("parent" in desc) { - return `${desc.parent.name} > ${desc.name}`; + return `${getFullName(desc.parent)} ... ${desc.name}`; } return desc.name; } @@ -1162,74 +1098,23 @@ function usesSanitizer(desc) { return desc.sanitizeResources || desc.sanitizeOps || desc.sanitizeExit; } -function canStreamReporting(desc) { - let currentDesc = desc; - while (currentDesc != null) { - if (!usesSanitizer(currentDesc)) { - return false; - } - currentDesc = currentDesc.parent; - } - for (const childDesc of MapPrototypeGet(testStates, desc.id).children) { - const state = MapPrototypeGet(testStates, childDesc.id); - if (!usesSanitizer(childDesc) && !state.finalized) { - return false; - } - } - return true; -} - -function stepReportWait(desc) { - const state = MapPrototypeGet(testStates, desc.id); - if (state.reportedWait) { - return; - } - ops.op_dispatch_test_event({ stepWait: desc.id }); - state.reportedWait = true; -} - -function stepReportResult(desc) { +function stepReportResult(desc, result, elapsed) { const state = MapPrototypeGet(testStates, desc.id); - if (state.reportedResult) { - return; - } - stepReportWait(desc); for (const childDesc of state.children) { - stepReportResult(childDesc); - } - let result; - if (state.status == "pending" || state.status == "failed") { - result = { - [state.status]: state.error && core.destructureError(state.error), - }; - } else { - result = state.status; + stepReportResult(childDesc, { failed: "incomplete" }, 0); } ops.op_dispatch_test_event({ - stepResult: [desc.id, result, state.elapsed], + stepResult: [desc.id, result, elapsed], }); - state.reportedResult = true; } function failedChildStepsCount(desc) { return ArrayPrototypeFilter( MapPrototypeGet(testStates, desc.id).children, - (d) => MapPrototypeGet(testStates, d.id).status === "failed", + (d) => MapPrototypeGet(testStates, d.id).failed, ).length; } -/** If a test validation error already occurred then don't bother checking - * the sanitizers as that will create extra noise. - */ -function shouldSkipSanitizers(desc) { - try { - testStepPostValidation(desc); - return false; - } catch { - return true; - } -} - /** @param desc {TestDescription | TestStepDescription} */ function createTestContext(desc) { let parent; @@ -1266,7 +1151,7 @@ function createTestContext(desc) { * @param maybeFn {((t: TestContext) => void | Promise<void>) | undefined} */ async step(nameOrFnOrOptions, maybeFn) { - if (MapPrototypeGet(testStates, desc.id).finalized) { + if (MapPrototypeGet(testStates, desc.id).completed) { throw new Error( "Cannot run test step after parent scope has finished execution. " + "Ensure any `.step(...)` calls are executed before their parent scope completes execution.", @@ -1322,12 +1207,8 @@ function createTestContext(desc) { const state = { context: createTestContext(stepDesc), children: [], - finalized: false, - status: "pending", - error: null, - elapsed: null, - reportedWait: false, - reportedResult: false, + failed: false, + completed: false, }; MapPrototypeSet(testStates, stepDesc.id, state); ArrayPrototypePush( @@ -1335,56 +1216,14 @@ function createTestContext(desc) { stepDesc, ); - try { - if (stepDesc.ignore) { - state.status = "ignored"; - state.finalized = true; - if (canStreamReporting(stepDesc)) { - stepReportResult(stepDesc); - } - return false; - } - - const testFn = wrapTestFnWithSanitizers(stepDesc.fn, stepDesc); - const start = DateNow(); - - try { - await testFn(stepDesc); - - if (failedChildStepsCount(stepDesc) > 0) { - state.status = "failed"; - } else { - state.status = "ok"; - } - } catch (error) { - state.error = error; - state.status = "failed"; - } - - state.elapsed = DateNow() - start; - - if (MapPrototypeGet(testStates, stepDesc.parent.id).finalized) { - // always point this test out as one that was still running - // if the parent step finalized - state.status = "pending"; - } - - state.finalized = true; - - if (state.reportedWait && canStreamReporting(stepDesc)) { - stepReportResult(stepDesc); - } - - return state.status === "ok"; - } finally { - if (canStreamReporting(stepDesc.parent)) { - const parentState = MapPrototypeGet(testStates, stepDesc.parent.id); - // flush any buffered steps - for (const childDesc of parentState.children) { - stepReportResult(childDesc); - } - } - } + ops.op_dispatch_test_event({ stepWait: stepDesc.id }); + const earlier = DateNow(); + const result = await runTest(stepDesc); + const elapsed = DateNow() - earlier; + state.failed = !!result.failed; + state.completed = true; + stepReportResult(stepDesc, result, elapsed); + return result == "ok"; }, }; } diff --git a/cli/lsp/testing/execution.rs b/cli/lsp/testing/execution.rs index ee182c690..44a0729de 100644 --- a/cli/lsp/testing/execution.rs +++ b/cli/lsp/testing/execution.rs @@ -439,9 +439,6 @@ impl TestRun { test::TestStepResult::Failed(_) => { summary.failed_steps += 1; } - test::TestStepResult::Pending(_) => { - summary.pending_steps += 1; - } } reporter.report_step_result( test_steps.get(&id).unwrap(), @@ -449,6 +446,7 @@ impl TestRun { duration, ); } + test::TestEvent::Sigint => {} } } @@ -716,11 +714,10 @@ impl LspTestReporter { test: desc.into(), }) } - test::TestResult::Failed(js_error) => { - let err_string = test::format_test_error(js_error); + test::TestResult::Failed(failure) => { self.progress(lsp_custom::TestRunProgressMessage::Failed { test: desc.into(), - messages: as_test_messages(err_string, false), + messages: as_test_messages(failure.to_string(), false), duration: Some(elapsed as u32), }) } @@ -830,24 +827,13 @@ impl LspTestReporter { test: desc.into(), }) } - test::TestStepResult::Failed(js_error) => { - let messages = if let Some(js_error) = js_error { - let err_string = test::format_test_error(js_error); - as_test_messages(err_string, false) - } else { - vec![] - }; + test::TestStepResult::Failed(failure) => { self.progress(lsp_custom::TestRunProgressMessage::Failed { test: desc.into(), - messages, + messages: as_test_messages(failure.to_string(), false), duration: Some(elapsed as u32), }) } - test::TestStepResult::Pending(_) => { - self.progress(lsp_custom::TestRunProgressMessage::Enqueued { - test: desc.into(), - }) - } } } diff --git a/cli/tests/integration/test_tests.rs b/cli/tests/integration/test_tests.rs index 047dcbc7e..107d137e7 100644 --- a/cli/tests/integration/test_tests.rs +++ b/cli/tests/integration/test_tests.rs @@ -4,6 +4,7 @@ use deno_core::url::Url; use test_util as util; use util::assert_contains; use util::env_vars_for_npm_tests; +use util::wildcard_match; use util::TestContext; #[test] @@ -444,6 +445,26 @@ itest!(parallel_output { exit_code: 1, }); +#[test] +fn sigint_with_hanging_test() { + util::with_pty( + &[ + "test", + "--quiet", + "--no-check", + "test/sigint_with_hanging_test.ts", + ], + |mut console| { + std::thread::sleep(std::time::Duration::from_secs(1)); + console.write_line("\x03"); + wildcard_match( + include_str!("../testdata/test/sigint_with_hanging_test.out"), + &console.read_all_output(), + ); + }, + ); +} + itest!(package_json_basic { args: "test", output: "package_json/basic/lib.test.out", diff --git a/cli/tests/testdata/test/ops_sanitizer_missing_details.out b/cli/tests/testdata/test/ops_sanitizer_missing_details.out index b277ba74f..36191912a 100644 --- a/cli/tests/testdata/test/ops_sanitizer_missing_details.out +++ b/cli/tests/testdata/test/ops_sanitizer_missing_details.out @@ -5,12 +5,9 @@ test 1 ... FAILED [WILDCARD] ERRORS test 1 => ./test/ops_sanitizer_missing_details.ts:[WILDCARD] -error: AssertionError: Test case is leaking async ops. - - - 1 async operation to op_write was started in this test, but never completed. - +error: Leaking async ops: + - 1 async operation to op_write was started in this test, but never completed. To get more details where ops were leaked, run again with --trace-ops flag. - at [WILDCARD] FAILURES diff --git a/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests.out b/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests.out index ded6708d1..d7ccc6a00 100644 --- a/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests.out +++ b/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests.out @@ -6,9 +6,8 @@ test 2 ... FAILED ([WILDCARD]) ERRORS test 1 => ./test/ops_sanitizer_multiple_timeout_tests.ts:[WILDCARD] -error: AssertionError: Test case is leaking async ops. - - - 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. The operations were started here: +error: Leaking async ops: + - 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. The operations were started here: at [WILDCARD] at setTimeout ([WILDCARD]) at test ([WILDCARD]/testdata/test/ops_sanitizer_multiple_timeout_tests.ts:4:3) @@ -21,12 +20,9 @@ error: AssertionError: Test case is leaking async ops. at [WILDCARD]/testdata/test/ops_sanitizer_multiple_timeout_tests.ts:8:27 at [WILDCARD] - at [WILDCARD] - test 2 => ./test/ops_sanitizer_multiple_timeout_tests.ts:[WILDCARD] -error: AssertionError: Test case is leaking async ops. - - - 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. The operations were started here: +error: Leaking async ops: + - 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. The operations were started here: at [WILDCARD] at setTimeout ([WILDCARD]) at test ([WILDCARD]/testdata/test/ops_sanitizer_multiple_timeout_tests.ts:4:3) @@ -39,8 +35,6 @@ error: AssertionError: Test case is leaking async ops. at [WILDCARD]/testdata/test/ops_sanitizer_multiple_timeout_tests.ts:10:27 at [WILDCARD] - at [WILDCARD] - FAILURES test 1 => ./test/ops_sanitizer_multiple_timeout_tests.ts:[WILDCARD] diff --git a/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests_no_trace.out b/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests_no_trace.out index 3f40bb9d0..1655c162f 100644 --- a/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests_no_trace.out +++ b/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests_no_trace.out @@ -6,20 +6,14 @@ test 2 ... FAILED ([WILDCARD]) ERRORS test 1 => ./test/ops_sanitizer_multiple_timeout_tests.ts:[WILDCARD] -error: AssertionError: Test case is leaking async ops. - - - 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. - +error: Leaking async ops: + - 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. To get more details where ops were leaked, run again with --trace-ops flag. - at [WILDCARD] test 2 => ./test/ops_sanitizer_multiple_timeout_tests.ts:[WILDCARD] -error: AssertionError: Test case is leaking async ops. - - - 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. - +error: Leaking async ops: + - 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. To get more details where ops were leaked, run again with --trace-ops flag. - at [WILDCARD] FAILURES diff --git a/cli/tests/testdata/test/ops_sanitizer_unstable.out b/cli/tests/testdata/test/ops_sanitizer_unstable.out index 98e0a26ff..2d5ab9079 100644 --- a/cli/tests/testdata/test/ops_sanitizer_unstable.out +++ b/cli/tests/testdata/test/ops_sanitizer_unstable.out @@ -6,16 +6,13 @@ leak interval ... FAILED ([WILDCARD]) ERRORS leak interval => ./test/ops_sanitizer_unstable.ts:[WILDCARD] -error: AssertionError: Test case is leaking async ops. - - - 1 async operation to sleep for a duration was started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. The operation was started here: +error: Leaking async ops: + - 1 async operation to sleep for a duration was started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. The operation was started here: at [WILDCARD] at setInterval ([WILDCARD]) at [WILDCARD]/testdata/test/ops_sanitizer_unstable.ts:3:3 at [WILDCARD] - at [WILDCARD] - FAILURES leak interval => ./test/ops_sanitizer_unstable.ts:[WILDCARD] diff --git a/cli/tests/testdata/test/parallel_output.out b/cli/tests/testdata/test/parallel_output.out index 3ec9c5221..f5c932171 100644 --- a/cli/tests/testdata/test/parallel_output.out +++ b/cli/tests/testdata/test/parallel_output.out @@ -12,45 +12,44 @@ Hello, world! (from step 4) ./test/parallel_output.ts => step output ... ok ([WILDCARD]ms) ./test/parallel_output.ts => step failures ... step 1 ... ok ([WILDCARD]ms) ./test/parallel_output.ts => step failures ... step 2 ... FAILED ([WILDCARD]ms) - error: Error: Fail. - throw new Error("Fail."); - ^ - at file:///[WILDCARD]/test/parallel_output.ts:15:11 - at [WILDCARD] - at file:///[WILDCARD]/test/parallel_output.ts:14:11 ./test/parallel_output.ts => step failures ... step 3 ... FAILED ([WILDCARD]ms) - error: Error: Fail. - await t.step("step 3", () => Promise.reject(new Error("Fail."))); - ^ - at file:///[WILDCARD]/test/parallel_output.ts:17:47 - at [WILDCARD] - at file:///[WILDCARD]/test/parallel_output.ts:17:11 -./test/parallel_output.ts => step failures ... FAILED ([WILDCARD]ms) +./test/parallel_output.ts => step failures ... FAILED (due to 2 failed steps) ([WILDCARD]ms) ./test/parallel_output.ts => step nested failure ... step 1 ... inner 1 ... ok ([WILDCARD]ms) ./test/parallel_output.ts => step nested failure ... step 1 ... inner 2 ... FAILED ([WILDCARD]ms) - error: Error: Failed. - throw new Error("Failed."); - ^ - at file:///[WILDCARD]/test/parallel_output.ts:24:13 - at [WILDCARD] - at file:///[WILDCARD]/test/parallel_output.ts:23:13 -./test/parallel_output.ts => step nested failure ... step 1 ... FAILED ([WILDCARD]ms) -./test/parallel_output.ts => step nested failure ... FAILED ([WILDCARD]ms) +./test/parallel_output.ts => step nested failure ... step 1 ... FAILED (due to 1 failed step) ([WILDCARD]ms) +./test/parallel_output.ts => step nested failure ... FAILED (due to 1 failed step) ([WILDCARD]ms) ERRORS -step failures => ./test/parallel_output.ts:12:6 -error: Error: 2 test steps failed. +step failures ... step 2 => ./test/parallel_output.ts:14:11 +error: Error: Fail. + throw new Error("Fail."); + ^ + at file:///[WILDCARD]/test/parallel_output.ts:15:11 at [WILDCARD] + at file:///[WILDCARD]/test/parallel_output.ts:14:11 -step nested failure => ./test/parallel_output.ts:20:6 -error: Error: 1 test step failed. +step failures ... step 3 => ./test/parallel_output.ts:17:11 +error: Error: Fail. + await t.step("step 3", () => Promise.reject(new Error("Fail."))); + ^ + at file:///[WILDCARD]/test/parallel_output.ts:17:47 at [WILDCARD] + at file:///[WILDCARD]/test/parallel_output.ts:17:11 + +step nested failure ... step 1 ... inner 2 => ./test/parallel_output.ts:23:13 +error: Error: Failed. + throw new Error("Failed."); + ^ + at file:///[WILDCARD]/test/parallel_output.ts:24:13 + at [WILDCARD] + at file:///[WILDCARD]/test/parallel_output.ts:23:13 FAILURES -step failures => ./test/parallel_output.ts:12:6 -step nested failure => ./test/parallel_output.ts:20:6 +step failures ... step 2 => ./test/parallel_output.ts:14:11 +step failures ... step 3 => ./test/parallel_output.ts:17:11 +step nested failure ... step 1 ... inner 2 => ./test/parallel_output.ts:23:13 FAILED | 1 passed (6 steps) | 2 failed (4 steps) ([WILDCARD]ms) diff --git a/cli/tests/testdata/test/resource_sanitizer.out b/cli/tests/testdata/test/resource_sanitizer.out index 695a45a4c..a0e916654 100644 --- a/cli/tests/testdata/test/resource_sanitizer.out +++ b/cli/tests/testdata/test/resource_sanitizer.out @@ -5,12 +5,9 @@ leak ... FAILED ([WILDCARD]) ERRORS leak => ./test/resource_sanitizer.ts:[WILDCARD] -error: AssertionError: Test case is leaking 2 resources: - - - The stdin pipe (rid 0) was opened before the test started, but was closed during the test. Do not close resources in a test that were not created during that test. - - A file (rid 3) was opened during the test, but not closed during the test. Close the file handle by calling `file.close()`. - - at [WILDCARD] +error: Leaking resources: + - The stdin pipe (rid 0) was opened before the test started, but was closed during the test. Do not close resources in a test that were not created during that test. + - A file (rid 3) was opened during the test, but not closed during the test. Close the file handle by calling `file.close()`. FAILURES diff --git a/cli/tests/testdata/test/sigint_with_hanging_test.out b/cli/tests/testdata/test/sigint_with_hanging_test.out new file mode 100644 index 000000000..c590c2192 --- /dev/null +++ b/cli/tests/testdata/test/sigint_with_hanging_test.out @@ -0,0 +1,10 @@ +running 1 test from [WILDCARD]/hanging_test.ts +test ... + step 1 ... + step 2 ... +SIGINT The following tests were pending: + +test => [WILDCARD]/hanging_test.ts:3:6 +test ... step 1 => [WILDCARD]/hanging_test.ts:9:13 +test ... step 1 ... step 2 => [WILDCARD]/hanging_test.ts:10:15 + diff --git a/cli/tests/testdata/test/sigint_with_hanging_test.ts b/cli/tests/testdata/test/sigint_with_hanging_test.ts new file mode 100644 index 000000000..07b126e98 --- /dev/null +++ b/cli/tests/testdata/test/sigint_with_hanging_test.ts @@ -0,0 +1,15 @@ +setInterval(() => {}, 10000); + +Deno.test({ + name: "test", + sanitizeOps: false, + sanitizeExit: false, + sanitizeResources: false, + async fn(t) { + await t.step("step 1", async (t) => { + await t.step("step 2", async () => { + await new Promise(() => {}); + }); + }); + }, +}); diff --git a/cli/tests/testdata/test/steps/failing_steps.out b/cli/tests/testdata/test/steps/failing_steps.out index c68889c8d..4b717f5cc 100644 --- a/cli/tests/testdata/test/steps/failing_steps.out +++ b/cli/tests/testdata/test/steps/failing_steps.out @@ -3,47 +3,42 @@ running 3 tests from ./test/steps/failing_steps.ts nested failure ... step 1 ... inner 1 ... FAILED ([WILDCARD]) - error: Error: Failed. - throw new Error("Failed."); - ^ - at [WILDCARD]/failing_steps.ts:[WILDCARD] - [WILDCARD] inner 2 ... ok ([WILDCARD]) - step 1 ... FAILED ([WILDCARD]) -nested failure ... FAILED ([WILDCARD]) + step 1 ... FAILED (due to 1 failed step) ([WILDCARD]) +nested failure ... FAILED (due to 1 failed step) ([WILDCARD]) multiple test step failures ... step 1 ... FAILED ([WILDCARD]) - error: Error: Fail. - throw new Error("Fail."); - ^ - [WILDCARD] step 2 ... FAILED ([WILDCARD]) - error: Error: Fail. - await t.step("step 2", () => Promise.reject(new Error("Fail."))); - ^ - at [WILDCARD]/failing_steps.ts:[WILDCARD] - [WILDCARD] -multiple test step failures ... FAILED ([WILDCARD]) +multiple test step failures ... FAILED (due to 2 failed steps) ([WILDCARD]) failing step in failing test ... step 1 ... FAILED ([WILDCARD]) - error: Error: Fail. - throw new Error("Fail."); - ^ - at [WILDCARD]/failing_steps.ts:[WILDCARD] - at [WILDCARD] failing step in failing test ... FAILED ([WILDCARD]) ERRORS -nested failure => ./test/steps/failing_steps.ts:[WILDCARD] -error: Error: 1 test step failed. - at runTest (ext:cli/40_testing.js:[WILDCARD]) - at async runTests (ext:cli/40_testing.js:[WILDCARD]) +nested failure ... step 1 ... inner 1 => ./test/steps/failing_steps.ts:[WILDCARD] +error: Error: Failed. + throw new Error("Failed."); + ^ + at [WILDCARD]/failing_steps.ts:[WILDCARD] + +multiple test step failures ... step 1 => ./test/steps/failing_steps.ts:[WILDCARD] +error: Error: Fail. + throw new Error("Fail."); + ^ + at [WILDCARD]/failing_steps.ts:[WILDCARD] -multiple test step failures => ./test/steps/failing_steps.ts:[WILDCARD] -error: Error: 2 test steps failed. - at runTest (ext:cli/40_testing.js:[WILDCARD]) - at async runTests (ext:cli/40_testing.js:[WILDCARD]) +multiple test step failures ... step 2 => ./test/steps/failing_steps.ts:[WILDCARD] +error: Error: Fail. + await t.step("step 2", () => Promise.reject(new Error("Fail."))); + ^ + at [WILDCARD]/failing_steps.ts:[WILDCARD] + +failing step in failing test ... step 1 => ./test/steps/failing_steps.ts:[WILDCARD] +error: Error: Fail. + throw new Error("Fail."); + ^ + at [WILDCARD]/failing_steps.ts:[WILDCARD] failing step in failing test => ./test/steps/failing_steps.ts:[WILDCARD] error: Error: Fail test. @@ -53,8 +48,10 @@ error: Error: Fail test. FAILURES -nested failure => ./test/steps/failing_steps.ts:[WILDCARD] -multiple test step failures => ./test/steps/failing_steps.ts:[WILDCARD] +nested failure ... step 1 ... inner 1 => ./test/steps/failing_steps.ts:[WILDCARD] +multiple test step failures ... step 1 => ./test/steps/failing_steps.ts:[WILDCARD] +multiple test step failures ... step 2 => ./test/steps/failing_steps.ts:[WILDCARD] +failing step in failing test ... step 1 => ./test/steps/failing_steps.ts:[WILDCARD] failing step in failing test => ./test/steps/failing_steps.ts:[WILDCARD] FAILED | 0 passed (1 step) | 3 failed (5 steps) ([WILDCARD]) diff --git a/cli/tests/testdata/test/steps/invalid_usage.out b/cli/tests/testdata/test/steps/invalid_usage.out index dc97a5eed..311be4fd7 100644 --- a/cli/tests/testdata/test/steps/invalid_usage.out +++ b/cli/tests/testdata/test/steps/invalid_usage.out @@ -4,65 +4,33 @@ capturing ... some step ... ok ([WILDCARD]) capturing ... FAILED ([WILDCARD]) top level missing await ... - step ... pending ([WILDCARD]) -top level missing await ... FAILED ([WILDCARD]) + step ... INCOMPLETE +top level missing await ... FAILED (due to incomplete steps) ([WILDCARD]) inner missing await ... step ... - inner ... pending ([WILDCARD]) - error: Error: Parent scope completed before test step finished execution. Ensure all steps are awaited (ex. `await t.step(...)`). - at [WILDCARD] - at async TestContext.step [WILDCARD] - step ... FAILED ([WILDCARD]) - error: Error: There were still test steps running after the current scope finished execution. Ensure all steps are awaited (ex. `await t.step(...)`). - await t.step("step", (t) => { - ^ - at [WILDCARD] - at async fn ([WILDCARD]/invalid_usage.ts:[WILDCARD]) -inner missing await ... FAILED ([WILDCARD]) + inner ... INCOMPLETE + step ... FAILED (due to incomplete steps) ([WILDCARD]) +inner missing await ... FAILED (due to 1 failed step) ([WILDCARD]) parallel steps with sanitizers ... - step 1 ... pending ([WILDCARD]) + step 1 ... INCOMPLETE step 2 ... FAILED ([WILDCARD]) - error: Error: Cannot start test step while another test step with sanitizers is running. - * parallel steps with sanitizers > step 1 - await t.step("step 2", () => {}); - ^ - at [WILDCARD] - at [WILDCARD]/invalid_usage.ts:[WILDCARD] -parallel steps with sanitizers ... FAILED ([WILDCARD]) +parallel steps with sanitizers ... FAILED (due to incomplete steps) ([WILDCARD]) parallel steps when first has sanitizer ... - step 1 ... pending ([WILDCARD]) + step 1 ... ok ([WILDCARD]) step 2 ... FAILED ([WILDCARD]) - error: Error: Cannot start test step while another test step with sanitizers is running. - * parallel steps when first has sanitizer > step 1 - await t.step({ - ^ - at [WILDCARD] - at [WILDCARD]/invalid_usage.ts:[WILDCARD] -parallel steps when first has sanitizer ... FAILED ([WILDCARD]) +parallel steps when first has sanitizer ... FAILED (due to 1 failed step) ([WILDCARD]) parallel steps when second has sanitizer ... step 1 ... ok ([WILDCARD]) step 2 ... FAILED ([WILDCARD]) - error: Error: Cannot start test step with sanitizers while another test step is running. - * parallel steps when second has sanitizer > step 1 - await t.step({ - ^ - at [WILDCARD] - at [WILDCARD]/invalid_usage.ts:[WILDCARD] -parallel steps when second has sanitizer ... FAILED ([WILDCARD]) +parallel steps when second has sanitizer ... FAILED (due to 1 failed step) ([WILDCARD]) parallel steps where only inner tests have sanitizers ... step 1 ... step inner ... ok ([WILDCARD]) step 1 ... ok ([WILDCARD]) step 2 ... step inner ... FAILED ([WILDCARD]) - error: Error: Cannot start test step with sanitizers while another test step is running. - * parallel steps where only inner tests have sanitizers > step 1 - await t.step({ - ^ - at [WILDCARD] - at [WILDCARD]/invalid_usage.ts:[WILDCARD] - step 2 ... FAILED ([WILDCARD]) -parallel steps where only inner tests have sanitizers ... FAILED ([WILDCARD]) + step 2 ... FAILED (due to 1 failed step) ([WILDCARD]) +parallel steps where only inner tests have sanitizers ... FAILED (due to 1 failed step) ([WILDCARD]) ERRORS @@ -73,38 +41,42 @@ error: Error: Cannot run test step after parent scope has finished execution. En at TestContext.step ([WILDCARD]) at [WILDCARD]/invalid_usage.ts:[WILDCARD] -top level missing await => ./test/steps/invalid_usage.ts:[WILDCARD] -error: Error: There were still test steps running after the current scope finished execution. Ensure all steps are awaited (ex. `await t.step(...)`). - [WILDCARD] +top level missing await ... step => ./test/steps/invalid_usage.ts:[WILDCARD] +error: Didn't complete before parent. Await step with `await t.step(...)`. + +inner missing await ... step ... inner => ./test/steps/invalid_usage.ts:[WILDCARD] +error: Didn't complete before parent. Await step with `await t.step(...)`. -inner missing await => ./test/steps/invalid_usage.ts:[WILDCARD] -error: Error: 1 test step failed. - at [WILDCARD] +parallel steps with sanitizers ... step 2 => ./test/steps/invalid_usage.ts:[WILDCARD] +error: Started test step while another test step with sanitizers was running: + * parallel steps with sanitizers ... step 1 -parallel steps with sanitizers => ./test/steps/invalid_usage.ts:[WILDCARD] -error: Error: There were still test steps running after the current scope finished execution. Ensure all steps are awaited (ex. `await t.step(...)`). - [WILDCARD] +parallel steps with sanitizers ... step 1 => ./test/steps/invalid_usage.ts:[WILDCARD] +error: Didn't complete before parent. Await step with `await t.step(...)`. -parallel steps when first has sanitizer => ./test/steps/invalid_usage.ts:[WILDCARD] -error: Error: 1 test step failed. - at runTest ([WILDCARD]) - at [WILDCARD] +parallel steps when first has sanitizer ... step 2 => ./test/steps/invalid_usage.ts:[WILDCARD] +error: Started test step while another test step with sanitizers was running: + * parallel steps when first has sanitizer ... step 1 -parallel steps when second has sanitizer => ./test/steps/invalid_usage.ts:[WILDCARD] -error: Error: 1 test step failed. - at runTest ([WILDCARD]) - at [WILDCARD] +parallel steps when second has sanitizer ... step 2 => ./test/steps/invalid_usage.ts:[WILDCARD] +error: Started test step with sanitizers while another test step was running: + * parallel steps when second has sanitizer ... step 1 + +parallel steps where only inner tests have sanitizers ... step 2 ... step inner => ./test/steps/invalid_usage.ts:[WILDCARD] +error: Started test step with sanitizers while another test step was running: + * parallel steps where only inner tests have sanitizers ... step 1 FAILURES -capturing => ./test/steps/invalid_usage.ts:[WILDCARD] -top level missing await => ./test/steps/invalid_usage.ts:[WILDCARD] -inner missing await => ./test/steps/invalid_usage.ts:[WILDCARD] -parallel steps with sanitizers => ./test/steps/invalid_usage.ts:[WILDCARD] -parallel steps when first has sanitizer => ./test/steps/invalid_usage.ts:[WILDCARD] -parallel steps when second has sanitizer => ./test/steps/invalid_usage.ts:[WILDCARD] -parallel steps where only inner tests have sanitizers => ./test/steps/invalid_usage.ts:[WILDCARD] +capturing => ./test/steps/invalid_usage.ts:3:6 +top level missing await ... step => ./test/steps/invalid_usage.ts:[WILDCARD] +inner missing await ... step ... inner => ./test/steps/invalid_usage.ts:[WILDCARD] +parallel steps with sanitizers ... step 2 => ./test/steps/invalid_usage.ts:[WILDCARD] +parallel steps with sanitizers ... step 1 => ./test/steps/invalid_usage.ts:[WILDCARD] +parallel steps when first has sanitizer ... step 2 => ./test/steps/invalid_usage.ts:[WILDCARD] +parallel steps when second has sanitizer ... step 2 => ./test/steps/invalid_usage.ts:[WILDCARD] +parallel steps where only inner tests have sanitizers ... step 2 ... step inner => ./test/steps/invalid_usage.ts:[WILDCARD] -FAILED | 0 passed (4 steps) | 7 failed (10 steps) ([WILDCARD]) +FAILED | 0 passed (5 steps) | 7 failed (9 steps) ([WILDCARD]) error: Test failed diff --git a/cli/tests/testdata/test/steps/invalid_usage.ts b/cli/tests/testdata/test/steps/invalid_usage.ts index bc761b5dd..fcbe8b454 100644 --- a/cli/tests/testdata/test/steps/invalid_usage.ts +++ b/cli/tests/testdata/test/steps/invalid_usage.ts @@ -11,7 +11,7 @@ Deno.test("capturing", async (t) => { Deno.test("top level missing await", (t) => { t.step("step", () => { - return new Promise((resolve) => setTimeout(resolve, 10)); + return new Promise(() => {}); }); }); diff --git a/cli/tests/unit_node/process_test.ts b/cli/tests/unit_node/process_test.ts index 7310e4ad7..686a3dbbc 100644 --- a/cli/tests/unit_node/process_test.ts +++ b/cli/tests/unit_node/process_test.ts @@ -155,24 +155,29 @@ Deno.test({ name: "process.on signal", ignore: Deno.build.os == "windows", async fn() { - const promise = deferred(); - let c = 0; - const listener = () => { - c += 1; - }; - process.on("SIGINT", listener); - setTimeout(async () => { - // Sends SIGINT 3 times. - for (const _ of Array(3)) { - await delay(20); - Deno.kill(Deno.pid, "SIGINT"); - } + const process = new Deno.Command(Deno.execPath(), { + args: [ + "eval", + ` + import process from "node:process"; + setInterval(() => {}, 1000); + process.on("SIGINT", () => { + console.log("foo"); + }); + `, + ], + stdout: "piped", + stderr: "null", + }).spawn(); + await delay(500); + for (const _ of Array(3)) { + process.kill("SIGINT"); await delay(20); - Deno.removeSignalListener("SIGINT", listener); - promise.resolve(); - }); - await promise; - assertEquals(c, 3); + } + await delay(20); + process.kill("SIGTERM"); + const output = await process.output(); + assertEquals(new TextDecoder().decode(output.stdout), "foo\nfoo\nfoo\n"); }, }); @@ -180,24 +185,35 @@ Deno.test({ name: "process.off signal", ignore: Deno.build.os == "windows", async fn() { - const promise = deferred(); - let c = 0; - const listener = () => { - c += 1; - process.off("SIGINT", listener); - }; - process.on("SIGINT", listener); - setTimeout(async () => { - // Sends SIGINT 3 times. - for (const _ of Array(3)) { - await delay(20); - Deno.kill(Deno.pid, "SIGINT"); - } + const process = new Deno.Command(Deno.execPath(), { + args: [ + "eval", + ` + import process from "node:process"; + setInterval(() => {}, 1000); + const listener = () => { + console.log("foo"); + process.off("SIGINT") + }; + process.on("SIGINT", listener); + `, + ], + stdout: "piped", + stderr: "null", + }).spawn(); + await delay(500); + for (const _ of Array(3)) { + try { + process.kill("SIGINT"); + } catch { /* should die after the first one */ } await delay(20); - promise.resolve(); - }); - await promise; - assertEquals(c, 1); + } + await delay(20); + try { + process.kill("SIGTERM"); + } catch { /* should be dead, avoid hanging just in case */ } + const output = await process.output(); + assertEquals(new TextDecoder().decode(output.stdout), "foo\n"); }, }); diff --git a/cli/tools/test.rs b/cli/tools/test.rs index 20788dbd9..28364050e 100644 --- a/cli/tools/test.rs +++ b/cli/tools/test.rs @@ -39,6 +39,7 @@ use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; use deno_runtime::tokio_util::run_local; use indexmap::IndexMap; +use indexmap::IndexSet; use log::Level; use rand::rngs::SmallRng; use rand::seq::SliceRandom; @@ -47,6 +48,8 @@ use regex::Regex; use serde::Deserialize; use std::cell::RefCell; use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::collections::HashMap; use std::collections::HashSet; use std::fmt::Write as _; use std::io::Read; @@ -58,8 +61,10 @@ use std::sync::atomic::AtomicUsize; use std::sync::Arc; use std::time::Duration; use std::time::Instant; +use tokio::signal; use tokio::sync::mpsc::unbounded_channel; use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::mpsc::WeakUnboundedSender; /// The test mode is used to determine how a specifier is to be tested. #[derive(Debug, Clone, Eq, PartialEq)] @@ -157,10 +162,97 @@ pub enum TestOutput { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] +pub enum TestFailure { + JsError(Box<JsError>), + FailedSteps(usize), + IncompleteSteps, + LeakedOps(Vec<String>, bool), // Details, isOpCallTracingEnabled + LeakedResources(Vec<String>), // Details + // The rest are for steps only. + Incomplete, + OverlapsWithSanitizers(IndexSet<String>), // Long names of overlapped tests + HasSanitizersAndOverlaps(IndexSet<String>), // Long names of overlapped tests +} + +impl ToString for TestFailure { + fn to_string(&self) -> String { + match self { + TestFailure::JsError(js_error) => format_test_error(js_error), + TestFailure::FailedSteps(1) => "1 test step failed.".to_string(), + TestFailure::FailedSteps(n) => format!("{} test steps failed.", n), + TestFailure::IncompleteSteps => "Completed while steps were still running. Ensure all steps are awaited with `await t.step(...)`.".to_string(), + TestFailure::Incomplete => "Didn't complete before parent. Await step with `await t.step(...)`.".to_string(), + TestFailure::LeakedOps(details, is_op_call_tracing_enabled) => { + let mut string = "Leaking async ops:".to_string(); + for detail in details { + string.push_str(&format!("\n - {}", detail)); + } + if !is_op_call_tracing_enabled { + string.push_str("\nTo get more details where ops were leaked, run again with --trace-ops flag."); + } + string + } + TestFailure::LeakedResources(details) => { + let mut string = "Leaking resources:".to_string(); + for detail in details { + string.push_str(&format!("\n - {}", detail)); + } + string + } + TestFailure::OverlapsWithSanitizers(long_names) => { + let mut string = "Started test step while another test step with sanitizers was running:".to_string(); + for long_name in long_names { + string.push_str(&format!("\n * {}", long_name)); + } + string + } + TestFailure::HasSanitizersAndOverlaps(long_names) => { + let mut string = "Started test step with sanitizers while another test step was running:".to_string(); + for long_name in long_names { + string.push_str(&format!("\n * {}", long_name)); + } + string + } + } + } +} + +impl TestFailure { + fn format_label(&self) -> String { + match self { + TestFailure::Incomplete => colors::gray("INCOMPLETE").to_string(), + _ => colors::red("FAILED").to_string(), + } + } + + fn format_inline_summary(&self) -> Option<String> { + match self { + TestFailure::FailedSteps(1) => Some("due to 1 failed step".to_string()), + TestFailure::FailedSteps(n) => Some(format!("due to {} failed steps", n)), + TestFailure::IncompleteSteps => { + Some("due to incomplete steps".to_string()) + } + _ => None, + } + } + + fn hide_in_summary(&self) -> bool { + // These failure variants are hidden in summaries because they are caused + // by child errors that will be summarized separately. + matches!( + self, + TestFailure::FailedSteps(_) | TestFailure::IncompleteSteps + ) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] pub enum TestResult { Ok, Ignored, - Failed(Box<JsError>), + Failed(TestFailure), Cancelled, } @@ -193,18 +285,7 @@ impl TestStepDescription { pub enum TestStepResult { Ok, Ignored, - Failed(Option<Box<JsError>>), - Pending(Option<Box<JsError>>), -} - -impl TestStepResult { - fn error(&self) -> Option<&JsError> { - match self { - TestStepResult::Failed(Some(error)) => Some(error), - TestStepResult::Pending(Some(error)) => Some(error), - _ => None, - } - } + Failed(TestFailure), } #[derive(Debug, Clone, Eq, PartialEq, Deserialize)] @@ -228,6 +309,7 @@ pub enum TestEvent { StepRegister(TestStepDescription), StepWait(usize), StepResult(usize, TestStepResult, u64), + Sigint, } #[derive(Debug, Clone, Deserialize)] @@ -238,11 +320,10 @@ pub struct TestSummary { pub ignored: usize, pub passed_steps: usize, pub failed_steps: usize, - pub pending_steps: usize, pub ignored_steps: usize, pub filtered_out: usize, pub measured: usize, - pub failures: Vec<(TestDescription, Box<JsError>)>, + pub failures: Vec<(TestDescription, TestFailure)>, pub uncaught_errors: Vec<(String, Box<JsError>)>, } @@ -262,7 +343,6 @@ impl TestSummary { ignored: 0, passed_steps: 0, failed_steps: 0, - pending_steps: 0, ignored_steps: 0, filtered_out: 0, measured: 0, @@ -280,10 +360,12 @@ struct PrettyTestReporter { parallel: bool, echo_output: bool, in_new_line: bool, - last_wait_id: Option<usize>, + scope_test_id: Option<usize>, cwd: Url, did_have_user_output: bool, started_tests: bool, + child_results_buffer: + HashMap<usize, IndexMap<usize, (TestStepDescription, TestStepResult, u64)>>, } impl PrettyTestReporter { @@ -292,10 +374,11 @@ impl PrettyTestReporter { parallel, echo_output, in_new_line: true, - last_wait_id: None, + scope_test_id: None, cwd: Url::from_directory_path(std::env::current_dir().unwrap()).unwrap(), did_have_user_output: false, started_tests: false, + child_results_buffer: Default::default(), } } @@ -316,7 +399,7 @@ impl PrettyTestReporter { self.in_new_line = false; // flush for faster feedback when line buffered std::io::stdout().flush().unwrap(); - self.last_wait_id = Some(description.id); + self.scope_test_id = Some(description.id); } fn to_relative_path_or_remote_url(&self, path_or_url: &str) -> String { @@ -341,7 +424,7 @@ impl PrettyTestReporter { self.in_new_line = false; // flush for faster feedback when line buffered std::io::stdout().flush().unwrap(); - self.last_wait_id = Some(description.id); + self.scope_test_id = Some(description.id); } fn force_report_step_result( @@ -350,32 +433,53 @@ impl PrettyTestReporter { result: &TestStepResult, elapsed: u64, ) { - let status = match result { - TestStepResult::Ok => colors::green("ok").to_string(), - TestStepResult::Ignored => colors::yellow("ignored").to_string(), - TestStepResult::Pending(_) => colors::gray("pending").to_string(), - TestStepResult::Failed(_) => colors::red("FAILED").to_string(), - }; - self.write_output_end(); - if self.in_new_line || self.last_wait_id != Some(description.id) { + if self.in_new_line || self.scope_test_id != Some(description.id) { self.force_report_step_wait(description); } - println!( - " {} {}", - status, - colors::gray(format!("({})", display::human_elapsed(elapsed.into()))) - ); + if !self.parallel { + let child_results = self + .child_results_buffer + .remove(&description.id) + .unwrap_or_default(); + for (desc, result, elapsed) in child_results.values() { + self.force_report_step_result(desc, result, *elapsed); + } + if !child_results.is_empty() { + self.force_report_step_wait(description); + } + } - if let Some(js_error) = result.error() { - let err_string = format_test_error(js_error); - let err_string = format!("{}: {}", colors::red_bold("error"), err_string); - for line in err_string.lines() { - println!("{}{}", " ".repeat(description.level + 1), line); + let status = match &result { + TestStepResult::Ok => colors::green("ok").to_string(), + TestStepResult::Ignored => colors::yellow("ignored").to_string(), + TestStepResult::Failed(failure) => failure.format_label(), + }; + print!(" {}", status); + if let TestStepResult::Failed(failure) = result { + if let Some(inline_summary) = failure.format_inline_summary() { + print!(" ({})", inline_summary) } } + if !matches!(result, TestStepResult::Failed(TestFailure::Incomplete)) { + print!( + " {}", + colors::gray(format!("({})", display::human_elapsed(elapsed.into()))) + ); + } + println!(); self.in_new_line = true; + if self.parallel { + self.scope_test_id = None; + } else { + self.scope_test_id = Some(description.parent_id); + } + self + .child_results_buffer + .entry(description.parent_id) + .or_default() + .remove(&description.id); } fn write_output_end(&mut self) { @@ -442,23 +546,28 @@ impl PrettyTestReporter { } self.write_output_end(); - if self.in_new_line || self.last_wait_id != Some(description.id) { + if self.in_new_line || self.scope_test_id != Some(description.id) { self.force_report_wait(description); } let status = match result { TestResult::Ok => colors::green("ok").to_string(), TestResult::Ignored => colors::yellow("ignored").to_string(), - TestResult::Failed(_) => colors::red("FAILED").to_string(), + TestResult::Failed(failure) => failure.format_label(), TestResult::Cancelled => colors::gray("cancelled").to_string(), }; - + print!(" {}", status); + if let TestResult::Failed(failure) = result { + if let Some(inline_summary) = failure.format_inline_summary() { + print!(" ({})", inline_summary) + } + } println!( - " {} {}", - status, + " {}", colors::gray(format!("({})", display::human_elapsed(elapsed.into()))) ); self.in_new_line = true; + self.scope_test_id = None; } fn report_uncaught_error(&mut self, origin: &str, _error: &JsError) { @@ -477,14 +586,14 @@ impl PrettyTestReporter { fn report_step_register(&mut self, _description: &TestStepDescription) {} fn report_step_wait(&mut self, description: &TestStepDescription) { - if !self.parallel { + if !self.parallel && self.scope_test_id == Some(description.parent_id) { self.force_report_step_wait(description); } } fn report_step_result( &mut self, - description: &TestStepDescription, + desc: &TestStepDescription, result: &TestStepResult, elapsed: u64, tests: &IndexMap<usize, TestDescription>, @@ -492,35 +601,34 @@ impl PrettyTestReporter { ) { if self.parallel { self.write_output_end(); - let root; - let mut ancestor_names = vec![]; - let mut current_desc = description; - loop { - if let Some(step_desc) = test_steps.get(¤t_desc.parent_id) { - ancestor_names.push(&step_desc.name); - current_desc = step_desc; - } else { - root = tests.get(¤t_desc.parent_id).unwrap(); - break; - } - } - ancestor_names.reverse(); print!( - "{}", + "{} {} ...", colors::gray(format!( "{} =>", - self.to_relative_path_or_remote_url(&description.origin) - )) + self.to_relative_path_or_remote_url(&desc.origin) + )), + self.format_test_step_ancestry(desc, tests, test_steps) ); - print!(" {} ...", root.name); - for name in ancestor_names { - print!(" {name} ..."); - } - print!(" {} ...", description.name); self.in_new_line = false; - self.last_wait_id = Some(description.id); + self.scope_test_id = Some(desc.id); + self.force_report_step_result(desc, result, elapsed); + } else { + let sibling_results = + self.child_results_buffer.entry(desc.parent_id).or_default(); + if self.scope_test_id == Some(desc.id) + || self.scope_test_id == Some(desc.parent_id) + { + let sibling_results = std::mem::take(sibling_results); + self.force_report_step_result(desc, result, elapsed); + // Flush buffered sibling results. + for (desc, result, elapsed) in sibling_results.values() { + self.force_report_step_result(desc, result, *elapsed); + } + } else { + sibling_results + .insert(desc.id, (desc.clone(), result.clone(), elapsed)); + } } - self.force_report_step_result(description, result, elapsed); } fn report_summary(&mut self, summary: &TestSummary, elapsed: &Duration) { @@ -528,14 +636,14 @@ impl PrettyTestReporter { #[allow(clippy::type_complexity)] // Type alias doesn't look better here let mut failures_by_origin: BTreeMap< String, - (Vec<(&TestDescription, &JsError)>, Option<&JsError>), + (Vec<(&TestDescription, &TestFailure)>, Option<&JsError>), > = BTreeMap::default(); let mut failure_titles = vec![]; - for (description, js_error) in &summary.failures { + for (description, failure) in &summary.failures { let (failures, _) = failures_by_origin .entry(description.origin.clone()) .or_default(); - failures.push((description, js_error.as_ref())); + failures.push((description, failure)); } for (origin, js_error) in &summary.uncaught_errors { let (_, uncaught_error) = @@ -544,27 +652,14 @@ impl PrettyTestReporter { } println!("\n{}\n", colors::white_bold_on_red(" ERRORS ")); for (origin, (failures, uncaught_error)) in failures_by_origin { - for (description, js_error) in failures { - let failure_title = format!( - "{} {}", - &description.name, - colors::gray(format!( - "=> {}:{}:{}", - self.to_relative_path_or_remote_url( - &description.location.file_name - ), - description.location.line_number, - description.location.column_number - )) - ); - println!("{}", &failure_title); - println!( - "{}: {}", - colors::red_bold("error"), - format_test_error(js_error) - ); - println!(); - failure_titles.push(failure_title); + for (description, failure) in failures { + if !failure.hide_in_summary() { + let failure_title = self.format_test_for_summary(description); + println!("{}", &failure_title); + println!("{}: {}", colors::red_bold("error"), failure.to_string()); + println!(); + failure_titles.push(failure_title); + } } if let Some(js_error) = uncaught_error { let failure_title = format!( @@ -613,7 +708,7 @@ impl PrettyTestReporter { summary.passed, get_steps_text(summary.passed_steps), summary.failed, - get_steps_text(summary.failed_steps + summary.pending_steps), + get_steps_text(summary.failed_steps), ) .unwrap(); @@ -647,6 +742,98 @@ impl PrettyTestReporter { ); self.in_new_line = true; } + + fn report_sigint( + &mut self, + tests_pending: &HashSet<usize>, + tests: &IndexMap<usize, TestDescription>, + test_steps: &IndexMap<usize, TestStepDescription>, + ) { + if tests_pending.is_empty() { + return; + } + let mut formatted_pending = BTreeSet::new(); + for id in tests_pending { + if let Some(desc) = tests.get(id) { + formatted_pending.insert(self.format_test_for_summary(desc)); + } + if let Some(desc) = test_steps.get(id) { + formatted_pending + .insert(self.format_test_step_for_summary(desc, tests, test_steps)); + } + } + println!( + "\n{} The following tests were pending:\n", + colors::intense_blue("SIGINT") + ); + for entry in formatted_pending { + println!("{}", entry); + } + println!(); + self.in_new_line = true; + } + + fn format_test_step_ancestry( + &self, + desc: &TestStepDescription, + tests: &IndexMap<usize, TestDescription>, + test_steps: &IndexMap<usize, TestStepDescription>, + ) -> String { + let root; + let mut ancestor_names = vec![]; + let mut current_desc = desc; + loop { + if let Some(step_desc) = test_steps.get(¤t_desc.parent_id) { + ancestor_names.push(&step_desc.name); + current_desc = step_desc; + } else { + root = tests.get(¤t_desc.parent_id).unwrap(); + break; + } + } + ancestor_names.reverse(); + let mut result = String::new(); + result.push_str(&root.name); + result.push_str(" ... "); + for name in ancestor_names { + result.push_str(name); + result.push_str(" ... "); + } + result.push_str(&desc.name); + result + } + + fn format_test_for_summary(&self, desc: &TestDescription) -> String { + format!( + "{} {}", + &desc.name, + colors::gray(format!( + "=> {}:{}:{}", + self.to_relative_path_or_remote_url(&desc.location.file_name), + desc.location.line_number, + desc.location.column_number + )) + ) + } + + fn format_test_step_for_summary( + &self, + desc: &TestStepDescription, + tests: &IndexMap<usize, TestDescription>, + test_steps: &IndexMap<usize, TestStepDescription>, + ) -> String { + let long_name = self.format_test_step_ancestry(desc, tests, test_steps); + format!( + "{} {}", + long_name, + colors::gray(format!( + "=> {}:{}:{}", + self.to_relative_path_or_remote_url(&desc.location.file_name), + desc.location.line_number, + desc.location.column_number + )) + ) + } } fn abbreviate_test_error(js_error: &JsError) -> JsError { @@ -1007,6 +1194,12 @@ async fn test_specifiers( let sender = TestEventSender::new(sender); let concurrent_jobs = options.concurrent_jobs; + let sender_ = sender.downgrade(); + let sigint_handler_handle = tokio::task::spawn(async move { + signal::ctrl_c().await.unwrap(); + sender_.upgrade().map(|s| s.send(TestEvent::Sigint).ok()); + }); + let join_handles = specifiers_with_mode .into_iter() @@ -1060,6 +1253,7 @@ async fn test_specifiers( let earlier = Instant::now(); let mut tests = IndexMap::new(); let mut test_steps = IndexMap::new(); + let mut tests_started = HashSet::new(); let mut tests_with_result = HashSet::new(); let mut summary = TestSummary::new(); let mut used_only = false; @@ -1083,7 +1277,9 @@ async fn test_specifiers( } TestEvent::Wait(id) => { - reporter.report_wait(tests.get(&id).unwrap()); + if tests_started.insert(id) { + reporter.report_wait(tests.get(&id).unwrap()); + } } TestEvent::Output(output) => { @@ -1100,9 +1296,11 @@ async fn test_specifiers( TestResult::Ignored => { summary.ignored += 1; } - TestResult::Failed(error) => { + TestResult::Failed(failure) => { summary.failed += 1; - summary.failures.push((description.clone(), error.clone())); + summary + .failures + .push((description.clone(), failure.clone())); } TestResult::Cancelled => { unreachable!("should be handled in TestEvent::UncaughtError"); @@ -1130,36 +1328,65 @@ async fn test_specifiers( } TestEvent::StepWait(id) => { - reporter.report_step_wait(test_steps.get(&id).unwrap()); + if tests_started.insert(id) { + reporter.report_step_wait(test_steps.get(&id).unwrap()); + } } TestEvent::StepResult(id, result, duration) => { - match &result { - TestStepResult::Ok => { - summary.passed_steps += 1; - } - TestStepResult::Ignored => { - summary.ignored_steps += 1; - } - TestStepResult::Failed(_) => { - summary.failed_steps += 1; - } - TestStepResult::Pending(_) => { - summary.pending_steps += 1; + if tests_with_result.insert(id) { + let description = test_steps.get(&id).unwrap(); + match &result { + TestStepResult::Ok => { + summary.passed_steps += 1; + } + TestStepResult::Ignored => { + summary.ignored_steps += 1; + } + TestStepResult::Failed(failure) => { + summary.failed_steps += 1; + summary.failures.push(( + TestDescription { + id: description.id, + name: reporter.format_test_step_ancestry( + description, + &tests, + &test_steps, + ), + origin: description.origin.clone(), + location: description.location.clone(), + }, + failure.clone(), + )) + } } + + reporter.report_step_result( + description, + &result, + duration, + &tests, + &test_steps, + ); } + } - reporter.report_step_result( - test_steps.get(&id).unwrap(), - &result, - duration, + TestEvent::Sigint => { + reporter.report_sigint( + &tests_started + .difference(&tests_with_result) + .copied() + .collect(), &tests, &test_steps, ); + std::process::exit(130); } } } + sigint_handler_handle.abort(); + let elapsed = Instant::now().duration_since(earlier); reporter.report_summary(&summary, &elapsed); @@ -1598,6 +1825,10 @@ impl TestEventSender { Ok(()) } + fn downgrade(&self) -> WeakUnboundedSender<TestEvent> { + self.sender.downgrade() + } + fn flush_stdout_and_stderr(&mut self) -> Result<(), AnyError> { self.stdout_writer.flush()?; self.stderr_writer.flush()?; |