From 2caf921c4c880e3fd259f7e16822372aade90fbf Mon Sep 17 00:00:00 2001 From: RedYetiDev <38299977+RedYetiDev@users.noreply.github.com> Date: Sat, 26 Oct 2024 21:59:31 -0400 Subject: [PATCH] feat(semver-major): Upgrade package to Node.js v23 --- .github/workflows/ci.yml | 10 +- .github/workflows/commitlint.yml | 4 +- .github/workflows/release-please.yml | 7 +- .npmignore | 3 +- .release-please-manifest.json | 3 + MAINTAINING.md | 44 + README.md | 1409 +----- bin/node--test-name-pattern.js | 7 - bin/node--test-only.js | 7 - bin/node--test.js | 7 - bin/node-core-test.js | 92 - example.js | 51 - lib/bootstrap.js | 105 + lib/cli.js | 25 + lib/internal/abort_controller.js | 31 +- lib/internal/assert.js | 3 - lib/internal/assert/utils.js | 9 + lib/internal/console/global.js | 4 +- lib/internal/deps/minimatch/index.js | 1 + lib/internal/error_serdes.js | 192 + lib/internal/errors.js | 669 ++- lib/internal/event_target.js | 5 + lib/internal/events/abort_listener.js | 5 + lib/internal/fs/glob.js | 658 +++ lib/internal/fs/utils.js | 21 + lib/internal/main/test_runner.js | 27 - lib/internal/modules/cjs/loader.js | 3 + lib/internal/modules/esm/loader.js | 19 + lib/internal/options.js | 116 +- lib/internal/per_context/primordials.js | 87 - lib/internal/priority_queue.js | 115 + lib/internal/process/pre_execution.js | 5 - lib/internal/process/task_queues.js | 3 + lib/internal/readline/interface.js | 4 + lib/internal/source_map/source_map_cache.js | 2 + lib/internal/streams/end-of-stream.js | 329 ++ lib/internal/streams/readable.js | 3 + lib/internal/streams/transform.js | 1 + lib/internal/streams/utils.js | 365 ++ lib/internal/test_runner/coverage.js | 687 +++ lib/internal/test_runner/harness.js | 408 +- lib/internal/test_runner/mock.js | 371 -- lib/internal/test_runner/mock/loader.js | 203 + lib/internal/test_runner/mock/mock.js | 806 ++++ lib/internal/test_runner/mock/mock_timers.js | 795 ++++ lib/internal/test_runner/reporter/dot.js | 44 +- lib/internal/test_runner/reporter/junit.js | 160 + lib/internal/test_runner/reporter/lcov.js | 107 + lib/internal/test_runner/reporter/spec.js | 169 +- lib/internal/test_runner/reporter/tap.js | 293 +- lib/internal/test_runner/reporter/utils.js | 93 + .../test_runner/reporter/v8-serializer.js | 47 + lib/internal/test_runner/runner.js | 967 +++- lib/internal/test_runner/snapshot.js | 254 + lib/internal/test_runner/tap_checker.js | 156 - lib/internal/test_runner/tap_lexer.js | 529 --- lib/internal/test_runner/tap_parser.js | 990 ---- lib/internal/test_runner/test.js | 1471 ++++-- lib/internal/test_runner/tests_stream.js | 179 +- lib/internal/test_runner/utils.js | 604 ++- lib/internal/test_runner/yaml_to_js.js | 121 - lib/internal/timers.js | 8 +- lib/internal/url.js | 16 + lib/internal/util.js | 80 +- lib/internal/util/colors.js | 56 +- lib/internal/util/debuglog.js | 3 + lib/internal/util/inspect.js | 7 +- lib/internal/util/inspector.js | 64 +- lib/internal/util/types.js | 4 +- lib/internal/validators.js | 170 +- lib/internal/watch_mode/files_watcher.js | 3 + lib/reporters.js | 47 + lib/test.d.ts | 119 - lib/test.js | 68 +- lib/timers/promises.js | 11 - package-lock.json | 4107 +---------------- package.json | 32 +- test/common/assertSnapshot.js | 104 + test/common/fixtures.js | 62 +- test/common/fixtures.mjs | 17 + test/common/index.js | 347 +- test/common/index.mjs | 36 + test/common/inspector-helper.js | 549 +++ test/common/pseudo-tty.py | 104 + test/common/tmpdir.js | 93 +- test/fixtures/empty.js | 0 test/fixtures/module-mocking/basic-cjs.js | 5 + .../basic-esm-without-extension.js | 1 + test/fixtures/module-mocking/basic-esm.mjs | 1 + .../wrong-import-after-module-mocking.mjs | 9 + test/fixtures/node-core-test/esm.test.mjs | 3 - ...la-with-explicit-exitCode-modification.mjs | 2 - ...ed-tla-with-explicit-process.exit-call.mjs | 2 - .../node-core-test/unfinished-tla.mjs | 1 - test/fixtures/test-runner/.gitignore | 1 - .../aborts/failed-test-still-call-abort.js | 25 + .../successful-test-still-call-abort.js | 23 + .../aborts/wait-for-abort-helper.js | 19 + .../test-runner/async-error-in-test-hook.mjs | 28 + test/fixtures/test-runner/concurrency/a.mjs | 13 + test/fixtures/test-runner/concurrency/b.mjs | 13 + .../test-runner/custom_reporters/coverage.mjs | 15 + .../test-runner/custom_reporters/custom.cjs | 21 +- .../test-runner/custom_reporters/custom.js | 20 +- .../test-runner/custom_reporters/custom.mjs | 11 +- .../custom_reporters/throwing-async.js | 8 + .../test-runner/custom_reporters/throwing.js | 6 + test/fixtures/test-runner/cwd/test.cjs | 4 + .../default-behavior/index.test.js | 4 + .../default-behavior/random.test.mjs | 5 + .../default-behavior/subdir/subdir_test.js | 0 .../default-behavior/test/random.cjs | 4 + .../default-behavior/test/skip_by_name.cjs | 5 + .../extraneous_set_immediate_async.mjs | 7 +- .../extraneous_set_timeout_async.mjs | 7 +- test/fixtures/test-runner/index.js | 5 +- test/fixtures/test-runner/index.test.js | 5 - test/fixtures/test-runner/invalid-tap.js | 4 +- .../test-runner/issue-54726/latest.js | 0 .../test-runner/issue-54726/non-matching.js | 0 .../matching-patterns/index-test.cjs | 4 + .../matching-patterns/index-test.js | 4 + .../matching-patterns/index-test.mjs | 4 + .../matching-patterns/typescript-test.cts | 4 + .../matching-patterns/typescript-test.mts | 4 + .../matching-patterns/typescript-test.ts | 4 + test/fixtures/test-runner/mock-nm.js | 21 + test/fixtures/test-runner/nested.js | 25 +- .../test-runner/never_ending_async.js | 7 +- .../fixtures/test-runner/never_ending_sync.js | 5 +- .../test-runner/no-isolation/global-hooks.js | 6 + .../test-runner/no-isolation/one.test.js | 37 + .../test-runner/no-isolation/two.test.js | 35 + .../test-runner/node_modules/test-nm.js | 3 - .../output/abort-runs-after-hook.js | 14 + .../output/abort-runs-after-hook.snapshot | 25 + .../test-runner/output/abort.js} | 48 +- .../test-runner/output/abort.snapshot} | 83 +- .../test-runner/output/abort_hooks.js | 62 + .../test-runner/output/abort_hooks.snapshot | 255 + .../test-runner/output/abort_suite.js | 26 + .../test-runner/output/abort_suite.snapshot | 167 + .../output/arbitrary-output-colored-1.js | 7 + .../output/arbitrary-output-colored.js | 12 + .../output/arbitrary-output-colored.snapshot | 28 + .../test-runner/output/arbitrary-output.js | 20 + .../output/arbitrary-output.snapshot | Bin 0 -> 550 bytes .../output/async-test-scheduling.mjs | 14 + .../output/async-test-scheduling.snapshot | 37 + ...efore-and-after-each-too-many-listeners.js | 8 + ...and-after-each-too-many-listeners.snapshot | 65 + ...er-each-with-timeout-too-many-listeners.js | 8 + ...h-with-timeout-too-many-listeners.snapshot | 65 + .../test-runner/output/default_output.js | 17 + .../output/default_output.snapshot | 73 + .../test-runner/output/describe_it.js | 392 ++ .../test-runner/output/describe_it.snapshot | 893 ++++ .../test-runner/output/describe_nested.js | 9 + .../output/describe_nested.snapshot} | 3 + .../output/dot_output_custom_columns.js | 21 + .../output/dot_output_custom_columns.snapshot | 3 + .../test-runner/output/dot_reporter.js | 7 + .../test-runner/output/dot_reporter.snapshot | 234 + test/fixtures/test-runner/output/eval_dot.js | 10 + .../test-runner/output/eval_dot.snapshot | 13 + test/fixtures/test-runner/output/eval_spec.js | 10 + .../test-runner/output/eval_spec.snapshot | 31 + test/fixtures/test-runner/output/eval_tap.js | 10 + .../test-runner/output/eval_tap.snapshot | 33 + .../output/filtered-suite-delayed-build.js | 16 + .../filtered-suite-delayed-build.snapshot | 34 + .../output/filtered-suite-order.mjs | 50 + .../output/filtered-suite-order.snapshot | 166 + .../output/filtered-suite-throws.js | 14 + .../output/filtered-suite-throws.snapshot | 78 + .../fixtures/test-runner/output/force_exit.js | 27 + .../test-runner/output/force_exit.snapshot | 16 + .../output/global-hooks-with-no-tests.js | 6 + .../global-hooks-with-no-tests.snapshot | 12 + .../global_after_should_fail_the_test.js | 10 + ...global_after_should_fail_the_test.snapshot | 36 + .../output/hooks-with-no-global-test.js | 78 + .../output/hooks-with-no-global-test.snapshot | 44 + test/fixtures/test-runner/output/hooks.js | 306 ++ .../test-runner/output/hooks.snapshot | 957 ++++ .../test-runner/output/hooks_spec_reporter.js | 11 + .../output/hooks_spec_reporter.snapshot | 791 ++++ .../test-runner/output/lcov_reporter.js | 7 + .../test-runner/output/lcov_reporter.snapshot | 2 + .../output/name_and_skip_patterns.js | 10 + .../output/name_and_skip_patterns.snapshot | 15 + .../test-runner/output/name_pattern.js | 89 + .../test-runner/output/name_pattern.snapshot | 183 + .../output/name_pattern_with_only.js | 13 + .../output/name_pattern_with_only.snapshot | 26 + test/fixtures/test-runner/output/no_refs.js | 12 + .../test-runner/output/no_refs.snapshot} | 17 +- test/fixtures/test-runner/output/no_tests.js | 6 + .../test-runner/output/no_tests.snapshot} | 0 .../output/non-tty-forced-color-output.js | 6 + .../non-tty-forced-color-output.snapshot | 9 + .../fixtures/test-runner/output/only_tests.js | 142 + .../test-runner/output/only_tests.snapshot | 203 + test/fixtures/test-runner/output/output.js | 405 ++ .../test-runner/output/output.snapshot | 965 ++++ .../fixtures/test-runner/output/output_cli.js | 8 + .../test-runner/output/output_cli.snapshot} | 380 +- .../test-runner/output/reset-color-depth.js | 5 + test/fixtures/test-runner/output/single.js | 3 + .../test-runner/output/skip-each-hooks.js | 21 + .../output/skip-each-hooks.snapshot | 11 + .../test-runner/output/skip_pattern.js | 20 + .../test-runner/output/skip_pattern.snapshot | 37 + .../output/source_mapped_locations.mjs | 8 + .../output/source_mapped_locations.mjs.map | 1 + .../output/source_mapped_locations.snapshot | 20 + .../output/source_mapped_locations.ts | 7 + .../test-runner/output/spec_reporter.js | 11 + .../test-runner/output/spec_reporter.snapshot | 598 +++ .../test-runner/output/spec_reporter_cli.js | 11 + .../output/spec_reporter_cli.snapshot | 605 +++ .../output/spec_reporter_successful.js | 6 + .../output/spec_reporter_successful.snapshot | 9 + .../test-runner/output/suite-skip-hooks.js | 60 + .../output/suite-skip-hooks.snapshot | 23 + .../fixtures/test-runner/output/tap_escape.js | 19 + .../test-runner/output/tap_escape.snapshot | 31 + ...agnostic-warning-without-test-only-flag.js | 40 + ...ic-warning-without-test-only-flag.snapshot | 21 + .../test-runner/output/test-runner-plan.js | 79 + .../output/test-runner-plan.snapshot | 137 + ...re_each_should_not_affect_further_tests.js | 46 + ...h_should_not_affect_further_tests.snapshot | 71 + .../output/unfinished-suite-async-error.js | 14 + .../unfinished-suite-async-error.snapshot | 53 + .../test-runner/output/unresolved_promise.js | 7 + .../output/unresolved_promise.snapshot} | 15 +- test/fixtures/test-runner/print-arguments.js | 5 + test/fixtures/test-runner/protoMutation.js | 4 +- test/fixtures/test-runner/random.test.mjs | 6 - test/fixtures/test-runner/recursive_run.js | 7 + test/fixtures/test-runner/reporters.js | 15 +- test/fixtures/test-runner/root-duration.mjs | 8 + test/fixtures/test-runner/run_inspect.js | 40 + .../test-runner/run_inspect_assert.js | 19 + test/fixtures/test-runner/shards/a.cjs | 4 + test/fixtures/test-runner/shards/b.cjs | 4 + test/fixtures/test-runner/shards/c.cjs | 4 + test/fixtures/test-runner/shards/d.cjs | 4 + test/fixtures/test-runner/shards/e.cjs | 4 + test/fixtures/test-runner/shards/f.cjs | 4 + test/fixtures/test-runner/shards/g.cjs | 4 + test/fixtures/test-runner/shards/h.cjs | 4 + test/fixtures/test-runner/shards/i.cjs | 4 + test/fixtures/test-runner/shards/j.cjs | 4 + .../test-runner/snapshots/imported-tests.js | 8 + .../snapshots/malformed-exports.js.snapshot | 1 + .../test-runner/snapshots/simple.js.snapshot | 6 + test/fixtures/test-runner/snapshots/unit-2.js | 11 + test/fixtures/test-runner/snapshots/unit.js | 30 + .../test-runner/subdir/subdir_test.js | 1 - test/fixtures/test-runner/test/random.cjs | 5 - test/fixtures/test-runner/test_only.js | 5 + .../test-runner/throws_sync_and_async.js | 10 + test/fixtures/test-runner/todo_exit_code.js | 15 + test/fixtures/test-runner/user-logs.js | 20 + test/message.js | 141 - test/message/test_runner_abort_suite.js | 28 - test/message/test_runner_abort_suite.out | 99 - test/message/test_runner_describe_nested.js | 11 - test/message/test_runner_desctibe_it.js | 372 -- test/message/test_runner_desctibe_it.out | 647 --- test/message/test_runner_hooks.js | 151 - test/message/test_runner_hooks.out | 500 -- test/message/test_runner_no_refs.js | 14 - test/message/test_runner_no_tests.js | 8 - test/message/test_runner_only_tests.js | 49 - test/message/test_runner_only_tests.out | 126 - test/message/test_runner_output.js | 385 -- test/message/test_runner_output_cli.js | 8 - test/message/test_runner_output_cli.out | 656 --- .../test_runner_output_dot_reporter.js | 7 - .../test_runner_output_dot_reporter.out | 4 - .../test_runner_output_spec_reporter.js | 11 - .../test_runner_output_spec_reporter.out | 260 -- test/message/test_runner_test_name_pattern.js | 48 - .../message/test_runner_test_name_pattern.out | 107 - ...test_runner_test_name_pattern_with_only.js | 14 - ...est_runner_test_name_pattern_with_only.out | 40 - .../message/test_runner_unresolved_promise.js | 9 - test/node-core-test.js | 81 - test/parallel.mjs | 21 - test/parallel/test-runner-aliases.js | 8 + test/parallel/test-runner-assert.js | 19 + test/parallel/test-runner-cli.js | 559 ++- test/parallel/test-runner-concurrency.js | 108 +- .../test-runner-enable-source-maps-issue.js | 16 + test/parallel/test-runner-exit-code.js | 97 +- .../test-runner-extraneous-async-activity.js | 85 +- .../parallel/test-runner-filetest-location.js | 20 + test/parallel/test-runner-filter-warning.js | 11 + .../test-runner-force-exit-failure.js | 25 + test/parallel/test-runner-force-exit-flush.js | 49 + test/parallel/test-runner-import-no-scheme.js | 66 + test/parallel/test-runner-inspect.mjs | 59 + test/parallel/test-runner-misc.js | 52 +- test/parallel/test-runner-mock-timers.js | 1061 +++++ test/parallel/test-runner-mocking.js | 1353 +++--- test/parallel/test-runner-module-mocking.js | 654 +++ ...test-runner-no-isolation-different-cwd.mjs | 35 + .../test-runner-no-isolation-filtering.js | 96 + .../test-runner-no-isolation-hooks.mjs | 59 + test/parallel/test-runner-no-isolation.mjs | 42 + .../parallel/test-runner-option-validation.js | 24 +- test/parallel/test-runner-output.mjs | 202 + test/parallel/test-runner-reporters.js | 232 +- ...st-runner-root-after-with-refed-handles.js | 26 + test/parallel/test-runner-root-duration.js | 26 + .../test-runner-run-files-undefined.mjs | 64 + test/parallel/test-runner-run.mjs | 607 ++- test/parallel/test-runner-snapshot-tests.js | 416 ++ test/parallel/test-runner-string-to-regexp.js | 22 +- .../test-runner-subtest-after-hook.js | 12 + test/parallel/test-runner-tap-checker.js | 120 - test/parallel/test-runner-tap-lexer.js | 447 -- .../parallel/test-runner-tap-parser-stream.js | 818 ---- test/parallel/test-runner-tap-parser.js | 1182 ----- test/parallel/test-runner-test-filepath.js | 52 + test/parallel/test-runner-test-filter.js | 43 - test/parallel/test-runner-test-fullname.js | 24 + test/parallel/test-runner-todo-skip-tests.js | 32 + test/parallel/test-runner-typechecking.js | 36 + test/parallel/test-runner-v8-deserializer.mjs | 109 + 333 files changed, 25558 insertions(+), 17757 deletions(-) create mode 100644 .release-please-manifest.json create mode 100644 MAINTAINING.md delete mode 100644 bin/node--test-name-pattern.js delete mode 100644 bin/node--test-only.js delete mode 100755 bin/node--test.js delete mode 100755 bin/node-core-test.js delete mode 100644 example.js create mode 100644 lib/bootstrap.js create mode 100755 lib/cli.js delete mode 100644 lib/internal/assert.js create mode 100644 lib/internal/assert/utils.js create mode 100644 lib/internal/deps/minimatch/index.js create mode 100644 lib/internal/error_serdes.js create mode 100644 lib/internal/event_target.js create mode 100644 lib/internal/events/abort_listener.js create mode 100644 lib/internal/fs/glob.js create mode 100644 lib/internal/fs/utils.js delete mode 100644 lib/internal/main/test_runner.js create mode 100644 lib/internal/modules/cjs/loader.js create mode 100644 lib/internal/modules/esm/loader.js delete mode 100644 lib/internal/per_context/primordials.js create mode 100644 lib/internal/priority_queue.js delete mode 100644 lib/internal/process/pre_execution.js create mode 100644 lib/internal/process/task_queues.js create mode 100644 lib/internal/readline/interface.js create mode 100644 lib/internal/source_map/source_map_cache.js create mode 100644 lib/internal/streams/end-of-stream.js create mode 100644 lib/internal/streams/readable.js create mode 100644 lib/internal/streams/transform.js create mode 100644 lib/internal/streams/utils.js create mode 100644 lib/internal/test_runner/coverage.js delete mode 100644 lib/internal/test_runner/mock.js create mode 100644 lib/internal/test_runner/mock/loader.js create mode 100644 lib/internal/test_runner/mock/mock.js create mode 100644 lib/internal/test_runner/mock/mock_timers.js create mode 100644 lib/internal/test_runner/reporter/junit.js create mode 100644 lib/internal/test_runner/reporter/lcov.js create mode 100644 lib/internal/test_runner/reporter/utils.js create mode 100644 lib/internal/test_runner/reporter/v8-serializer.js create mode 100644 lib/internal/test_runner/snapshot.js delete mode 100644 lib/internal/test_runner/tap_checker.js delete mode 100644 lib/internal/test_runner/tap_lexer.js delete mode 100644 lib/internal/test_runner/tap_parser.js delete mode 100644 lib/internal/test_runner/yaml_to_js.js create mode 100644 lib/internal/url.js create mode 100644 lib/internal/util/debuglog.js create mode 100644 lib/internal/watch_mode/files_watcher.js create mode 100644 lib/reporters.js delete mode 100644 lib/test.d.ts delete mode 100644 lib/timers/promises.js create mode 100644 test/common/assertSnapshot.js create mode 100644 test/common/fixtures.mjs create mode 100644 test/common/index.mjs create mode 100644 test/common/inspector-helper.js create mode 100644 test/common/pseudo-tty.py create mode 100644 test/fixtures/empty.js create mode 100644 test/fixtures/module-mocking/basic-cjs.js create mode 100644 test/fixtures/module-mocking/basic-esm-without-extension.js create mode 100644 test/fixtures/module-mocking/basic-esm.mjs create mode 100644 test/fixtures/module-mocking/wrong-import-after-module-mocking.mjs delete mode 100644 test/fixtures/node-core-test/esm.test.mjs delete mode 100644 test/fixtures/node-core-test/finished-tla-with-explicit-exitCode-modification.mjs delete mode 100644 test/fixtures/node-core-test/finished-tla-with-explicit-process.exit-call.mjs delete mode 100644 test/fixtures/node-core-test/unfinished-tla.mjs delete mode 100644 test/fixtures/test-runner/.gitignore create mode 100644 test/fixtures/test-runner/aborts/failed-test-still-call-abort.js create mode 100644 test/fixtures/test-runner/aborts/successful-test-still-call-abort.js create mode 100644 test/fixtures/test-runner/aborts/wait-for-abort-helper.js create mode 100644 test/fixtures/test-runner/async-error-in-test-hook.mjs create mode 100644 test/fixtures/test-runner/concurrency/a.mjs create mode 100644 test/fixtures/test-runner/concurrency/b.mjs create mode 100644 test/fixtures/test-runner/custom_reporters/coverage.mjs create mode 100644 test/fixtures/test-runner/custom_reporters/throwing-async.js create mode 100644 test/fixtures/test-runner/custom_reporters/throwing.js create mode 100644 test/fixtures/test-runner/cwd/test.cjs create mode 100644 test/fixtures/test-runner/default-behavior/index.test.js create mode 100644 test/fixtures/test-runner/default-behavior/random.test.mjs create mode 100644 test/fixtures/test-runner/default-behavior/subdir/subdir_test.js create mode 100644 test/fixtures/test-runner/default-behavior/test/random.cjs create mode 100644 test/fixtures/test-runner/default-behavior/test/skip_by_name.cjs delete mode 100644 test/fixtures/test-runner/index.test.js create mode 100644 test/fixtures/test-runner/issue-54726/latest.js create mode 100644 test/fixtures/test-runner/issue-54726/non-matching.js create mode 100644 test/fixtures/test-runner/matching-patterns/index-test.cjs create mode 100644 test/fixtures/test-runner/matching-patterns/index-test.js create mode 100644 test/fixtures/test-runner/matching-patterns/index-test.mjs create mode 100644 test/fixtures/test-runner/matching-patterns/typescript-test.cts create mode 100644 test/fixtures/test-runner/matching-patterns/typescript-test.mts create mode 100644 test/fixtures/test-runner/matching-patterns/typescript-test.ts create mode 100644 test/fixtures/test-runner/mock-nm.js create mode 100644 test/fixtures/test-runner/no-isolation/global-hooks.js create mode 100644 test/fixtures/test-runner/no-isolation/one.test.js create mode 100644 test/fixtures/test-runner/no-isolation/two.test.js delete mode 100644 test/fixtures/test-runner/node_modules/test-nm.js create mode 100644 test/fixtures/test-runner/output/abort-runs-after-hook.js create mode 100644 test/fixtures/test-runner/output/abort-runs-after-hook.snapshot rename test/{message/test_runner_abort.js => fixtures/test-runner/output/abort.js} (59%) rename test/{message/test_runner_abort.out => fixtures/test-runner/output/abort.snapshot} (62%) create mode 100644 test/fixtures/test-runner/output/abort_hooks.js create mode 100644 test/fixtures/test-runner/output/abort_hooks.snapshot create mode 100644 test/fixtures/test-runner/output/abort_suite.js create mode 100644 test/fixtures/test-runner/output/abort_suite.snapshot create mode 100644 test/fixtures/test-runner/output/arbitrary-output-colored-1.js create mode 100644 test/fixtures/test-runner/output/arbitrary-output-colored.js create mode 100644 test/fixtures/test-runner/output/arbitrary-output-colored.snapshot create mode 100644 test/fixtures/test-runner/output/arbitrary-output.js create mode 100644 test/fixtures/test-runner/output/arbitrary-output.snapshot create mode 100644 test/fixtures/test-runner/output/async-test-scheduling.mjs create mode 100644 test/fixtures/test-runner/output/async-test-scheduling.snapshot create mode 100644 test/fixtures/test-runner/output/before-and-after-each-too-many-listeners.js create mode 100644 test/fixtures/test-runner/output/before-and-after-each-too-many-listeners.snapshot create mode 100644 test/fixtures/test-runner/output/before-and-after-each-with-timeout-too-many-listeners.js create mode 100644 test/fixtures/test-runner/output/before-and-after-each-with-timeout-too-many-listeners.snapshot create mode 100644 test/fixtures/test-runner/output/default_output.js create mode 100644 test/fixtures/test-runner/output/default_output.snapshot create mode 100644 test/fixtures/test-runner/output/describe_it.js create mode 100644 test/fixtures/test-runner/output/describe_it.snapshot create mode 100644 test/fixtures/test-runner/output/describe_nested.js rename test/{message/test_runner_describe_nested.out => fixtures/test-runner/output/describe_nested.snapshot} (89%) create mode 100644 test/fixtures/test-runner/output/dot_output_custom_columns.js create mode 100644 test/fixtures/test-runner/output/dot_output_custom_columns.snapshot create mode 100644 test/fixtures/test-runner/output/dot_reporter.js create mode 100644 test/fixtures/test-runner/output/dot_reporter.snapshot create mode 100644 test/fixtures/test-runner/output/eval_dot.js create mode 100644 test/fixtures/test-runner/output/eval_dot.snapshot create mode 100644 test/fixtures/test-runner/output/eval_spec.js create mode 100644 test/fixtures/test-runner/output/eval_spec.snapshot create mode 100644 test/fixtures/test-runner/output/eval_tap.js create mode 100644 test/fixtures/test-runner/output/eval_tap.snapshot create mode 100644 test/fixtures/test-runner/output/filtered-suite-delayed-build.js create mode 100644 test/fixtures/test-runner/output/filtered-suite-delayed-build.snapshot create mode 100644 test/fixtures/test-runner/output/filtered-suite-order.mjs create mode 100644 test/fixtures/test-runner/output/filtered-suite-order.snapshot create mode 100644 test/fixtures/test-runner/output/filtered-suite-throws.js create mode 100644 test/fixtures/test-runner/output/filtered-suite-throws.snapshot create mode 100644 test/fixtures/test-runner/output/force_exit.js create mode 100644 test/fixtures/test-runner/output/force_exit.snapshot create mode 100644 test/fixtures/test-runner/output/global-hooks-with-no-tests.js create mode 100644 test/fixtures/test-runner/output/global-hooks-with-no-tests.snapshot create mode 100644 test/fixtures/test-runner/output/global_after_should_fail_the_test.js create mode 100644 test/fixtures/test-runner/output/global_after_should_fail_the_test.snapshot create mode 100644 test/fixtures/test-runner/output/hooks-with-no-global-test.js create mode 100644 test/fixtures/test-runner/output/hooks-with-no-global-test.snapshot create mode 100644 test/fixtures/test-runner/output/hooks.js create mode 100644 test/fixtures/test-runner/output/hooks.snapshot create mode 100644 test/fixtures/test-runner/output/hooks_spec_reporter.js create mode 100644 test/fixtures/test-runner/output/hooks_spec_reporter.snapshot create mode 100644 test/fixtures/test-runner/output/lcov_reporter.js create mode 100644 test/fixtures/test-runner/output/lcov_reporter.snapshot create mode 100644 test/fixtures/test-runner/output/name_and_skip_patterns.js create mode 100644 test/fixtures/test-runner/output/name_and_skip_patterns.snapshot create mode 100644 test/fixtures/test-runner/output/name_pattern.js create mode 100644 test/fixtures/test-runner/output/name_pattern.snapshot create mode 100644 test/fixtures/test-runner/output/name_pattern_with_only.js create mode 100644 test/fixtures/test-runner/output/name_pattern_with_only.snapshot create mode 100644 test/fixtures/test-runner/output/no_refs.js rename test/{message/test_runner_no_refs.out => fixtures/test-runner/output/no_refs.snapshot} (58%) create mode 100644 test/fixtures/test-runner/output/no_tests.js rename test/{message/test_runner_no_tests.out => fixtures/test-runner/output/no_tests.snapshot} (100%) create mode 100644 test/fixtures/test-runner/output/non-tty-forced-color-output.js create mode 100644 test/fixtures/test-runner/output/non-tty-forced-color-output.snapshot create mode 100644 test/fixtures/test-runner/output/only_tests.js create mode 100644 test/fixtures/test-runner/output/only_tests.snapshot create mode 100644 test/fixtures/test-runner/output/output.js create mode 100644 test/fixtures/test-runner/output/output.snapshot create mode 100644 test/fixtures/test-runner/output/output_cli.js rename test/{message/test_runner_output.out => fixtures/test-runner/output/output_cli.snapshot} (54%) create mode 100644 test/fixtures/test-runner/output/reset-color-depth.js create mode 100644 test/fixtures/test-runner/output/single.js create mode 100644 test/fixtures/test-runner/output/skip-each-hooks.js create mode 100644 test/fixtures/test-runner/output/skip-each-hooks.snapshot create mode 100644 test/fixtures/test-runner/output/skip_pattern.js create mode 100644 test/fixtures/test-runner/output/skip_pattern.snapshot create mode 100644 test/fixtures/test-runner/output/source_mapped_locations.mjs create mode 100644 test/fixtures/test-runner/output/source_mapped_locations.mjs.map create mode 100644 test/fixtures/test-runner/output/source_mapped_locations.snapshot create mode 100644 test/fixtures/test-runner/output/source_mapped_locations.ts create mode 100644 test/fixtures/test-runner/output/spec_reporter.js create mode 100644 test/fixtures/test-runner/output/spec_reporter.snapshot create mode 100644 test/fixtures/test-runner/output/spec_reporter_cli.js create mode 100644 test/fixtures/test-runner/output/spec_reporter_cli.snapshot create mode 100644 test/fixtures/test-runner/output/spec_reporter_successful.js create mode 100644 test/fixtures/test-runner/output/spec_reporter_successful.snapshot create mode 100644 test/fixtures/test-runner/output/suite-skip-hooks.js create mode 100644 test/fixtures/test-runner/output/suite-skip-hooks.snapshot create mode 100644 test/fixtures/test-runner/output/tap_escape.js create mode 100644 test/fixtures/test-runner/output/tap_escape.snapshot create mode 100644 test/fixtures/test-runner/output/test-diagnostic-warning-without-test-only-flag.js create mode 100644 test/fixtures/test-runner/output/test-diagnostic-warning-without-test-only-flag.snapshot create mode 100644 test/fixtures/test-runner/output/test-runner-plan.js create mode 100644 test/fixtures/test-runner/output/test-runner-plan.snapshot create mode 100644 test/fixtures/test-runner/output/timeout_in_before_each_should_not_affect_further_tests.js create mode 100644 test/fixtures/test-runner/output/timeout_in_before_each_should_not_affect_further_tests.snapshot create mode 100644 test/fixtures/test-runner/output/unfinished-suite-async-error.js create mode 100644 test/fixtures/test-runner/output/unfinished-suite-async-error.snapshot create mode 100644 test/fixtures/test-runner/output/unresolved_promise.js rename test/{message/test_runner_unresolved_promise.out => fixtures/test-runner/output/unresolved_promise.snapshot} (57%) create mode 100644 test/fixtures/test-runner/print-arguments.js delete mode 100644 test/fixtures/test-runner/random.test.mjs create mode 100644 test/fixtures/test-runner/recursive_run.js create mode 100644 test/fixtures/test-runner/root-duration.mjs create mode 100644 test/fixtures/test-runner/run_inspect.js create mode 100644 test/fixtures/test-runner/run_inspect_assert.js create mode 100644 test/fixtures/test-runner/shards/a.cjs create mode 100644 test/fixtures/test-runner/shards/b.cjs create mode 100644 test/fixtures/test-runner/shards/c.cjs create mode 100644 test/fixtures/test-runner/shards/d.cjs create mode 100644 test/fixtures/test-runner/shards/e.cjs create mode 100644 test/fixtures/test-runner/shards/f.cjs create mode 100644 test/fixtures/test-runner/shards/g.cjs create mode 100644 test/fixtures/test-runner/shards/h.cjs create mode 100644 test/fixtures/test-runner/shards/i.cjs create mode 100644 test/fixtures/test-runner/shards/j.cjs create mode 100644 test/fixtures/test-runner/snapshots/imported-tests.js create mode 100644 test/fixtures/test-runner/snapshots/malformed-exports.js.snapshot create mode 100644 test/fixtures/test-runner/snapshots/simple.js.snapshot create mode 100644 test/fixtures/test-runner/snapshots/unit-2.js create mode 100644 test/fixtures/test-runner/snapshots/unit.js delete mode 100644 test/fixtures/test-runner/subdir/subdir_test.js delete mode 100644 test/fixtures/test-runner/test/random.cjs create mode 100644 test/fixtures/test-runner/test_only.js create mode 100644 test/fixtures/test-runner/throws_sync_and_async.js create mode 100644 test/fixtures/test-runner/todo_exit_code.js create mode 100644 test/fixtures/test-runner/user-logs.js delete mode 100755 test/message.js delete mode 100644 test/message/test_runner_abort_suite.js delete mode 100644 test/message/test_runner_abort_suite.out delete mode 100644 test/message/test_runner_describe_nested.js delete mode 100644 test/message/test_runner_desctibe_it.js delete mode 100644 test/message/test_runner_desctibe_it.out delete mode 100644 test/message/test_runner_hooks.js delete mode 100644 test/message/test_runner_hooks.out delete mode 100644 test/message/test_runner_no_refs.js delete mode 100644 test/message/test_runner_no_tests.js delete mode 100644 test/message/test_runner_only_tests.js delete mode 100644 test/message/test_runner_only_tests.out delete mode 100644 test/message/test_runner_output.js delete mode 100644 test/message/test_runner_output_cli.js delete mode 100644 test/message/test_runner_output_cli.out delete mode 100644 test/message/test_runner_output_dot_reporter.js delete mode 100644 test/message/test_runner_output_dot_reporter.out delete mode 100644 test/message/test_runner_output_spec_reporter.js delete mode 100644 test/message/test_runner_output_spec_reporter.out delete mode 100644 test/message/test_runner_test_name_pattern.js delete mode 100644 test/message/test_runner_test_name_pattern.out delete mode 100644 test/message/test_runner_test_name_pattern_with_only.js delete mode 100644 test/message/test_runner_test_name_pattern_with_only.out delete mode 100644 test/message/test_runner_unresolved_promise.js delete mode 100644 test/node-core-test.js delete mode 100755 test/parallel.mjs create mode 100644 test/parallel/test-runner-aliases.js create mode 100644 test/parallel/test-runner-assert.js create mode 100644 test/parallel/test-runner-enable-source-maps-issue.js create mode 100644 test/parallel/test-runner-filetest-location.js create mode 100644 test/parallel/test-runner-filter-warning.js create mode 100644 test/parallel/test-runner-force-exit-failure.js create mode 100644 test/parallel/test-runner-force-exit-flush.js create mode 100644 test/parallel/test-runner-import-no-scheme.js create mode 100644 test/parallel/test-runner-inspect.mjs create mode 100644 test/parallel/test-runner-mock-timers.js create mode 100644 test/parallel/test-runner-module-mocking.js create mode 100644 test/parallel/test-runner-no-isolation-different-cwd.mjs create mode 100644 test/parallel/test-runner-no-isolation-filtering.js create mode 100644 test/parallel/test-runner-no-isolation-hooks.mjs create mode 100644 test/parallel/test-runner-no-isolation.mjs create mode 100644 test/parallel/test-runner-output.mjs create mode 100644 test/parallel/test-runner-root-after-with-refed-handles.js create mode 100644 test/parallel/test-runner-root-duration.js create mode 100644 test/parallel/test-runner-run-files-undefined.mjs create mode 100644 test/parallel/test-runner-snapshot-tests.js create mode 100644 test/parallel/test-runner-subtest-after-hook.js delete mode 100644 test/parallel/test-runner-tap-checker.js delete mode 100644 test/parallel/test-runner-tap-lexer.js delete mode 100644 test/parallel/test-runner-tap-parser-stream.js delete mode 100644 test/parallel/test-runner-tap-parser.js create mode 100644 test/parallel/test-runner-test-filepath.js delete mode 100644 test/parallel/test-runner-test-filter.js create mode 100644 test/parallel/test-runner-test-fullname.js create mode 100644 test/parallel/test-runner-todo-skip-tests.js create mode 100644 test/parallel/test-runner-typechecking.js create mode 100644 test/parallel/test-runner-v8-deserializer.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1dd3faa..1b0bc76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,15 +8,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: ['14', '16', '18'] + node: ['18', '20', '22'] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: npm ci - run: npm test - - name: Run test with experimental flag - run: npm test - env: - NODE_OPTIONS: --experimental-abortcontroller --no-warnings diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 4799fc8..83783a4 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -10,9 +10,9 @@ jobs: commitlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 100 - - uses: wagoid/commitlint-github-action@v2 + - uses: wagoid/commitlint-github-action@v6 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index a0ff111..870c827 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -11,18 +11,17 @@ jobs: outputs: release_created: ${{ steps.release.outputs.release_created }} steps: - - uses: google-github-actions/release-please-action@v3 + - uses: googleapis/release-please-action@v4 id: release with: release-type: node - package-name: test npm-publish: needs: release-please if: ${{ needs.release-please.outputs.release_created }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: lts/* registry-url: 'https://registry.npmjs.org' diff --git a/.npmignore b/.npmignore index 8b13789..93121e1 100644 --- a/.npmignore +++ b/.npmignore @@ -1 +1,2 @@ - +test +.github \ No newline at end of file diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..5099974 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "4.0.0" +} \ No newline at end of file diff --git a/MAINTAINING.md b/MAINTAINING.md new file mode 100644 index 0000000..96ac8fb --- /dev/null +++ b/MAINTAINING.md @@ -0,0 +1,44 @@ +# Maintenance Guide for the Package + +This guide outlines the procedures necessary for maintaining this package, ensuring its functionality and compatibility with newer versions of Node.js. The goal is to streamline the maintenance process until a majority of users can transition to the latest versions, paving the way for eventual deprecation of this library. + +## Overview + +Maintaining this library involves updating specific internal files and ensuring that all references are correctly modified. The steps below provide a clear pathway for effective package maintenance. + +## Maintenance Steps + +### 1. Identify Files for Update +Start by identifying the internal files that require updates. These files are typically located in the `lib/internal/` directory. For example, `lib/internal/test_runner/runner.js` is one file that may need attention. + +### 2. Update File Contents +- Replace the entire contents of the identified file with the updated version from your reference source. Ensure you use the correct version that corresponds to the changes made in Node.js internals. + +### 3. Modify Require Statements +- After replacing the file contents, locate all instances of the following pattern in the file: + ```javascript + require('internal/...'); + ``` +- Update these instances to the new syntax: + ```javascript + require('#internal/...'); + ``` + +### 4. Add Necessary Imports +- If the updated file requires specific bindings, include the following line at the top of the file: + ```javascript + const { primordials, internalBinding } = require('#lib/bootstrap'); + ``` + +### 5. Follow Special Comments +- Pay close attention to any comments formatted as `/* NOTE(Author): ... */` within the files. These notes may contain essential guidelines or considerations regarding the code. Adhere to any instructions provided in these comments during the update process. + +### 6. Implement Polyfills as Needed +- When updating the library, you may encounter new features that require polyfilling if they are not present in the library. Add the minimal amount of code necessary for functionality. For instance, in `lib/internal/options`, only implement parsing for the options that are actually needed. + +## Final Steps + +- After completing the updates, conduct thorough testing of the package to ensure all functionality works as expected with the new changes. +- Document any significant modifications made during the update process to maintain a clear history for future reference. + +If you have any questions about this document, it was written by @RedYetiDev. \ No newline at end of file diff --git a/README.md b/README.md index 44d70f6..3775163 100644 --- a/README.md +++ b/README.md @@ -1,1402 +1,17 @@ -# The `test` npm package +# The `test` npm Package [![CI](https://github.com/nodejs/node-core-test/actions/workflows/ci.yml/badge.svg)](https://github.com/nodejs/node-core-test/actions/workflows/ci.yml) -This is a user-land port of [`node:test`](https://nodejs.org/api/test.html), -the experimental test runner introduced in Node.js 18. This module makes it -available in Node.js 14 and later. +This package is a user-land implementation of [`node:test`](https://nodejs.org/api/test.html), the experimental test runner introduced in Node.js 23. It provides compatibility for Node.js 18 and later. -Minimal dependencies, with full test suite. +### Key Features: +- Minimal dependencies. +- Complete test suite. -Differences from the core implementation: - -- Doesn't hide its own stack frames. -- Some features require the use of `--experimental-abortcontroller` CLI flag to - work on Node.js v14.x. It's recommended to pass - `NODE_OPTIONS='--experimental-abortcontroller --no-warnings'` in your env if - you are testing on v14.x. - -## Docs - -### Test runner - -> Stability: 1 - Experimental - - - -The `node:test` module facilitates the creation of JavaScript tests. -To access it: - -```mjs -import test from 'test' -``` - -```cjs -const test = require('test') -``` - -Tests created via the `test` module consist of a single function that is -processed in one of three ways: - -1. A synchronous function that is considered failing if it throws an exception, - and is considered passing otherwise. -2. A function that returns a `Promise` that is considered failing if the - `Promise` rejects, and is considered passing if the `Promise` resolves. -3. A function that receives a callback function. If the callback receives any - truthy value as its first argument, the test is considered failing. If a - falsy value is passed as the first argument to the callback, the test is - considered passing. If the test function receives a callback function and - also returns a `Promise`, the test will fail. - -The following example illustrates how tests are written using the -`test` module. - -```js -test('synchronous passing test', t => { - // This test passes because it does not throw an exception. - assert.strictEqual(1, 1) -}) - -test('synchronous failing test', t => { - // This test fails because it throws an exception. - assert.strictEqual(1, 2) -}) - -test('asynchronous passing test', async t => { - // This test passes because the Promise returned by the async - // function is not rejected. - assert.strictEqual(1, 1) -}) - -test('asynchronous failing test', async t => { - // This test fails because the Promise returned by the async - // function is rejected. - assert.strictEqual(1, 2) -}) - -test('failing test using Promises', t => { - // Promises can be used directly as well. - return new Promise((resolve, reject) => { - setImmediate(() => { - reject(new Error('this will cause the test to fail')) - }) - }) -}) - -test('callback passing test', (t, done) => { - // done() is the callback function. When the setImmediate() runs, it invokes - // done() with no arguments. - setImmediate(done) -}) - -test('callback failing test', (t, done) => { - // When the setImmediate() runs, done() is invoked with an Error object and - // the test fails. - setImmediate(() => { - done(new Error('callback failure')) - }) -}) -``` - -If any tests fail, the process exit code is set to `1`. - -#### Subtests - -The test context's `test()` method allows subtests to be created. This method -behaves identically to the top level `test()` function. The following example -demonstrates the creation of a top level test with two subtests. - -```js -test('top level test', async t => { - await t.test('subtest 1', t => { - assert.strictEqual(1, 1) - }) - - await t.test('subtest 2', t => { - assert.strictEqual(2, 2) - }) -}) -``` - -In this example, `await` is used to ensure that both subtests have completed. -This is necessary because parent tests do not wait for their subtests to -complete. Any subtests that are still outstanding when their parent finishes -are cancelled and treated as failures. Any subtest failures cause the parent -test to fail. - -## Skipping tests - -Individual tests can be skipped by passing the `skip` option to the test, or by -calling the test context's `skip()` method as shown in the -following example. - -```js -// The skip option is used, but no message is provided. -test('skip option', { skip: true }, t => { - // This code is never executed. -}) - -// The skip option is used, and a message is provided. -test('skip option with message', { skip: 'this is skipped' }, t => { - // This code is never executed. -}) - -test('skip() method', t => { - // Make sure to return here as well if the test contains additional logic. - t.skip() -}) - -test('skip() method with message', t => { - // Make sure to return here as well if the test contains additional logic. - t.skip('this is skipped') -}) -``` - -## `describe`/`it` syntax - -Running tests can also be done using `describe` to declare a suite -and `it` to declare a test. -A suite is used to organize and group related tests together. -`it` is an alias for `test`, except there is no test context passed, -since nesting is done using suites. - -```js -describe('A thing', () => { - it('should work', () => { - assert.strictEqual(1, 1); - }); - - it('should be ok', () => { - assert.strictEqual(2, 2); - }); - - describe('a nested thing', () => { - it('should work', () => { - assert.strictEqual(3, 3); - }); - }); -}); -``` - -`describe` and `it` are imported from the `test` module. - -```mjs -import { describe, it } from 'test'; -``` - -```cjs -const { describe, it } = require('test'); -``` - -### `only` tests - -If `node--test` is started with the `--test-only` command-line option, it is -possible to skip all top level tests except for a selected subset by passing -the `only` option to the tests that should be run. When a test with the `only` -option set is run, all subtests are also run. The test context's `runOnly()` -method can be used to implement the same behavior at the subtest level. - -```js -// Assume node--test is run with the --test-only command-line option. -// The 'only' option is set, so this test is run. -test('this test is run', { only: true }, async t => { - // Within this test, all subtests are run by default. - await t.test('running subtest') - - // The test context can be updated to run subtests with the 'only' option. - t.runOnly(true) - await t.test('this subtest is now skipped') - await t.test('this subtest is run', { only: true }) - - // Switch the context back to execute all tests. - t.runOnly(false) - await t.test('this subtest is now run') - - // Explicitly do not run these tests. - await t.test('skipped subtest 3', { only: false }) - await t.test('skipped subtest 4', { skip: true }) -}) - -// The 'only' option is not set, so this test is skipped. -test('this test is not run', () => { - // This code is not run. - throw new Error('fail') -}) -``` - -## Filtering tests by name - -The [`--test-name-pattern`][] command-line option can be used to only run tests -whose name matches the provided pattern. Test name patterns are interpreted as -JavaScript regular expressions. The `--test-name-pattern` option can be -specified multiple times in order to run nested tests. For each test that is -executed, any corresponding test hooks, such as `beforeEach()`, are also -run. - -Given the following test file, starting Node.js with the -`--test-name-pattern="test [1-3]"` option would cause the test runner to execute -`test 1`, `test 2`, and `test 3`. If `test 1` did not match the test name -pattern, then its subtests would not execute, despite matching the pattern. The -same set of tests could also be executed by passing `--test-name-pattern` -multiple times (e.g. `--test-name-pattern="test 1"`, -`--test-name-pattern="test 2"`, etc.). - -```js -test('test 1', async (t) => { - await t.test('test 2'); - await t.test('test 3'); -}); -test('Test 4', async (t) => { - await t.test('Test 5'); - await t.test('test 6'); -}); -``` - -Test name patterns can also be specified using regular expression literals. This -allows regular expression flags to be used. In the previous example, starting -Node.js with `--test-name-pattern="/test [4-5]/i"` would match `Test 4` and -`Test 5` because the pattern is case-insensitive. - -Test name patterns do not change the set of files that the test runner executes. - -## Extraneous asynchronous activity - -Once a test function finishes executing, the results are reported as quickly -as possible while maintaining the order of the tests. However, it is possible -for the test function to generate asynchronous activity that outlives the test -itself. The test runner handles this type of activity, but does not delay the -reporting of test results in order to accommodate it. - -In the following example, a test completes with two `setImmediate()` -operations still outstanding. The first `setImmediate()` attempts to create a -new subtest. Because the parent test has already finished and output its -results, the new subtest is immediately marked as failed, and reported later -to the {TestsStream}. - -The second `setImmediate()` creates an `uncaughtException` event. -`uncaughtException` and `unhandledRejection` events originating from a completed -test are marked as failed by the `test` module and reported as diagnostic -warnings at the top level by the {TestsStream}. - -```js -test('a test that creates asynchronous activity', t => { - setImmediate(() => { - t.test('subtest that is created too late', t => { - throw new Error('error1') - }) - }) - - setImmediate(() => { - throw new Error('error2') - }) - - // The test finishes after this line. -}) -``` - -## Running tests from the command line - -The Node.js test runner can be invoked from the command line: - -```bash -node--test -``` - -By default, Node.js will recursively search the current directory for -JavaScript source files matching a specific naming convention. Matching files -are executed as test files. More information on the expected test file naming -convention and behavior can be found in the [test runner execution model][] -section. - -Alternatively, one or more paths can be provided as the final argument(s) to -the Node.js command, as shown below. - -```bash -node--test test1.js test2.mjs custom_test_dir/ -node--test test1.js test2.mjs custom_test_dir/ -``` - -In this example, the test runner will execute the files `test1.js` and -`test2.mjs`. The test runner will also recursively search the -`custom_test_dir/` directory for test files to execute. - -### Test runner execution model - -When searching for test files to execute, the test runner behaves as follows: - -- Any files explicitly provided by the user are executed. -- If the user did not explicitly specify any paths, the current working - directory is recursively searched for files as specified in the following - steps. -- `node_modules` directories are skipped unless explicitly provided by the - user. -- If a directory named `test` is encountered, the test runner will search it - recursively for all all `.js`, `.cjs`, and `.mjs` files. All of these files - are treated as test files, and do not need to match the specific naming - convention detailed below. This is to accommodate projects that place all of - their tests in a single `test` directory. -- In all other directories, `.js`, `.cjs`, and `.mjs` files matching the - following patterns are treated as test files: - - `^test$` - Files whose basename is the string `'test'`. Examples: - `test.js`, `test.cjs`, `test.mjs`. - - `^test-.+` - Files whose basename starts with the string `'test-'` - followed by one or more characters. Examples: `test-example.js`, - `test-another-example.mjs`. - - `.+[\.\-\_]test$` - Files whose basename ends with `.test`, `-test`, or - `_test`, preceded by one or more characters. Examples: `example.test.js`, - `example-test.cjs`, `example_test.mjs`. - - Other file types understood by Node.js such as `.node` and `.json` are not - automatically executed by the test runner, but are supported if explicitly - provided on the command line. - -Each matching test file is executed in a separate child process. If the child -process finishes with an exit code of 0, the test is considered passing. -Otherwise, the test is considered to be a failure. Test files must be -executable by Node.js, but are not required to use the `node:test` module -internally. - -## Mocking - -The `node:test` module supports mocking during testing via a top-level `mock` -object. The following example creates a spy on a function that adds two numbers -together. The spy is then used to assert that the function was called as -expected. - -```mjs -import assert from 'node:assert'; -import { mock, test } from 'test'; -test('spies on a function', () => { - const sum = mock.fn((a, b) => { - return a + b; - }); - assert.strictEqual(sum.mock.calls.length, 0); - assert.strictEqual(sum(3, 4), 7); - assert.strictEqual(sum.mock.calls.length, 1); - const call = sum.mock.calls[0]; - assert.deepStrictEqual(call.arguments, [3, 4]); - assert.strictEqual(call.result, 7); - assert.strictEqual(call.error, undefined); - // Reset the globally tracked mocks. - mock.reset(); -}); -``` - -```cjs -'use strict'; -const assert = require('node:assert'); -const { mock, test } = require('test'); -test('spies on a function', () => { - const sum = mock.fn((a, b) => { - return a + b; - }); - assert.strictEqual(sum.mock.calls.length, 0); - assert.strictEqual(sum(3, 4), 7); - assert.strictEqual(sum.mock.calls.length, 1); - const call = sum.mock.calls[0]; - assert.deepStrictEqual(call.arguments, [3, 4]); - assert.strictEqual(call.result, 7); - assert.strictEqual(call.error, undefined); - // Reset the globally tracked mocks. - mock.reset(); -}); -``` - -The same mocking functionality is also exposed on the [`TestContext`][] object -of each test. The following example creates a spy on an object method using the -API exposed on the `TestContext`. The benefit of mocking via the test context is -that the test runner will automatically restore all mocked functionality once -the test finishes. - -```js -test('spies on an object method', (t) => { - const number = { - value: 5, - add(a) { - return this.value + a; - }, - }; - t.mock.method(number, 'add'); - assert.strictEqual(number.add.mock.calls.length, 0); - assert.strictEqual(number.add(3), 8); - assert.strictEqual(number.add.mock.calls.length, 1); - const call = number.add.mock.calls[0]; - assert.deepStrictEqual(call.arguments, [3]); - assert.strictEqual(call.result, 8); - assert.strictEqual(call.target, undefined); - assert.strictEqual(call.this, number); -}); -``` - - -## Test reporters - - - -The `node:test` module supports passing [`--test-reporter`][] -flags for the test runner to use a specific reporter. - -The following built-reporters are supported: - -* `tap` - The `tap` reporter is the default reporter used by the test runner. It outputs - the test results in the [TAP][] format. - -* `spec` - The `spec` reporter outputs the test results in a human-readable format. - -* `dot` - The `dot` reporter outputs the test results in a compact format, - where each passing test is represented by a `.`, - and each failing test is represented by a `X`. - -### Custom reporters - -[`--test-reporter`][] can be used to specify a path to custom reporter. -a custom reporter is a module that exports a value -accepted by [stream.compose][]. -Reporters should transform events emitted by a {TestsStream} - -Example of a custom reporter using {stream.Transform}: - -```mjs -import { Transform } from 'node:stream'; -const customReporter = new Transform({ - writableObjectMode: true, - transform(event, encoding, callback) { - switch (event.type) { - case 'test:start': - callback(null, `test ${event.data.name} started`); - break; - case 'test:pass': - callback(null, `test ${event.data.name} passed`); - break; - case 'test:fail': - callback(null, `test ${event.data.name} failed`); - break; - case 'test:plan': - callback(null, 'test plan'); - break; - case 'test:diagnostic': - callback(null, event.data.message); - break; - } - }, -}); -export default customReporter; -``` - -```cjs -const { Transform } = require('node:stream'); -const customReporter = new Transform({ - writableObjectMode: true, - transform(event, encoding, callback) { - switch (event.type) { - case 'test:start': - callback(null, `test ${event.data.name} started`); - break; - case 'test:pass': - callback(null, `test ${event.data.name} passed`); - break; - case 'test:fail': - callback(null, `test ${event.data.name} failed`); - break; - case 'test:plan': - callback(null, 'test plan'); - break; - case 'test:diagnostic': - callback(null, event.data.message); - break; - } - }, -}); -module.exports = customReporter; -``` - -Example of a custom reporter using a generator function: - -```mjs -export default async function * customReporter(source) { - for await (const event of source) { - switch (event.type) { - case 'test:start': - yield `test ${event.data.name} started\n`; - break; - case 'test:pass': - yield `test ${event.data.name} passed\n`; - break; - case 'test:fail': - yield `test ${event.data.name} failed\n`; - break; - case 'test:plan': - yield 'test plan'; - break; - case 'test:diagnostic': - yield `${event.data.message}\n`; - break; - } - } -} -``` - -```cjs -module.exports = async function * customReporter(source) { - for await (const event of source) { - switch (event.type) { - case 'test:start': - yield `test ${event.data.name} started\n`; - break; - case 'test:pass': - yield `test ${event.data.name} passed\n`; - break; - case 'test:fail': - yield `test ${event.data.name} failed\n`; - break; - case 'test:plan': - yield 'test plan\n'; - break; - case 'test:diagnostic': - yield `${event.data.message}\n`; - break; - } - } -}; -``` - -The value provided to `--test-reporter` should be a string like one used in an -`import()` in JavaScript code, or a value provided for [`--import`][]. - -### Multiple reporters - -The [`--test-reporter`][] flag can be specified multiple times to report test -results in several formats. In this situation -it is required to specify a destination for each reporter -using [`--test-reporter-destination`][]. -Destination can be `stdout`, `stderr`, or a file path. -Reporters and destinations are paired according -to the order they were specified. - -In the following example, the `spec` reporter will output to `stdout`, -and the `dot` reporter will output to `file.txt`: - -```bash -node --test-reporter=spec --test-reporter=dot --test-reporter-destination=stdout --test-reporter-destination=file.txt -``` - -When a single reporter is specified, the destination will default to `stdout`, -unless a destination is explicitly provided. - -## `run([options])` - - - -* `options` {Object} Configuration options for running tests. The following - properties are supported: - * `concurrency` {number|boolean} If a number is provided, - then that many files would run in parallel. - If `true`, it would run `os.availableParallelism() - 1` test files in - parallel. - If `false`, it would only run one test file at a time. - **Default:** `false`. - * `files`: {Array} An array containing the list of files to run. - **Default** matching files from [test runner execution model][]. - * `signal` {AbortSignal} Allows aborting an in-progress test execution. - * `timeout` {number} A number of milliseconds the test execution will - fail after. - If unspecified, subtests inherit this value from their parent. - **Default:** `Infinity`. - * `inspectPort` {number|Function} Sets inspector port of test child process. - This can be a number, or a function that takes no arguments and returns a - number. If a nullish value is provided, each process gets its own port, - incremented from the primary's `process.debugPort`. - **Default:** `undefined`. - -* Returns: {TestsStream} - -```js -run({ files: [path.resolve('./tests/test.js')] }) - .pipe(process.stdout); -``` - -## `test([name][, options][, fn])` - -- `name` {string} The name of the test, which is displayed when reporting test - results. **Default:** The `name` property of `fn`, or `''` if `fn` - does not have a name. -- `options` {Object} Configuration options for the test. The following - properties are supported: - - `concurrency` {number|boolean} If a number is provided, - then that many tests would run in parallel. - If `true`, it would run `os.availableParallelism() - 1` tests in parallel. - For subtests, it will be `Infinity` tests in parallel. - If `false`, it would only run one test at a time. - If unspecified, subtests inherit this value from their parent. - **Default:** `false`. - - `only` {boolean} If truthy, and the test context is configured to run - `only` tests, then this test will be run. Otherwise, the test is skipped. - **Default:** `false`. - - `signal` {AbortSignal} Allows aborting an in-progress test. - - `skip` {boolean|string} If truthy, the test is skipped. If a string is - provided, that string is displayed in the test results as the reason for - skipping the test. **Default:** `false`. - - `todo` {boolean|string} If truthy, the test marked as `TODO`. If a string - is provided, that string is displayed in the test results as the reason why - the test is `TODO`. **Default:** `false`. - - `timeout` {number} A number of milliseconds the test will fail after. - If unspecified, subtests inherit this value from their parent. - **Default:** `Infinity`. -- `fn` {Function|AsyncFunction} The function under test. The first argument - to this function is a [`TestContext`][] object. If the test uses callbacks, - the callback function is passed as the second argument. **Default:** A no-op - function. -- Returns: {Promise} Resolved with `undefined` once the test completes. - -The `test()` function is the value imported from the `test` module. Each -invocation of this function results in reporting the test to the {TestsStream}. - -The `TestContext` object passed to the `fn` argument can be used to perform -actions related to the current test. Examples include skipping the test, adding -additional diagnostic information, or creating subtests. - -`test()` returns a `Promise` that resolves once the test completes. The return -value can usually be discarded for top level tests. However, the return value -from subtests should be used to prevent the parent test from finishing first -and cancelling the subtest as shown in the following example. - -```js -test('top level test', async t => { - // The setTimeout() in the following subtest would cause it to outlive its - // parent test if 'await' is removed on the next line. Once the parent test - // completes, it will cancel any outstanding subtests. - await t.test('longer running subtest', async t => { - return new Promise((resolve, reject) => { - setTimeout(resolve, 1000) - }) - }) -}) -``` - -The `timeout` option can be used to fail the test if it takes longer than -`timeout` milliseconds to complete. However, it is not a reliable mechanism for -canceling tests because a running test might block the application thread and -thus prevent the scheduled cancellation. - -## `describe([name][, options][, fn])` - -* `name` {string} The name of the suite, which is displayed when reporting test - results. **Default:** The `name` property of `fn`, or `''` if `fn` - does not have a name. -* `options` {Object} Configuration options for the suite. - supports the same options as `test([name][, options][, fn])`. -* `fn` {Function|AsyncFunction} The function under suite - declaring all subtests and subsuites. - The first argument to this function is a [`SuiteContext`][] object. - **Default:** A no-op function. -* Returns: `undefined`. - -The `describe()` function imported from the `test` module. Each -invocation of this function results in the creation of a Subtest. -After invocation of top level `describe` functions, -all top level tests and suites will execute. - -## `describe.skip([name][, options][, fn])` - -Shorthand for skipping a suite, same as [`describe([name], { skip: true }[, fn])`][describe options]. - -## `describe.todo([name][, options][, fn])` - -Shorthand for marking a suite as `TODO`, same as -[`describe([name], { todo: true }[, fn])`][describe options]. - -## `it([name][, options][, fn])` - -* `name` {string} The name of the test, which is displayed when reporting test - results. **Default:** The `name` property of `fn`, or `''` if `fn` - does not have a name. -* `options` {Object} Configuration options for the suite. - supports the same options as `test([name][, options][, fn])`. -* `fn` {Function|AsyncFunction} The function under test. - If the test uses callbacks, the callback function is passed as an argument. - **Default:** A no-op function. -* Returns: `undefined`. - -The `it()` function is the value imported from the `test` module. - -## `it.skip([name][, options][, fn])` - -Shorthand for skipping a test, -same as [`it([name], { skip: true }[, fn])`][it options]. - -## `it.todo([name][, options][, fn])` - -Shorthand for marking a test as `TODO`, -same as [`it([name], { todo: true }[, fn])`][it options]. - -### `before([, fn][, options])` - -* `fn` {Function|AsyncFunction} The hook function. - If the hook uses callbacks, - the callback function is passed as the second argument. **Default:** A no-op - function. -* `options` {Object} Configuration options for the hook. The following - properties are supported: - * `signal` {AbortSignal} Allows aborting an in-progress hook. - * `timeout` {number} A number of milliseconds the hook will fail after. - If unspecified, subtests inherit this value from their parent. - **Default:** `Infinity`. - -This function is used to create a hook running before running a suite. - -```js -describe('tests', async () => { - before(() => console.log('about to run some test')); - it('is a subtest', () => { - assert.ok('some relevant assertion here'); - }); -}); -``` - -### `after([, fn][, options])` - -* `fn` {Function|AsyncFunction} The hook function. - If the hook uses callbacks, - the callback function is passed as the second argument. **Default:** A no-op - function. -* `options` {Object} Configuration options for the hook. The following - properties are supported: - * `signal` {AbortSignal} Allows aborting an in-progress hook. - * `timeout` {number} A number of milliseconds the hook will fail after. - If unspecified, subtests inherit this value from their parent. - **Default:** `Infinity`. - -This function is used to create a hook running after running a suite. - -```js -describe('tests', async () => { - after(() => console.log('finished running tests')); - it('is a subtest', () => { - assert.ok('some relevant assertion here'); - }); -}); -``` - -### `beforeEach([, fn][, options])` - -* `fn` {Function|AsyncFunction} The hook function. - If the hook uses callbacks, - the callback function is passed as the second argument. **Default:** A no-op - function. -* `options` {Object} Configuration options for the hook. The following - properties are supported: - * `signal` {AbortSignal} Allows aborting an in-progress hook. - * `timeout` {number} A number of milliseconds the hook will fail after. - If unspecified, subtests inherit this value from their parent. - **Default:** `Infinity`. - -This function is used to create a hook running -before each subtest of the current suite. - -```js -describe('tests', async () => { - beforeEach(() => t.diagnostics('about to run a test')); - it('is a subtest', () => { - assert.ok('some relevant assertion here'); - }); -}); -``` - -### `afterEach([, fn][, options])` - -* `fn` {Function|AsyncFunction} The hook function. - If the hook uses callbacks, - the callback function is passed as the second argument. **Default:** A no-op - function. -* `options` {Object} Configuration options for the hook. The following - properties are supported: - * `signal` {AbortSignal} Allows aborting an in-progress hook. - * `timeout` {number} A number of milliseconds the hook will fail after. - If unspecified, subtests inherit this value from their parent. - **Default:** `Infinity`. - -This function is used to create a hook running -after each subtest of the current test. - -```js -describe('tests', async () => { - afterEach(() => t.diagnostics('about to run a test')); - it('is a subtest', () => { - assert.ok('some relevant assertion here'); - }); -}); -``` - -## Class: `MockFunctionContext` - - - -The `MockFunctionContext` class is used to inspect or manipulate the behavior of -mocks created via the [`MockTracker`][] APIs. - -### `ctx.calls` - - - -* {Array} - -A getter that returns a copy of the internal array used to track calls to the -mock. Each entry in the array is an object with the following properties. - -* `arguments` {Array} An array of the arguments passed to the mock function. -* `error` {any} If the mocked function threw then this property contains the - thrown value. **Default:** `undefined`. -* `result` {any} The value returned by the mocked function. -* `stack` {Error} An `Error` object whose stack can be used to determine the - callsite of the mocked function invocation. -* `target` {Function|undefined} If the mocked function is a constructor, this - field contains the class being constructed. Otherwise this will be - `undefined`. -* `this` {any} The mocked function's `this` value. - -### `ctx.callCount()` - - - -* Returns: {integer} The number of times that this mock has been invoked. - -This function returns the number of times that this mock has been invoked. This -function is more efficient than checking `ctx.calls.length` because `ctx.calls` -is a getter that creates a copy of the internal call tracking array. - -### `ctx.mockImplementation(implementation)` - - - -* `implementation` {Function|AsyncFunction} The function to be used as the - mock's new implementation. - -This function is used to change the behavior of an existing mock. - -The following example creates a mock function using `t.mock.fn()`, calls the -mock function, and then changes the mock implementation to a different function. - -```js -test('changes a mock behavior', (t) => { - let cnt = 0; - function addOne() { - cnt++; - return cnt; - } - function addTwo() { - cnt += 2; - return cnt; - } - const fn = t.mock.fn(addOne); - assert.strictEqual(fn(), 1); - fn.mock.mockImplementation(addTwo); - assert.strictEqual(fn(), 3); - assert.strictEqual(fn(), 5); -}); -``` - -### `ctx.mockImplementationOnce(implementation[, onCall])` - - - -* `implementation` {Function|AsyncFunction} The function to be used as the - mock's implementation for the invocation number specified by `onCall`. -* `onCall` {integer} The invocation number that will use `implementation`. If - the specified invocation has already occurred then an exception is thrown. - **Default:** The number of the next invocation. - -This function is used to change the behavior of an existing mock for a single -invocation. Once invocation `onCall` has occurred, the mock will revert to -whatever behavior it would have used had `mockImplementationOnce()` not been -called. - -The following example creates a mock function using `t.mock.fn()`, calls the -mock function, changes the mock implementation to a different function for the -next invocation, and then resumes its previous behavior. - -```js -test('changes a mock behavior once', (t) => { - let cnt = 0; - function addOne() { - cnt++; - return cnt; - } - function addTwo() { - cnt += 2; - return cnt; - } - const fn = t.mock.fn(addOne); - assert.strictEqual(fn(), 1); - fn.mock.mockImplementationOnce(addTwo); - assert.strictEqual(fn(), 3); - assert.strictEqual(fn(), 4); -}); -``` - -### `ctx.restore()` - - - -Resets the implementation of the mock function to its original behavior. The -mock can still be used after calling this function. - -## Class: `MockTracker` - - - -The `MockTracker` class is used to manage mocking functionality. The test runner -module provides a top level `mock` export which is a `MockTracker` instance. -Each test also provides its own `MockTracker` instance via the test context's -`mock` property. - -### `mock.fn([original[, implementation]][, options])` - - - -* `original` {Function|AsyncFunction} An optional function to create a mock on. - **Default:** A no-op function. -* `implementation` {Function|AsyncFunction} An optional function used as the - mock implementation for `original`. This is useful for creating mocks that - exhibit one behavior for a specified number of calls and then restore the - behavior of `original`. **Default:** The function specified by `original`. -* `options` {Object} Optional configuration options for the mock function. The - following properties are supported: - * `times` {integer} The number of times that the mock will use the behavior of - `implementation`. Once the mock function has been called `times` times, it - will automatically restore the behavior of `original`. This value must be an - integer greater than zero. **Default:** `Infinity`. -* Returns: {Proxy} The mocked function. The mocked function contains a special - `mock` property, which is an instance of [`MockFunctionContext`][], and can - be used for inspecting and changing the behavior of the mocked function. - -This function is used to create a mock function. - -The following example creates a mock function that increments a counter by one -on each invocation. The `times` option is used to modify the mock behavior such -that the first two invocations add two to the counter instead of one. - -```js -test('mocks a counting function', (t) => { - let cnt = 0; - function addOne() { - cnt++; - return cnt; - } - function addTwo() { - cnt += 2; - return cnt; - } - const fn = t.mock.fn(addOne, addTwo, { times: 2 }); - assert.strictEqual(fn(), 2); - assert.strictEqual(fn(), 4); - assert.strictEqual(fn(), 5); - assert.strictEqual(fn(), 6); -}); -``` - -### `mock.getter(object, methodName[, implementation][, options])` - - - -This function is syntax sugar for [`MockTracker.method`][] with `options.getter` -set to `true`. - -### `mock.method(object, methodName[, implementation][, options])` - - - -* `object` {Object} The object whose method is being mocked. -* `methodName` {string|symbol} The identifier of the method on `object` to mock. - If `object[methodName]` is not a function, an error is thrown. -* `implementation` {Function|AsyncFunction} An optional function used as the - mock implementation for `object[methodName]`. **Default:** The original method - specified by `object[methodName]`. -* `options` {Object} Optional configuration options for the mock method. The - following properties are supported: - * `getter` {boolean} If `true`, `object[methodName]` is treated as a getter. - This option cannot be used with the `setter` option. **Default:** false. - * `setter` {boolean} If `true`, `object[methodName]` is treated as a setter. - This option cannot be used with the `getter` option. **Default:** false. - * `times` {integer} The number of times that the mock will use the behavior of - `implementation`. Once the mocked method has been called `times` times, it - will automatically restore the original behavior. This value must be an - integer greater than zero. **Default:** `Infinity`. -* Returns: {Proxy} The mocked method. The mocked method contains a special - `mock` property, which is an instance of [`MockFunctionContext`][], and can - be used for inspecting and changing the behavior of the mocked method. - -This function is used to create a mock on an existing object method. The -following example demonstrates how a mock is created on an existing object -method. - -```js -test('spies on an object method', (t) => { - const number = { - value: 5, - subtract(a) { - return this.value - a; - }, - }; - t.mock.method(number, 'subtract'); - assert.strictEqual(number.subtract.mock.calls.length, 0); - assert.strictEqual(number.subtract(3), 2); - assert.strictEqual(number.subtract.mock.calls.length, 1); - const call = number.subtract.mock.calls[0]; - assert.deepStrictEqual(call.arguments, [3]); - assert.strictEqual(call.result, 2); - assert.strictEqual(call.error, undefined); - assert.strictEqual(call.target, undefined); - assert.strictEqual(call.this, number); -}); -``` - -### `mock.reset()` - - - -This function restores the default behavior of all mocks that were previously -created by this `MockTracker` and disassociates the mocks from the -`MockTracker` instance. Once disassociated, the mocks can still be used, but the -`MockTracker` instance can no longer be used to reset their behavior or -otherwise interact with them. - -After each test completes, this function is called on the test context's -`MockTracker`. If the global `MockTracker` is used extensively, calling this -function manually is recommended. - -### `mock.restoreAll()` - - - -This function restores the default behavior of all mocks that were previously -created by this `MockTracker`. Unlike `mock.reset()`, `mock.restoreAll()` does -not disassociate the mocks from the `MockTracker` instance. - -### `mock.setter(object, methodName[, implementation][, options])` - - - -This function is syntax sugar for [`MockTracker.method`][] with `options.setter` -set to `true`. - -## Class: `TestsStream` - - - -* Extends {ReadableStream} - -A successful call to [`run()`][] method will return a new {TestsStream} -object, streaming a series of events representing the execution of the tests. -`TestsStream` will emit events, in the order of the tests definition - -### Event: `'test:diagnostic'` - -* `data` {Object} - * `file` {string|undefined} The path of the test file, - undefined if test is not ran through a file. - * `message` {string} The diagnostic message. - * `nesting` {number} The nesting level of the test. - -Emitted when [`context.diagnostic`][] is called. - -### Event: `'test:fail'` - -* `data` {Object} - * `details` {Object} Additional execution metadata. - * `duration` {number} The duration of the test in milliseconds. - * `error` {Error} The error thrown by the test. - * `file` {string|undefined} The path of the test file, - undefined if test is not ran through a file. - * `name` {string} The test name. - * `nesting` {number} The nesting level of the test. - * `testNumber` {number} The ordinal number of the test. - * `todo` {string|boolean|undefined} Present if [`context.todo`][] is called - * `skip` {string|boolean|undefined} Present if [`context.skip`][] is called - -Emitted when a test fails. - -### Event: `'test:pass'` - -* `data` {Object} - * `details` {Object} Additional execution metadata. - * `duration` {number} The duration of the test in milliseconds. - * `file` {string|undefined} The path of the test file, - undefined if test is not ran through a file. - * `name` {string} The test name. - * `nesting` {number} The nesting level of the test. - * `testNumber` {number} The ordinal number of the test. - * `todo` {string|boolean|undefined} Present if [`context.todo`][] is called - * `skip` {string|boolean|undefined} Present if [`context.skip`][] is called - -Emitted when a test passes. - -### Event: `'test:plan'` - -* `data` {Object} - * `file` {string|undefined} The path of the test file, - undefined if test is not ran through a file. - * `nesting` {number} The nesting level of the test. - * `count` {number} The number of subtests that have ran. - -Emitted when all subtests have completed for a given test. - -### Event: `'test:start'` - -* `data` {Object} - * `file` {string|undefined} The path of the test file, - undefined if test is not ran through a file. - * `name` {string} The test name. - * `nesting` {number} The nesting level of the test. - -Emitted when a test starts. - -## Class: `TestContext` - -An instance of `TestContext` is passed to each test function in order to -interact with the test runner. However, the `TestContext` constructor is not -exposed as part of the API. - -### `context.beforeEach([, fn][, options])` - -* `fn` {Function|AsyncFunction} The hook function. The first argument - to this function is a [`TestContext`][] object. If the hook uses callbacks, - the callback function is passed as the second argument. **Default:** A no-op - function. -* `options` {Object} Configuration options for the hook. The following - properties are supported: - * `signal` {AbortSignal} Allows aborting an in-progress hook. - * `timeout` {number} A number of milliseconds the hook will fail after. - If unspecified, subtests inherit this value from their parent. - **Default:** `Infinity`. - -This function is used to create a hook running -before each subtest of the current test. - -```js -test('top level test', async (t) => { - t.beforeEach((t) => t.diagnostics(`about to run ${t.name}`)); - await t.test( - 'This is a subtest', - (t) => { - assert.ok('some relevant assertion here'); - } - ); -}); -``` - -### `context.after([fn][, options])` - - - -* `fn` {Function|AsyncFunction} The hook function. The first argument - to this function is a [`TestContext`][] object. If the hook uses callbacks, - the callback function is passed as the second argument. **Default:** A no-op - function. -* `options` {Object} Configuration options for the hook. The following - properties are supported: - * `signal` {AbortSignal} Allows aborting an in-progress hook. - * `timeout` {number} A number of milliseconds the hook will fail after. - If unspecified, subtests inherit this value from their parent. - **Default:** `Infinity`. - -This function is used to create a hook that runs after the current test -finishes. - -```js -test('top level test', async (t) => { - t.after((t) => t.diagnostic(`finished running ${t.name}`)); - assert.ok('some relevant assertion here'); -}); -``` - -### `context.afterEach([, fn][, options])` - -* `fn` {Function|AsyncFunction} The hook function. The first argument - to this function is a [`TestContext`][] object. If the hook uses callbacks, - the callback function is passed as the second argument. **Default:** A no-op - function. -* `options` {Object} Configuration options for the hook. The following - properties are supported: - * `signal` {AbortSignal} Allows aborting an in-progress hook. - * `timeout` {number} A number of milliseconds the hook will fail after. - If unspecified, subtests inherit this value from their parent. - **Default:** `Infinity`. - -This function is used to create a hook running -after each subtest of the current test. - -```js -test('top level test', async (t) => { - t.afterEach((t) => t.diagnostics(`finished running ${t.name}`)); - await t.test( - 'This is a subtest', - (t) => { - assert.ok('some relevant assertion here'); - } - ); -}); -``` - -### `context.diagnostic(message)` - -- `message` {string}Message to be reported. - -This function is used to write diagnostics to the output. Any diagnostic -information is included at the end of the test's results. This function does -not return a value. - - `context.name` - -The name of the test. - -### `context.runOnly(shouldRunOnlyTests)` - -- `shouldRunOnlyTests` {boolean} Whether or not to run `only` tests. - -If `shouldRunOnlyTests` is truthy, the test context will only run tests that -have the `only` option set. Otherwise, all tests are run. If Node.js was not -started with the [`--test-only`][] command-line option, this function is a -no-op. - -### `context.signal` - -* [`AbortSignal`][] Can be used to abort test subtasks when the test has been aborted. - -> **Warning** -> On Node.js v14.x, this feature won't be available unless you pass the -> `--experimental-abortcontroller` CLI flag or added an external global polyfill -> for `AbortController`. - -```js -test('top level test', async (t) => { - await fetch('some/uri', { signal: t.signal }); -}); -``` - -### `context.skip([message])` - -* `message` {string} Optional skip message. - -This function causes the test's output to indicate the test as skipped. If -`message` is provided, it is included in the output. Calling `skip()` does -not terminate execution of the test function. This function does not return a -value. - -### `context.todo([message])` - -* `message` {string} Optional `TODO` message. - -This function adds a `TODO` directive to the test's output. If `message` is -provided, it is included in the output. Calling `todo()` does not terminate -execution of the test function. This function does not return a value. - -### `context.test([name][, options][, fn])` - -- `name` {string} The name of the subtest, which is displayed when reporting - test results. **Default:** The `name` property of `fn`, or `''` if - `fn` does not have a name. -- `options` {Object} Configuration options for the subtest. The following - properties are supported: - - `concurrency` {number|boolean|null} If a number is provided, - then that many tests would run in parallel. - If `true`, it would run all subtests in parallel. - If `false`, it would only run one test at a time. - If unspecified, subtests inherit this value from their parent. - **Default:** `null`. - - `only` {boolean} If truthy, and the test context is configured to run - `only` tests, then this test will be run. Otherwise, the test is skipped. - **Default:** `false`. - - `skip` {boolean|string} If truthy, the test is skipped. If a string is - provided, that string is displayed in the test results as the reason for - skipping the test. **Default:** `false`. - - `signal` {AbortSignal} Allows aborting an in-progress test. - - `todo` {boolean|string} If truthy, the test marked as `TODO`. If a string - is provided, that string is displayed in the test results as the reason why - the test is `TODO`. **Default:** `false`. - - `timeout` {number} A number of milliseconds the test will fail after. - If unspecified, subtests inherit this value from their parent. - **Default:** `Infinity`. -- `fn` {Function|AsyncFunction} The function under test. The first argument - to this function is a [`TestContext`][] object. If the test uses callbacks, - the callback function is passed as the second argument. **Default:** A no-op - function. -- Returns: {Promise} Resolved with `undefined` once the test completes. - -This function is used to create subtests under the current test. This function -behaves in the same fashion as the top level [`test()`][] function. - -## Class: `SuiteContext` - -An instance of `SuiteContext` is passed to each suite function in order to -interact with the test runner. However, the `SuiteContext` constructor is not -exposed as part of the API. - -### `context.name` - -The name of the suite. - -### `context.signal` - -* [`AbortSignal`][] Can be used to abort test subtasks when the test has been aborted. - -> **Warning** -> On Node.js v14.x, this feature won't be available unless you pass the -> `--experimental-abortcontroller` CLI flag or added an external global polyfill -> for `AbortController`. - - -[`AbortSignal`]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal -[TAP]: https://testanything.org/ -[`MockFunctionContext`]: #class-mockfunctioncontext -[`MockTracker.method`]: #mockmethodobject-methodname-implementation-options -[`MockTracker`]: #class-mocktracke -[`SuiteContext`]: #class-suitecontext -[`TestContext`]: #class-testcontext -[`context.diagnostic`]: #contextdiagnosticmessage -[`context.skip`]: #contextskipmessage -[`context.todo`]: #contexttodomessage -[`run()`]: #runoptions -[`test()`]: #testname-options-fn -[describe options]: #describename-options-fn -[it options]: #testname-options-fn -[test runner execution model]: #test-runner-execution-model - -## License - -[MIT](./LICENSE) +### Differences from Core Implementation: +- Exposes its own stack frames. +- Lacks source map caching. +- Does not support module mocking. +- No inherent file watching capabilities. +- No built-in coverage support. +- Cannot import external scripts into `run()`-called test files. \ No newline at end of file diff --git a/bin/node--test-name-pattern.js b/bin/node--test-name-pattern.js deleted file mode 100644 index 128755a..0000000 --- a/bin/node--test-name-pattern.js +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env node - -const { argv } = require('#internal/options') - -argv['test-name-pattern'] = true - -require('./node-core-test.js') diff --git a/bin/node--test-only.js b/bin/node--test-only.js deleted file mode 100644 index 6145b14..0000000 --- a/bin/node--test-only.js +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env node - -const { argv } = require('#internal/options') - -argv['test-only'] = true - -require('./node-core-test.js') diff --git a/bin/node--test.js b/bin/node--test.js deleted file mode 100755 index cc56438..0000000 --- a/bin/node--test.js +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env node - -const { argv } = require('#internal/options') - -argv.test = true - -require('./node-core-test.js') diff --git a/bin/node-core-test.js b/bin/node-core-test.js deleted file mode 100755 index e41f837..0000000 --- a/bin/node-core-test.js +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env node -'use strict' - -const Module = require('node:module') -const path = require('node:path') -const { pathToFileURL } = require('node:url') -const minimist = require('minimist') - -const { argv } = require('#internal/options') - -const stringArgs = ['test-name-pattern', 'test-reporter', 'test-reporter-destination'] - -Object.assign(argv, minimist(process.argv.slice(2), { - boolean: ['test', 'test-only'], - string: stringArgs, - default: Object.prototype.hasOwnProperty.call(argv, 'test') ? { test: argv.test } : undefined -})) - -stringArgs.forEach((arg) => { - if (typeof argv[arg] === 'string') { - argv[arg] = [argv[arg]] - } else if (!Array.isArray(argv[arg])) { - argv[arg] = [] - } -}) - -process.argv.splice(1, Infinity, ...argv._) -if (argv.test) { - require('#internal/main/test_runner') -} else { - const entryPointPath = path.resolve(argv._[0]) - try { - loadMainModule(entryPointPath) - } catch (err) { - if (err.code !== 'ERR_REQUIRE_ESM') throw err - - // Override process exit code logic to handle TLA: - - let shouldOverwriteExitCode = true - const { exit: originalExitFunction } = process - process.exit = function exit (code) { - if (code === undefined && shouldOverwriteExitCode) { - process.exitCode = 0 - } - Reflect.apply(originalExitFunction, process, arguments) - } - Object.defineProperty(process, 'exitCode', { - get: () => 13, - set (val) { - shouldOverwriteExitCode = false - delete process.exitCode - process.exitCode = val - }, - configurable: true, - enumerable: true - }) - - // Import module - - import(pathToFileURL(entryPointPath)).then(() => { - if (shouldOverwriteExitCode) process.exitCode = 0 - }, (err) => { - console.error(err) - process.exit(1) - }) - } -} - -/** - * Loads a module as a main module, enabling the `require.main === module` pattern. - * https://github.com/nodejs/corepack/blob/5ff6e82028e58448ba5ba986854b61ecdc69885b/sources/nodeUtils.ts#L24 - */ -function loadMainModule (id) { - const modulePath = Module._resolveFilename(id, null, true) - - const module = new Module(modulePath, undefined) - - module.filename = modulePath - module.paths = Module._nodeModulePaths(path.dirname(modulePath)) - - Module._cache[modulePath] = module - - process.mainModule = module - module.id = '.' - - try { - return module.load(modulePath) - } catch (error) { - delete Module._cache[modulePath] - throw error - } -} diff --git a/example.js b/example.js deleted file mode 100644 index 950487c..0000000 --- a/example.js +++ /dev/null @@ -1,51 +0,0 @@ -// https://github.com/nodejs/node/blob/b476b1b91ef8715f096f815db5a0c8722b613678/doc/api/test.md - -'use strict' - -const assert = require('assert') -const test = require('.') - -test('synchronous passing test', t => { - // This test passes because it does not throw an exception. - assert.strictEqual(1, 1) -}) - -test('synchronous failing test', t => { - // This test fails because it throws an exception. - assert.strictEqual(1, 2) -}) - -test('asynchronous passing test', async t => { - // This test passes because the Promise returned by the async - // function is not rejected. - assert.strictEqual(1, 1) -}) - -test('asynchronous failing test', async t => { - // This test fails because the Promise returned by the async - // function is rejected. - assert.strictEqual(1, 2) -}) - -test('failing test using Promises', t => { - // Promises can be used directly as well. - return new Promise((resolve, reject) => { - setImmediate(() => { - reject(new Error('this will cause the test to fail')) - }) - }) -}) - -test('callback passing test', (t, done) => { - // done() is the callback function. When the setImmediate() runs, it invokes - // done() with no arguments. - setImmediate(done) -}) - -test('callback failing test', (t, done) => { - // When the setImmediate() runs, done() is invoked with an Error object and - // the test fails. - setImmediate(() => { - done(new Error('callback failure')) - }) -}) diff --git a/lib/bootstrap.js b/lib/bootstrap.js new file mode 100644 index 0000000..15957ad --- /dev/null +++ b/lib/bootstrap.js @@ -0,0 +1,105 @@ +const util = require('node:util'); +const { GetPrimordial } = require('frozen-fruit'); +const toBePrimordialized = new Set(Object.getOwnPropertyNames(globalThis)); +const arrayLikePromiseFunc = (array, mapFn) => Promise.all(mapFn ? array.map(mapFn) : array); + +const primordials = { + Symbol, + Number, + Promise, + Int32Array, + Error, + Proxy, + Array, + RegExp, + EvalError, + RangeError, + ReferenceError, + TypeError, + SyntaxError, + URIError, + String, + globalThis, + SafeMap: Map, + SafeSet: Set, + PromiseWithResolvers: function () { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; + }, + ArrayPrototypePushApply: (self, value) => self.push.apply(self, value), + ArrayPrototypeUnshiftApply: (self, value) => self.unshift.apply(self, value), + hardenRegExp: (pattern) => pattern, + + SafePromiseAll: arrayLikePromiseFunc, + SafePromiseAllReturnVoid: arrayLikePromiseFunc, + SafePromiseAllReturnArrayLike: arrayLikePromiseFunc, + SafePromisePrototypeFinally: (promise, onFinally) => promise.finally(onFinally), + SafePromiseAllSettledReturnVoid: (array, mapFn) => Promise.allSettled(mapFn ? array.map(mapFn) : array), + SafePromiseRace: (array, mapFn) => Promise.race(mapFn ? array.map(mapFn) : array) +}; + +// Collect primordials from specified global properties +for (const name of toBePrimordialized) { + if (name[0].toUpperCase() === name[0]) { // Check for capitalized names + GetPrimordial(globalThis[name], name, primordials); + } +} + +GetPrimordial(Reflect.getPrototypeOf(Uint8Array), 'TypedArray', primordials) + +primordials.PromiseResolve = Promise.resolve.bind(Promise); + +const proxiedPrimordials = new Proxy(primordials, { + get(target, prop) { + // Check if the property exists in the target object + if (prop in target) { + return target[prop]; // Return the property value if found + } else { + throw new Error(`Key "${prop}" not found in primordials.`); // Throw an error if not found + } + } +}); + +module.exports = { + primordials: proxiedPrimordials, + internalBinding: function (name) { + switch (name) { + case 'util': return { + getCallerLocation: () => { + const originalPrepareStackTrace = Error.prepareStackTrace; + + // Override Error.prepareStackTrace to customize stack trace formatting + Error.prepareStackTrace = (_, stack) => stack; + + // Create an error to generate a stack trace + const stack = new Error().stack; + + // Restore original Error.prepareStackTrace + Error.prepareStackTrace = originalPrepareStackTrace; + + // Return the second element in the stack trace to get the caller's location + if (!stack[2]) return [0, 0, '']; + return [ + stack[2].getLineNumber(), + stack[2].getColumnNumber(), + stack[2].getFileName() + ]; + } + } + case 'errors': return { + exitCodes: { + kGenericUserError: 1 + } + } + case 'types': return { + isDate: util.types.isDate + } + default: throw name + } + } +}; \ No newline at end of file diff --git a/lib/cli.js b/lib/cli.js new file mode 100755 index 0000000..2695e60 --- /dev/null +++ b/lib/cli.js @@ -0,0 +1,25 @@ +#!/usr/bin/env node + +'use strict'; + +const { isUsingInspector } = require('#internal/util/inspector'); +const { run } = require('#internal/test_runner/runner'); +const { parseCommandLine } = require('#internal/test_runner/utils'); +const { notParsedAsOptions } = require('#internal/options') + +const options = parseCommandLine(); + +if (isUsingInspector() && options.isolation === 'process') { + process.emitWarning('Using the inspector with --test forces running at a concurrency of 1. ' + + 'Use the inspectPort option to run with concurrency'); + options.concurrency = 1; + options.inspectPort = process.debugPort; +} + +options.globPatterns = notParsedAsOptions; + +run(options).on('test:summary', (data) => { + if (!data.success) { + process.exitCode = 1; + } +}); \ No newline at end of file diff --git a/lib/internal/abort_controller.js b/lib/internal/abort_controller.js index 36ce226..f4da4aa 100644 --- a/lib/internal/abort_controller.js +++ b/lib/internal/abort_controller.js @@ -1,30 +1,3 @@ -'use strict' - -if (typeof AbortController === 'undefined') { - // Node.js AbortController implementation is behind a CLI flag. - // This is a minimal mock to make the code work if the native - // implementation in not available. - module.exports = { - AbortController: class AbortController { - #eventListeners = new Set() - signal = { - aborted: false, - addEventListener: (_, listener) => { - this.#eventListeners.add(listener) - }, - removeEventListener: (_, listener) => { - this.#eventListeners.delete(listener) - } - } - - abort () { - this.signal.aborted = true - this.#eventListeners.forEach(listener => listener()) - } - } - } -} else { - module.exports = { +module.exports = { AbortController - } -} +} \ No newline at end of file diff --git a/lib/internal/assert.js b/lib/internal/assert.js deleted file mode 100644 index 860d651..0000000 --- a/lib/internal/assert.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict' - -module.exports = require('assert') diff --git a/lib/internal/assert/utils.js b/lib/internal/assert/utils.js new file mode 100644 index 0000000..2ae9ed0 --- /dev/null +++ b/lib/internal/assert/utils.js @@ -0,0 +1,9 @@ +const assert = require('node:assert'); + +function innerOk(_caller, _length, ...args) { + return assert.ok(...args); +} + +module.exports = { + innerOk +} \ No newline at end of file diff --git a/lib/internal/console/global.js b/lib/internal/console/global.js index a664f5c..9b3fe10 100644 --- a/lib/internal/console/global.js +++ b/lib/internal/console/global.js @@ -1,3 +1 @@ -'use strict' - -module.exports = console +module.exports = console; \ No newline at end of file diff --git a/lib/internal/deps/minimatch/index.js b/lib/internal/deps/minimatch/index.js new file mode 100644 index 0000000..e7e64b6 --- /dev/null +++ b/lib/internal/deps/minimatch/index.js @@ -0,0 +1 @@ +module.exports = require('minimatch'); \ No newline at end of file diff --git a/lib/internal/error_serdes.js b/lib/internal/error_serdes.js new file mode 100644 index 0000000..ad7c084 --- /dev/null +++ b/lib/internal/error_serdes.js @@ -0,0 +1,192 @@ +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; + +const { + ArrayPrototypeForEach, + Error, + EvalError, + FunctionPrototypeCall, + ObjectAssign, + ObjectCreate, + ObjectDefineProperty, + ObjectGetOwnPropertyDescriptor, + ObjectGetOwnPropertyNames, + ObjectGetPrototypeOf, + ObjectKeys, + ObjectPrototypeToString, + RangeError, + ReferenceError, + SafeSet, + StringFromCharCode, + StringPrototypeSubstring, + SymbolFor, + SymbolToStringTag, + SyntaxError, + TypeError, + TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetByteOffset, + URIError, +} = primordials; + +const { Buffer } = require('buffer'); +const { inspect: { custom: customInspectSymbol } } = require('util'); + +const kSerializedError = 0; +const kSerializedObject = 1; +const kInspectedError = 2; +const kInspectedSymbol = 3; +const kCustomInspectedObject = 4; + +const kSymbolStringLength = 'Symbol('.length; + +const errors = { + Error, TypeError, RangeError, URIError, SyntaxError, ReferenceError, EvalError, +}; +const errorConstructorNames = new SafeSet(ObjectKeys(errors)); + +function TryGetAllProperties(object, target = object) { + const all = { __proto__: null }; + if (object === null) + return all; + ObjectAssign(all, + TryGetAllProperties(ObjectGetPrototypeOf(object), target)); + const keys = ObjectGetOwnPropertyNames(object); + ArrayPrototypeForEach(keys, (key) => { + let descriptor; + try { + // TODO: create a null-prototype descriptor with needed properties only + descriptor = ObjectGetOwnPropertyDescriptor(object, key); + } catch { return; } + const getter = descriptor.get; + if (getter && key !== '__proto__') { + try { + descriptor.value = FunctionPrototypeCall(getter, target); + delete descriptor.get; + delete descriptor.set; + } catch { + // Continue regardless of error. + } + } + if (key === 'cause') { + descriptor.value = serializeError(descriptor.value); + all[key] = descriptor; + } else if ('value' in descriptor && + typeof descriptor.value !== 'function' && typeof descriptor.value !== 'symbol') { + all[key] = descriptor; + } + }); + return all; +} + +function GetConstructors(object) { + const constructors = []; + + for (let current = object; + current !== null; + current = ObjectGetPrototypeOf(current)) { + const desc = ObjectGetOwnPropertyDescriptor(current, 'constructor'); + if (desc?.value) { + ObjectDefineProperty(constructors, constructors.length, { + __proto__: null, + value: desc.value, enumerable: true, + }); + } + } + + return constructors; +} + +function GetName(object) { + const desc = ObjectGetOwnPropertyDescriptor(object, 'name'); + return desc?.value; +} + +let internalUtilInspect; +function inspect(...args) { + internalUtilInspect ??= require('#internal/util/inspect'); + return internalUtilInspect.inspect(...args); +} + +let serialize; +function serializeError(error) { + serialize ??= require('v8').serialize; + if (typeof error === 'symbol') { + return Buffer.from(StringFromCharCode(kInspectedSymbol) + inspect(error), 'utf8'); + } + try { + if (typeof error === 'object' && + ObjectPrototypeToString(error) === '[object Error]') { + const constructors = GetConstructors(error); + for (let i = 0; i < constructors.length; i++) { + const name = GetName(constructors[i]); + if (errorConstructorNames.has(name)) { + const serialized = serialize({ + constructor: name, + properties: TryGetAllProperties(error), + }); + return Buffer.concat([Buffer.from([kSerializedError]), serialized]); + } + } + } + } catch { + // Continue regardless of error. + } + try { + if (error != null && customInspectSymbol in error) { + return Buffer.from(StringFromCharCode(kCustomInspectedObject) + inspect(error), 'utf8'); + } + } catch { + // Continue regardless of error. + } + try { + const serialized = serialize(error); + return Buffer.concat([Buffer.from([kSerializedObject]), serialized]); + } catch { + // Continue regardless of error. + } + return Buffer.from(StringFromCharCode(kInspectedError) + inspect(error), 'utf8'); +} + +function fromBuffer(error) { + return Buffer.from(TypedArrayPrototypeGetBuffer(error), + TypedArrayPrototypeGetByteOffset(error) + 1, + TypedArrayPrototypeGetByteLength(error) - 1); +} + +let deserialize; +function deserializeError(error) { + deserialize ??= require('v8').deserialize; + switch (error[0]) { + case kSerializedError: { + const { constructor, properties } = deserialize(error.subarray(1)); + const ctor = errors[constructor]; + ObjectDefineProperty(properties, SymbolToStringTag, { + __proto__: null, + value: { __proto__: null, value: 'Error', configurable: true }, + enumerable: true, + }); + if ('cause' in properties && 'value' in properties.cause) { + properties.cause.value = deserializeError(properties.cause.value); + } + return ObjectCreate(ctor.prototype, properties); + } + case kSerializedObject: + return deserialize(error.subarray(1)); + case kInspectedError: + return fromBuffer(error).toString('utf8'); + case kInspectedSymbol: { + const buf = fromBuffer(error); + return SymbolFor(StringPrototypeSubstring(buf.toString('utf8'), kSymbolStringLength, buf.length - 1)); + } + case kCustomInspectedObject: + return { + __proto__: null, + [customInspectSymbol]: () => fromBuffer(error).toString('utf8'), + }; + } + require('assert').fail('This should not happen'); +} + +module.exports = { serializeError, deserializeError }; \ No newline at end of file diff --git a/lib/internal/errors.js b/lib/internal/errors.js index ab98407..ae92076 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1,397 +1,366 @@ -// https://github.com/nodejs/node/blob/f8ce9117b19702487eb600493d941f7876e00e01/lib/internal/errors.js - -'use strict' - -const { - ArrayPrototypeFilter, - ArrayPrototypeJoin, - ArrayPrototypeUnshift, - Error, - ErrorCaptureStackTrace, - ObjectDefineProperty, - ObjectDefineProperties, - ObjectIsExtensible, - ObjectGetOwnPropertyDescriptor, - ObjectPrototypeHasOwnProperty, - ReflectApply, - SafeMap, - SafeWeakMap, - StringPrototypeIncludes, - StringPrototypeMatch, - StringPrototypeStartsWith, - StringPrototypeSlice, - Symbol, - SymbolFor -} = require('#internal/per_context/primordials') - -const kIsNodeError = Symbol('kIsNodeError') - -const messages = new SafeMap() -const codes = {} - -const overrideStackTrace = new SafeWeakMap() -let userStackTraceLimit -const nodeInternalPrefix = '__node_internal_' - -// Lazily loaded -let assert - -let internalUtilInspect = null -function lazyInternalUtilInspect () { - if (!internalUtilInspect) { - internalUtilInspect = require('#internal/util/inspect') - } - return internalUtilInspect -} - -let buffer -function lazyBuffer () { - if (buffer === undefined) { buffer = require('buffer').Buffer } - return buffer -} +const util = require('node:util'); +const v8 = require('node:v8'); -function isErrorStackTraceLimitWritable () { - const desc = ObjectGetOwnPropertyDescriptor(Error, 'stackTraceLimit') - if (desc === undefined) { - return ObjectIsExtensible(Error) - } +const kIsNodeError = Symbol('kIsNodeError'); +const messages = new Map(); +const codes = {}; - return ObjectPrototypeHasOwnProperty(desc, 'writable') - ? desc.writable - : desc.set !== undefined +function isErrorStackTraceLimitWritable() { + return !v8.startupSnapshot.isBuildingSnapshot() && + (Object.getOwnPropertyDescriptor(Error, 'stackTraceLimit')?.writable ?? + Object.isExtensible(Error)); } -function inspectWithNoCustomRetry (obj, options) { - const utilInspect = lazyInternalUtilInspect() - +function inspectWithNoCustomRetry(obj, options) { try { - return utilInspect.inspect(obj, options) + return util.inspect(obj, options); } catch { - return utilInspect.inspect(obj, { ...options, customInspect: false }) + return util.inspect(obj, { ...options, customInspect: false }); } } -// A specialized Error that includes an additional info property with -// additional information about the error condition. -// It has the properties present in a UVException but with a custom error -// message followed by the uv error code and uv error message. -// It also has its own error code with the original uv error context put into -// `err.info`. -// The context passed into this error must have .code, .syscall and .message, -// and may have .path and .dest. class SystemError extends Error { - constructor (key, context) { - const limit = Error.stackTraceLimit - if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = 0 - super() - // Reset the limit and setting the name property. - if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = limit - const prefix = getMessage(key, [], this) - let message = `${prefix}: ${context.syscall} returned ` + - `${context.code} (${context.message})` - - if (context.path !== undefined) { message += ` ${context.path}` } - if (context.dest !== undefined) { message += ` => ${context.dest}` } - - captureLargerStackTrace(this) - - this.code = key - - ObjectDefineProperties(this, { - [kIsNodeError]: { - value: true, - enumerable: false, - writable: false, - configurable: true - }, - name: { - value: 'SystemError', - enumerable: false, - writable: true, - configurable: true - }, - message: { - value: message, - enumerable: false, - writable: true, - configurable: true - }, - info: { - value: context, - enumerable: true, - configurable: true, - writable: false - }, - errno: { - get () { - return context.errno - }, - set: (value) => { - context.errno = value - }, - enumerable: true, - configurable: true - }, - syscall: { - get () { - return context.syscall - }, - set: (value) => { - context.syscall = value - }, - enumerable: true, - configurable: true + constructor(key, context) { + super(); + if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = 0; + + const prefix = getMessage(key, [], this); + this.message = `${prefix}: ${context.syscall} returned ${context.code} (${context.message})` + + (context.path ? ` ${context.path}` : '') + + (context.dest ? ` => ${context.dest}` : ''); + + captureLargerStackTrace(this); + + this.code = key; + this.name = 'SystemError'; + this.info = context; + + this[kIsNodeError] = true; + + ['errno', 'syscall', 'path', 'dest'].forEach(prop => { + if (context[prop]) { + Object.defineProperty(this, prop, { + get: () => context[prop]?.toString(), + set: value => context[prop] = value ? Buffer.from(value.toString()) : undefined, + enumerable: true, + configurable: true + }); } - }) - - if (context.path !== undefined) { - // TODO(BridgeAR): Investigate why and when the `.toString()` was - // introduced. The `path` and `dest` properties in the context seem to - // always be of type string. We should probably just remove the - // `.toString()` and `Buffer.from()` operations and set the value on the - // context as the user did. - ObjectDefineProperty(this, 'path', { - get () { - return context.path != null - ? context.path.toString() - : context.path - }, - set: (value) => { - context.path = value - ? lazyBuffer().from(value.toString()) - : undefined - }, - enumerable: true, - configurable: true - }) - } - - if (context.dest !== undefined) { - ObjectDefineProperty(this, 'dest', { - get () { - return context.dest != null - ? context.dest.toString() - : context.dest - }, - set: (value) => { - context.dest = value - ? lazyBuffer().from(value.toString()) - : undefined - }, - enumerable: true, - configurable: true - }) - } + }); } - toString () { - return `${this.name} [${this.code}]: ${this.message}` + toString() { + return `${this.name} [${this.code}]: ${this.message}`; } - [SymbolFor('nodejs.util.inspect.custom')] (recurseTimes, ctx) { - return lazyInternalUtilInspect().inspect(this, { - ...ctx, - getters: true, - customInspect: false - }) + [Symbol.for('nodejs.util.inspect.custom')](recurseTimes, ctx) { + return util.inspect(this, { ...ctx, getters: true, customInspect: false }); } } -function makeSystemErrorWithCode (key) { - return class NodeError extends SystemError { - constructor (ctx) { - super(key, ctx) +function makeSystemErrorWithCode(key) { + return class extends SystemError { + constructor(context) { + super(key, context); } - } + }; } -function makeNodeErrorWithCode (Base, key) { - return function NodeError (...args) { - const limit = Error.stackTraceLimit - if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = 0 - const error = new Base() - // Reset the limit and setting the name property. - if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = limit - const message = getMessage(key, args, error) - ObjectDefineProperties(error, { - [kIsNodeError]: { - value: true, - enumerable: false, - writable: false, - configurable: true - }, - message: { - value: message, - enumerable: false, - writable: true, - configurable: true - }, - toString: { - value () { - return `${this.name} [${key}]: ${this.message}` - }, - enumerable: false, - writable: true, - configurable: true - } - }) - captureLargerStackTrace(error) - error.code = key - return error - } +function makeNodeErrorWithCode(Base, key) { + return function NodeError(...args) { + const error = new Base(); + error.message = getMessage(key, args, error); + error.code = key; + + Object.defineProperty(error, kIsNodeError, { + value: true, + enumerable: false, + writable: false, + configurable: true + }); + + error.toString = function () { + return `${this.name} [${key}]: ${this.message}`; + }; + + captureLargerStackTrace(error); + return error; + }; } -/** - * This function removes unnecessary frames from Node.js core errors. - * @template {(...args: any[]) => any} T - * @type {(fn: T) => T} - */ -function hideStackFrames (fn) { - // We rename the functions that will be hidden to cut off the stacktrace - // at the outermost one - const hidden = nodeInternalPrefix + fn.name - ObjectDefineProperty(fn, 'name', { value: hidden }) - return fn + +function registerError(sym, val, def, ...otherClasses) { + messages.set(sym, val); + const errorClass = def === SystemError ? makeSystemErrorWithCode(sym) : makeNodeErrorWithCode(def, sym); + otherClasses.forEach(clazz => errorClass[clazz.name] = makeNodeErrorWithCode(clazz, sym)); + codes[sym] = errorClass; } -// Utility function for registering the error codes. Only used here. Exported -// *only* to allow for testing. -function E (sym, val, def, ...otherClasses) { - // Special case for SystemError that formats the error message differently - // The SystemErrors only have SystemError as their base classes. - messages.set(sym, val) - if (def === SystemError) { - def = makeSystemErrorWithCode(sym) - } else { - def = makeNodeErrorWithCode(def, sym) - } +function getMessage(key, args, err) { + const msg = messages.get(key); + return typeof msg === 'function' ? msg.apply(err, [args, err]) : util.format(msg, ...args); +} - if (otherClasses.length !== 0) { - otherClasses.forEach((clazz) => { - def[clazz.name] = makeNodeErrorWithCode(clazz, sym) - }) +const captureLargerStackTrace = function (err) { + if (isErrorStackTraceLimitWritable()) { + Error.stackTraceLimit = Infinity; + Error.captureStackTrace(err); + Error.stackTraceLimit = Error.stackTraceLimit; } - codes[sym] = def -} +}; -function getMessage (key, args, self) { - const msg = messages.get(key) +function determineSpecificType(value) { + if (value === null) { + return 'null'; + } else if (value === undefined) { + return 'undefined'; + } - if (assert === undefined) assert = require('#internal/assert') + const type = typeof value; + + switch (type) { + case 'bigint': + return `type bigint (${value}n)`; + case 'number': + if (value === 0) { + return 1 / value === -Infinity ? 'type number (-0)' : 'type number (0)'; + } else if (value !== value) { // eslint-disable-line no-self-compare + return 'type number (NaN)'; + } else if (value === Infinity) { + return 'type number (Infinity)'; + } else if (value === -Infinity) { + return 'type number (-Infinity)'; + } + return `type number (${value})`; + case 'boolean': + return value ? 'type boolean (true)' : 'type boolean (false)'; + case 'symbol': + return `type symbol (${String(value)})`; + case 'function': + return `function ${value.name}`; + case 'object': + if (value.constructor && 'name' in value.constructor) { + return `an instance of ${value.constructor.name}`; + } + return `${util.inspect(value, { depth: -1 })}`; + case 'string': + value.length > 28 && (value = `${value.slice(0, 25)}...`); + if (value.indexOf("'") === -1) { + return `type string ('${value}')`; + } + return `type string (${JSON.stringify(value)})`; + default: + value = util.inspect(value, { colors: false }); + if (value.length > 28) { + value = `${value.slice(0, 25)}...`; + } - if (typeof msg === 'function') { - assert( - msg.length <= args.length, // Default options do not count. - `Code: ${key}; The provided arguments length (${args.length}) does not ` + - `match the required ones (${msg.length}).` - ) - return ReflectApply(msg, self, args) + return `type ${type} (${value})`; } - - const expectedLength = - (StringPrototypeMatch(msg, /%[dfijoOs]/g) || []).length - assert( - expectedLength === args.length, - `Code: ${key}; The provided arguments length (${args.length}) does not ` + - `match the required ones (${expectedLength}).` - ) - if (args.length === 0) { return msg } - - ArrayPrototypeUnshift(args, msg) - return ReflectApply(lazyInternalUtilInspect().format, null, args) } -const captureLargerStackTrace = hideStackFrames( - function captureLargerStackTrace (err) { - const stackTraceLimitIsWritable = isErrorStackTraceLimitWritable() - if (stackTraceLimitIsWritable) { - userStackTraceLimit = Error.stackTraceLimit - Error.stackTraceLimit = Infinity - } - ErrorCaptureStackTrace(err) - // Reset the limit - if (stackTraceLimitIsWritable) Error.stackTraceLimit = userStackTraceLimit - - return err - }) - -// Hide stack lines before the first user code line. -function hideInternalStackFrames (error) { - overrideStackTrace.set(error, (error, stackFrames) => { - let frames = stackFrames - if (typeof stackFrames === 'object') { - frames = ArrayPrototypeFilter( - stackFrames, - (frm) => !StringPrototypeStartsWith(frm.getFileName() || '', - 'node:internal') - ) +class AbortError extends Error { + constructor(message = 'The operation was aborted', options = undefined) { + if (options !== undefined && typeof options !== 'object') { + throw new codes.ERR_INVALID_ARG_TYPE('options', 'Object', options); } - ArrayPrototypeUnshift(frames, error) - return ArrayPrototypeJoin(frames, '\n at ') - }) + super(message, options); + this.code = 'ABORT_ERR'; + this.name = 'AbortError'; + } } -class AbortError extends Error { - constructor (message = 'The operation was aborted', options = undefined) { - super(message, options) - this.code = 'ABORT_ERR' - this.name = 'AbortError' +module.exports = { inspectWithNoCustomRetry, kIsNodeError, codes, AbortError }; + +// Register errors +registerError('ERR_TEST_FAILURE', ([error, failureType], self) => { + let msg = error?.message ?? error; + + if (typeof msg !== 'string') { + msg = inspectWithNoCustomRetry(msg); + } + + self.failureType = failureType; + self.cause = error; + + return msg; +}, Error); + +registerError('ERR_SOURCE_MAP_CORRUPT', "The source map for '%s' does not exist or is corrupt.", Error); +registerError('ERR_SOURCE_MAP_MISSING_SOURCE', "Cannot find '%s' imported from the source map for '%s'", Error); + +// Sorted by a rough estimate on most frequently used entries. +const kTypes = [ + 'string', + 'function', + 'number', + 'object', + 'Function', + 'Object', + 'boolean', + 'bigint', + 'symbol', +]; + +function determineSpecificType(value) { + if (value === null) return 'null'; + if (value === undefined) return 'undefined'; + + const type = typeof value; + + switch (type) { + case 'bigint': + return `type bigint (${value}n)`; + case 'number': + if (Object.is(value, -0)) return 'type number (-0)'; + if (Number.isNaN(value)) return 'type number (NaN)'; + if (!isFinite(value)) return `type number (${value})`; + return `type number (${value})`; + case 'boolean': + return `type boolean (${value})`; + case 'symbol': + return `type symbol (${value.toString()})`; + case 'function': + return `function ${value.name || '(anonymous)'}`; + case 'string': + const displayString = value.length > 28 ? `${value.slice(0, 25)}...` : value; + return `type string ('${displayString}')`; + case 'object': + if (value.constructor && value.constructor.name) { + return `an instance of ${value.constructor.name}`; + } + return `an object: ${util.inspect(value, { depth: -1, colors: false })}`; + default: + let displayOther = util.inspect(value, { depth: -1, colors: false }); + if (displayOther.length > 28) { + displayOther = `${displayOther.slice(0, 25)}...`; + } + return `type ${type} (${displayOther})`; } } -module.exports = { - AbortError, - codes, - inspectWithNoCustomRetry, - kIsNodeError +function formatList(array, type = 'and') { + if (array.length === 0) return ''; + if (array.length === 1) return array[0]; + if (array.length === 2) return `${array[0]} ${type} ${array[1]}`; + + const lastItem = `${type} ${array[array.length - 1]}`; + return `${array.slice(0, -1).join(', ')}, ${lastItem}`; } +const classRegExp = /^[A-Z][a-zA-Z0-9]*$/; -E('ERR_TAP_LEXER_ERROR', function (errorMsg) { - hideInternalStackFrames(this) - return errorMsg -}, Error) -E('ERR_TAP_PARSER_ERROR', function (errorMsg, details, tokenCausedError, source) { - hideInternalStackFrames(this) - this.cause = tokenCausedError - const { column, line, start, end } = tokenCausedError.location - const errorDetails = `${details} at line ${line}, column ${column} (start ${start}, end ${end})` - return errorMsg + errorDetails -}, SyntaxError) -E('ERR_TAP_VALIDATION_ERROR', function (errorMsg) { - hideInternalStackFrames(this) - return errorMsg -}, Error) -E('ERR_TEST_FAILURE', function (error, failureType) { - hideInternalStackFrames(this) - assert(typeof failureType === 'string', - "The 'failureType' argument must be of type string.") - - let msg = error?.message ?? error +registerError('ERR_INVALID_ARG_TYPE', ([name, expected, actual]) => { + if (!Array.isArray(expected)) { + expected = [expected]; + } - if (typeof msg !== 'string') { - msg = inspectWithNoCustomRetry(msg) + let msg = 'The '; + if (name.endsWith(' argument')) { + // For cases like 'first argument' + msg += `${name} `; + } else { + const type = name.includes('.') ? 'property' : 'argument'; + msg += `"${name}" ${type} `; + } + msg += 'must be '; + + const types = []; + const instances = []; + const other = []; + + for (const value of expected) { + if (kTypes.includes(value)) { + types.push(value.toLowerCase()) + } else if (classRegExp.exec(value) !== null) { + instances.push(value); + } else { + other.push(value); + } } - this.failureType = failureType - this.cause = error - return msg -}, Error) -E('ERR_INVALID_ARG_TYPE', - (name, expected, actual) => `Expected ${name} to be ${expected}, got type ${typeof actual}`, - TypeError) -E('ERR_INVALID_ARG_VALUE', (name, value, reason = 'is invalid') => { - let inspected - try { - inspected = String(value) - } catch { - inspected = `type ${typeof value}` + // Special handle `object` in case other instances are allowed to outline + // the differences between each other. + if (instances.length > 0) { + const pos = types.indexOf('object'); + if (pos !== -1) { + types.splice(pos, 1); + instances.push('Object'); + } + } + + if (types.length > 0) { + msg += `${types.length > 1 ? 'one of type' : 'of type'} ${formatList(types, 'or')}`; + if (instances.length > 0 || other.length > 0) + msg += ' or '; } + + if (instances.length > 0) { + msg += `an instance of ${formatList(instances, 'or')}`; + if (other.length > 0) + msg += ' or '; + } + + if (other.length > 0) { + if (other.length > 1) { + msg += `one of ${formatList(other, 'or')}`; + } else { + if (other[0].toLowerCase() !== other[0]) + msg += 'an '; + msg += `${other[0]}`; + } + } + + msg += `. Received ${determineSpecificType(actual)}`; + + return msg; +}, TypeError); + +registerError('ERR_INVALID_ARG_VALUE', ([name, value, reason = 'is invalid']) => { + let inspected = util.inspect(value); if (inspected.length > 128) { - inspected = `${StringPrototypeSlice(inspected, 0, 128)}...` + inspected = `${inspected.slice(0, 128)}...`; } - const type = StringPrototypeIncludes(name, '.') ? 'property' : 'argument' - return `The ${type} '${name}' ${reason}. Received ${inspected}` -}, TypeError, RangeError) -E('ERR_OUT_OF_RANGE', - (name, expected, actual) => `Expected ${name} to be ${expected}, got ${actual}`, - RangeError) + const type = name.includes('.') ? 'property' : 'argument'; + return `The ${type} '${name}' ${reason}. Received ${inspected}`; +}, TypeError, RangeError); +registerError('ERR_INVALID_STATE', 'Invalid state: %s', Error, TypeError, RangeError); +registerError('ERR_SOCKET_BAD_PORT', ([name, port, allowZero = true]) => { + const operator = allowZero ? '>=' : '>'; + return `${name} should be ${operator} 0 and < 65536. Received ${determineSpecificType(port)}.`; +}, RangeError); + +function addNumericalSeparator(val) { + let res = ''; + let i = val.length; + const start = val[0] === '-' ? 1 : 0; + for (; i >= start + 4; i -= 3) { + res = `_${val.slice(i - 3, i)}${res}`; + } + return `${val.slice(0, i)}${res}`; +} + +registerError('ERR_OUT_OF_RANGE', + ([str, range, input, replaceDefaultBoolean = false]) => { + // Set the default message based on the replaceDefaultBoolean flag + const defaultMessage = `The value of "${str}" is out of range.`; + let msg = replaceDefaultBoolean ? str : defaultMessage; + + // Determine the received value representation + let received; + if (Number.isInteger(input) && Math.abs(input) > 2 ** 32) { + received = addNumericalSeparator(String(input)); + } else if (typeof input === 'bigint') { + received = String(input); + if (Math.abs(input) > 2n ** 32n) { + received = addNumericalSeparator(received); + } + received += 'n'; // Indicate it's a BigInt + } else { + received = util.inspect(input); + } + + // Append the range and received information to the message + return `${msg} It must be ${range}. Received ${received}`; + }, RangeError); + +registerError('UNSUPPORTED_FEATURE', '%s is not supported by this polyfill. Please upgrade to the latest version of Node.js to make use of this feature', Error); \ No newline at end of file diff --git a/lib/internal/event_target.js b/lib/internal/event_target.js new file mode 100644 index 0000000..0c1ba39 --- /dev/null +++ b/lib/internal/event_target.js @@ -0,0 +1,5 @@ +const kResistStopPropagation = Symbol('kResistStopPropagation'); + +module.exports = { + kResistStopPropagation +} \ No newline at end of file diff --git a/lib/internal/events/abort_listener.js b/lib/internal/events/abort_listener.js new file mode 100644 index 0000000..a93bf47 --- /dev/null +++ b/lib/internal/events/abort_listener.js @@ -0,0 +1,5 @@ +const { EventEmitter } = require('node:events'); + +module.exports = { + addAbortListener: EventEmitter.addAbortListener +} \ No newline at end of file diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js new file mode 100644 index 0000000..12e3367 --- /dev/null +++ b/lib/internal/fs/glob.js @@ -0,0 +1,658 @@ +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; + +const { + ArrayFrom, + ArrayPrototypeAt, + ArrayPrototypeFlatMap, + ArrayPrototypeMap, + ArrayPrototypePop, + ArrayPrototypePush, + ArrayPrototypeSome, + PromisePrototypeThen, + SafeMap, + SafeSet, + StringPrototypeEndsWith, +} = primordials; + +const { lstatSync, readdirSync } = require('fs'); +const { lstat, readdir } = require('fs/promises'); +const { join, resolve, basename, isAbsolute, dirname } = require('path'); + +const { + kEmptyObject, + isWindows, + isMacOS, +} = require('#internal/util'); +const { + validateFunction, + validateObject, + validateString, + validateStringArray, +} = require('#internal/validators'); +const { DirentFromStats } = require('#internal/fs/utils'); + +let minimatch; +function lazyMinimatch() { + minimatch ??= require('#internal/deps/minimatch/index'); + return minimatch; +} + +/** + * @param {string} path + * @returns {Promise} + */ +async function getDirent(path) { + let stat; + try { + stat = await lstat(path); + } catch { + return null; + } + return new DirentFromStats(basename(path), stat, dirname(path)); +} + +/** + * @param {string} path + * @returns {DirentFromStats|null} + */ +function getDirentSync(path) { + const stat = lstatSync(path, { throwIfNoEntry: false }); + if (stat === undefined) { + return null; + } + return new DirentFromStats(basename(path), stat, dirname(path)); +} + +class Cache { + #cache = new SafeMap(); + #statsCache = new SafeMap(); + #readdirCache = new SafeMap(); + + stat(path) { + const cached = this.#statsCache.get(path); + if (cached) { + return cached; + } + const promise = getDirent(path); + this.#statsCache.set(path, promise); + return promise; + } + statSync(path) { + const cached = this.#statsCache.get(path); + if (cached) { + return cached; + } + const val = getDirentSync(path); + this.#statsCache.set(path, val); + return val; + } + addToStatCache(path, val) { + this.#statsCache.set(path, val); + } + async readdir(path) { + const cached = this.#readdirCache.get(path); + if (cached) { + return cached; + } + const promise = PromisePrototypeThen(readdir(path, { __proto__: null, withFileTypes: true }), null, () => null); + this.#readdirCache.set(path, promise); + return promise; + } + readdirSync(path) { + const cached = this.#readdirCache.get(path); + if (cached) { + return cached; + } + let val; + try { + val = readdirSync(path, { __proto__: null, withFileTypes: true }); + } catch { + val = []; + } + this.#readdirCache.set(path, val); + return val; + } + add(path, pattern) { + let cache = this.#cache.get(path); + if (!cache) { + cache = new SafeSet(); + this.#cache.set(path, cache); + } + const originalSize = cache.size; + pattern.indexes.forEach((index) => cache.add(pattern.cacheKey(index))); + return cache.size !== originalSize + pattern.indexes.size; + } + seen(path, pattern, index) { + return this.#cache.get(path)?.has(pattern.cacheKey(index)); + } +} + +class Pattern { + #pattern; + #globStrings; + indexes; + symlinks; + last; + + constructor(pattern, globStrings, indexes, symlinks) { + this.#pattern = pattern; + this.#globStrings = globStrings; + this.indexes = indexes; + this.symlinks = symlinks; + this.last = pattern.length - 1; + } + + isLast(isDirectory) { + return this.indexes.has(this.last) || + (this.at(-1) === '' && isDirectory && + this.indexes.has(this.last - 1) && this.at(-2) === lazyMinimatch().GLOBSTAR); + } + isFirst() { + return this.indexes.has(0); + } + get hasSeenSymlinks() { + return ArrayPrototypeSome(ArrayFrom(this.indexes), (i) => !this.symlinks.has(i)); + } + at(index) { + return ArrayPrototypeAt(this.#pattern, index); + } + child(indexes, symlinks = new SafeSet()) { + return new Pattern(this.#pattern, this.#globStrings, indexes, symlinks); + } + test(index, path) { + if (index > this.#pattern.length) { + return false; + } + const pattern = this.#pattern[index]; + if (pattern === lazyMinimatch().GLOBSTAR) { + return true; + } + if (typeof pattern === 'string') { + return pattern === path; + } + if (typeof pattern?.test === 'function') { + return pattern.test(path); + } + return false; + } + + cacheKey(index) { + let key = ''; + for (let i = index; i < this.#globStrings.length; i++) { + key += this.#globStrings[i]; + if (i !== this.#globStrings.length - 1) { + key += '/'; + } + } + return key; + } +} + +class Glob { + #root; + #exclude; + #cache = new Cache(); + #results = new SafeSet(); + #queue = []; + #subpatterns = new SafeMap(); + #patterns; + #withFileTypes; + constructor(pattern, options = kEmptyObject) { + validateObject(options, 'options'); + const { exclude, cwd, withFileTypes } = options; + if (exclude != null) { + validateFunction(exclude, 'options.exclude'); + } + this.#root = cwd ?? '.'; + this.#exclude = exclude; + this.#withFileTypes = !!withFileTypes; + let patterns; + if (typeof pattern === 'object') { + validateStringArray(pattern, 'patterns'); + patterns = pattern; + } else { + validateString(pattern, 'patterns'); + patterns = [pattern]; + } + this.matchers = ArrayPrototypeMap(patterns, (pattern) => new (lazyMinimatch().Minimatch)(pattern, { + __proto__: null, + nocase: isWindows || isMacOS, + windowsPathsNoEscape: true, + nonegate: true, + nocomment: true, + optimizationLevel: 2, + platform: process.platform, + nocaseMagicOnly: true, + })); + + this.#patterns = ArrayPrototypeFlatMap(this.matchers, (matcher) => ArrayPrototypeMap(matcher.set, + (pattern, i) => new Pattern( + pattern, + matcher.globParts[i], + new SafeSet().add(0), + new SafeSet(), + ))); + } + + globSync() { + ArrayPrototypePush(this.#queue, { __proto__: null, path: '.', patterns: this.#patterns }); + while (this.#queue.length > 0) { + const item = ArrayPrototypePop(this.#queue); + for (let i = 0; i < item.patterns.length; i++) { + this.#addSubpatterns(item.path, item.patterns[i]); + } + this.#subpatterns + .forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns })); + this.#subpatterns.clear(); + } + return ArrayFrom( + this.#results, + this.#withFileTypes ? (path) => this.#cache.statSync( + isAbsolute(path) ? + path : + join(this.#root, path), + ) : undefined, + ); + } + #addSubpattern(path, pattern) { + if (!this.#subpatterns.has(path)) { + this.#subpatterns.set(path, [pattern]); + } else { + ArrayPrototypePush(this.#subpatterns.get(path), pattern); + } + } + #addSubpatterns(path, pattern) { + const seen = this.#cache.add(path, pattern); + if (seen) { + return; + } + const fullpath = resolve(this.#root, path); + const stat = this.#cache.statSync(fullpath); + const last = pattern.last; + const isDirectory = stat?.isDirectory() || (stat?.isSymbolicLink() && pattern.hasSeenSymlinks); + const isLast = pattern.isLast(isDirectory); + const isFirst = pattern.isFirst(); + + if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) { + // Absolute path, go to root + this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet().add(1))); + return; + } + if (isFirst && pattern.at(0) === '') { + // Absolute path, go to root + this.#addSubpattern('/', pattern.child(new SafeSet().add(1))); + return; + } + if (isFirst && pattern.at(0) === '..') { + // Start with .., go to parent + this.#addSubpattern('../', pattern.child(new SafeSet().add(1))); + return; + } + if (isFirst && pattern.at(0) === '.') { + // Start with ., proceed + this.#addSubpattern('.', pattern.child(new SafeSet().add(1))); + return; + } + + if (isLast && typeof pattern.at(-1) === 'string') { + // Add result if it exists + const p = pattern.at(-1); + const stat = this.#cache.statSync(join(fullpath, p)); + if (stat && (p || isDirectory)) { + this.#results.add(join(path, p)); + } + if (pattern.indexes.size === 1 && pattern.indexes.has(last)) { + return; + } + } else if (isLast && pattern.at(-1) === lazyMinimatch().GLOBSTAR && + (path !== '.' || pattern.at(0) === '.' || (last === 0 && stat))) { + // If pattern ends with **, add to results + // if path is ".", add it only if pattern starts with "." or pattern is exactly "**" + this.#results.add(path); + } + + if (!isDirectory) { + return; + } + + let children; + const firstPattern = pattern.indexes.size === 1 && pattern.at(pattern.indexes.values().next().value); + if (typeof firstPattern === 'string') { + const stat = this.#cache.statSync(join(fullpath, firstPattern)); + if (stat) { + stat.name = firstPattern; + children = [stat]; + } else { + return; + } + } else { + children = this.#cache.readdirSync(fullpath); + } + + for (let i = 0; i < children.length; i++) { + const entry = children[i]; + const entryPath = join(path, entry.name); + this.#cache.addToStatCache(join(fullpath, entry.name), entry); + + const subPatterns = new SafeSet(); + const nSymlinks = new SafeSet(); + for (const index of pattern.indexes) { + // For each child, check potential patterns + if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) { + return; + } + const current = pattern.at(index); + const nextIndex = index + 1; + const next = pattern.at(nextIndex); + const fromSymlink = pattern.symlinks.has(index); + + if (current === lazyMinimatch().GLOBSTAR) { + if (entry.name[0] === '.' || (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) { + continue; + } + if (!fromSymlink && entry.isDirectory()) { + // If directory, add ** to its potential patterns + subPatterns.add(index); + } else if (!fromSymlink && index === last) { + // If ** is last, add to results + this.#results.add(entryPath); + } + + // Any pattern after ** is also a potential pattern + // so we can already test it here + const nextMatches = pattern.test(nextIndex, entry.name); + if (nextMatches && nextIndex === last && !isLast) { + // If next pattern is the last one, add to results + this.#results.add(entryPath); + } else if (nextMatches && entry.isDirectory()) { + // Pattern matched, meaning two patterns forward + // are also potential patterns + // e.g **/b/c when entry is a/b - add c to potential patterns + subPatterns.add(index + 2); + } + if ((nextMatches || pattern.at(0) === '.') && + (entry.isDirectory() || entry.isSymbolicLink()) && !fromSymlink) { + // If pattern after ** matches, or pattern starts with "." + // and entry is a directory or symlink, add to potential patterns + subPatterns.add(nextIndex); + } + + if (entry.isSymbolicLink()) { + nSymlinks.add(index); + } + + if (next === '..' && entry.isDirectory()) { + // In case pattern is "**/..", + // both parent and current directory should be added to the queue + // if this is the last pattern, add to results instead + const parent = join(path, '..'); + if (nextIndex < last) { + if (!this.#subpatterns.has(path) && !this.#cache.seen(path, pattern, nextIndex + 1)) { + this.#subpatterns.set(path, [pattern.child(new SafeSet().add(nextIndex + 1))]); + } + if (!this.#subpatterns.has(parent) && !this.#cache.seen(parent, pattern, nextIndex + 1)) { + this.#subpatterns.set(parent, [pattern.child(new SafeSet().add(nextIndex + 1))]); + } + } else { + if (!this.#cache.seen(path, pattern, nextIndex)) { + this.#cache.add(path, pattern.child(new SafeSet().add(nextIndex))); + this.#results.add(path); + } + if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) { + this.#cache.add(parent, pattern.child(new SafeSet().add(nextIndex))); + this.#results.add(parent); + } + } + } + } + if (typeof current === 'string') { + if (pattern.test(index, entry.name) && index !== last) { + // If current pattern matches entry name + // the next pattern is a potential pattern + subPatterns.add(nextIndex); + } else if (current === '.' && pattern.test(nextIndex, entry.name)) { + // If current pattern is ".", proceed to test next pattern + if (nextIndex === last) { + this.#results.add(entryPath); + } else { + subPatterns.add(nextIndex + 1); + } + } + } + if (typeof current === 'object' && pattern.test(index, entry.name)) { + // If current pattern is a regex that matches entry name (e.g *.js) + // add next pattern to potential patterns, or to results if it's the last pattern + if (index === last) { + this.#results.add(entryPath); + } else if (entry.isDirectory()) { + subPatterns.add(nextIndex); + } + } + } + if (subPatterns.size > 0) { + // If there are potential patterns, add to queue + this.#addSubpattern(entryPath, pattern.child(subPatterns, nSymlinks)); + } + } + } + + + async* glob() { + ArrayPrototypePush(this.#queue, { __proto__: null, path: '.', patterns: this.#patterns }); + while (this.#queue.length > 0) { + const item = ArrayPrototypePop(this.#queue); + for (let i = 0; i < item.patterns.length; i++) { + yield* this.#iterateSubpatterns(item.path, item.patterns[i]); + } + this.#subpatterns + .forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns })); + this.#subpatterns.clear(); + } + } + async* #iterateSubpatterns(path, pattern) { + const seen = this.#cache.add(path, pattern); + if (seen) { + return; + } + const fullpath = resolve(this.#root, path); + const stat = await this.#cache.stat(fullpath); + const last = pattern.last; + const isDirectory = stat?.isDirectory() || (stat?.isSymbolicLink() && pattern.hasSeenSymlinks); + const isLast = pattern.isLast(isDirectory); + const isFirst = pattern.isFirst(); + + if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) { + // Absolute path, go to root + this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet().add(1))); + return; + } + if (isFirst && pattern.at(0) === '') { + // Absolute path, go to root + this.#addSubpattern('/', pattern.child(new SafeSet().add(1))); + return; + } + if (isFirst && pattern.at(0) === '..') { + // Start with .., go to parent + this.#addSubpattern('../', pattern.child(new SafeSet().add(1))); + return; + } + if (isFirst && pattern.at(0) === '.') { + // Start with ., proceed + this.#addSubpattern('.', pattern.child(new SafeSet().add(1))); + return; + } + + if (isLast && typeof pattern.at(-1) === 'string') { + // Add result if it exists + const p = pattern.at(-1); + const stat = await this.#cache.stat(join(fullpath, p)); + if (stat && (p || isDirectory)) { + const result = join(path, p); + if (!this.#results.has(result)) { + this.#results.add(result); + yield this.#withFileTypes ? stat : result; + } + } + if (pattern.indexes.size === 1 && pattern.indexes.has(last)) { + return; + } + } else if (isLast && pattern.at(-1) === lazyMinimatch().GLOBSTAR && + (path !== '.' || pattern.at(0) === '.' || (last === 0 && stat))) { + // If pattern ends with **, add to results + // if path is ".", add it only if pattern starts with "." or pattern is exactly "**" + if (!this.#results.has(path)) { + this.#results.add(path); + yield this.#withFileTypes ? stat : path; + } + } + + if (!isDirectory) { + return; + } + + let children; + const firstPattern = pattern.indexes.size === 1 && pattern.at(pattern.indexes.values().next().value); + if (typeof firstPattern === 'string') { + const stat = await this.#cache.stat(join(fullpath, firstPattern)); + if (stat) { + stat.name = firstPattern; + children = [stat]; + } else { + return; + } + } else { + children = await this.#cache.readdir(fullpath); + } + + for (let i = 0; i < children.length; i++) { + const entry = children[i]; + const entryPath = join(path, entry.name); + this.#cache.addToStatCache(join(fullpath, entry.name), entry); + + const subPatterns = new SafeSet(); + const nSymlinks = new SafeSet(); + for (const index of pattern.indexes) { + // For each child, check potential patterns + if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) { + return; + } + const current = pattern.at(index); + const nextIndex = index + 1; + const next = pattern.at(nextIndex); + const fromSymlink = pattern.symlinks.has(index); + + if (current === lazyMinimatch().GLOBSTAR) { + if (entry.name[0] === '.' || (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) { + continue; + } + if (!fromSymlink && entry.isDirectory()) { + // If directory, add ** to its potential patterns + subPatterns.add(index); + } else if (!fromSymlink && index === last) { + // If ** is last, add to results + if (!this.#results.has(entryPath)) { + this.#results.add(entryPath); + yield this.#withFileTypes ? entry : entryPath; + } + } + + // Any pattern after ** is also a potential pattern + // so we can already test it here + const nextMatches = pattern.test(nextIndex, entry.name); + if (nextMatches && nextIndex === last && !isLast) { + // If next pattern is the last one, add to results + if (!this.#results.has(entryPath)) { + this.#results.add(entryPath); + yield this.#withFileTypes ? entry : entryPath; + } + } else if (nextMatches && entry.isDirectory()) { + // Pattern matched, meaning two patterns forward + // are also potential patterns + // e.g **/b/c when entry is a/b - add c to potential patterns + subPatterns.add(index + 2); + } + if ((nextMatches || pattern.at(0) === '.') && + (entry.isDirectory() || entry.isSymbolicLink()) && !fromSymlink) { + // If pattern after ** matches, or pattern starts with "." + // and entry is a directory or symlink, add to potential patterns + subPatterns.add(nextIndex); + } + + if (entry.isSymbolicLink()) { + nSymlinks.add(index); + } + + if (next === '..' && entry.isDirectory()) { + // In case pattern is "**/..", + // both parent and current directory should be added to the queue + // if this is the last pattern, add to results instead + const parent = join(path, '..'); + if (nextIndex < last) { + if (!this.#subpatterns.has(path) && !this.#cache.seen(path, pattern, nextIndex + 1)) { + this.#subpatterns.set(path, [pattern.child(new SafeSet().add(nextIndex + 1))]); + } + if (!this.#subpatterns.has(parent) && !this.#cache.seen(parent, pattern, nextIndex + 1)) { + this.#subpatterns.set(parent, [pattern.child(new SafeSet().add(nextIndex + 1))]); + } + } else { + if (!this.#cache.seen(path, pattern, nextIndex)) { + this.#cache.add(path, pattern.child(new SafeSet().add(nextIndex))); + if (!this.#results.has(path)) { + this.#results.add(path); + yield this.#withFileTypes ? this.#cache.statSync(fullpath) : path; + } + } + if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) { + this.#cache.add(parent, pattern.child(new SafeSet().add(nextIndex))); + if (!this.#results.has(parent)) { + this.#results.add(parent); + yield this.#withFileTypes ? this.#cache.statSync(join(this.#root, parent)) : parent; + } + } + } + } + } + if (typeof current === 'string') { + if (pattern.test(index, entry.name) && index !== last) { + // If current pattern matches entry name + // the next pattern is a potential pattern + subPatterns.add(nextIndex); + } else if (current === '.' && pattern.test(nextIndex, entry.name)) { + // If current pattern is ".", proceed to test next pattern + if (nextIndex === last) { + if (!this.#results.has(entryPath)) { + this.#results.add(entryPath); + yield this.#withFileTypes ? entry : entryPath; + } + } else { + subPatterns.add(nextIndex + 1); + } + } + } + if (typeof current === 'object' && pattern.test(index, entry.name)) { + // If current pattern is a regex that matches entry name (e.g *.js) + // add next pattern to potential patterns, or to results if it's the last pattern + if (index === last) { + if (!this.#results.has(entryPath)) { + this.#results.add(entryPath); + yield this.#withFileTypes ? entry : entryPath; + } + } else if (entry.isDirectory()) { + subPatterns.add(nextIndex); + } + } + } + if (subPatterns.size > 0) { + // If there are potential patterns, add to queue + this.#addSubpattern(entryPath, pattern.child(subPatterns, nSymlinks)); + } + } + } +} + +module.exports = { + __proto__: null, + Glob, +}; \ No newline at end of file diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js new file mode 100644 index 0000000..88dc1d5 --- /dev/null +++ b/lib/internal/fs/utils.js @@ -0,0 +1,21 @@ +const { Dirent } = require('node:fs'); + +const kStats = Symbol('stats'); + +class DirentFromStats extends Dirent { + constructor(name, stats, path) { + super(name, null, path); + this[kStats] = stats; + } +} + +for (const name of Reflect.ownKeys(Dirent.prototype)) { + if (name === 'constructor') { + continue; + } + DirentFromStats.prototype[name] = function () { + return this[kStats][name](); + }; +} + +module.exports = { DirentFromStats } \ No newline at end of file diff --git a/lib/internal/main/test_runner.js b/lib/internal/main/test_runner.js deleted file mode 100644 index f058df8..0000000 --- a/lib/internal/main/test_runner.js +++ /dev/null @@ -1,27 +0,0 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/lib/internal/main/test_runner.js -'use strict' -const { - prepareMainThreadExecution -} = require('#internal/process/pre_execution') -const { isUsingInspector } = require('#internal/util/inspector') -const { run } = require('#internal/test_runner/runner') -const { setupTestReporters } = require('#internal/test_runner/utils') - -prepareMainThreadExecution(false) -// markBootstrapComplete(); - -let concurrency = true -let inspectPort - -if (isUsingInspector()) { - process.emitWarning('Using the inspector with --test forces running at a concurrency of 1. ' + - 'Use the inspectPort option to run with concurrency') - concurrency = 1 - inspectPort = process.debugPort -} - -const testsStream = run({ concurrency, inspectPort }) -testsStream.once('test:fail', () => { - process.exitCode = 1 -}) -setupTestReporters(testsStream) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js new file mode 100644 index 0000000..bdfc053 --- /dev/null +++ b/lib/internal/modules/cjs/loader.js @@ -0,0 +1,3 @@ +module.exports = { + Module: module.constructor +} \ No newline at end of file diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js new file mode 100644 index 0000000..1026eed --- /dev/null +++ b/lib/internal/modules/esm/loader.js @@ -0,0 +1,19 @@ +const { + codes: { UNSUPPORTED_FEATURE } +} = require('#internal/errors'); + +const cascadedLoader = { + import(a, _, b) { + return import(a, b); + }, + + register() { + throw new UNSUPPORTED_FEATURE('Module mocking'); + }, +} + +module.exports = { + getOrInitializeCascadedLoader() { + return cascadedLoader + } +} \ No newline at end of file diff --git a/lib/internal/options.js b/lib/internal/options.js index 9645d94..9e5ae65 100644 --- a/lib/internal/options.js +++ b/lib/internal/options.js @@ -1,14 +1,114 @@ -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/lib/internal/options.js +const { + codes: { ERR_INVALID_ARG_VALUE, ERR_UNSUPPORTED_FEATURE } +} = require('#internal/errors') -'use strict' +const notParsedAsOptions = []; +const parsedAsOptions = []; -const argv = Object.create(null) +function getDefaultValue(type, def) { + if (def !== undefined) return def; + const defaults = { + 'array': [], + 'boolean': false, + }; + return defaults[type] ?? undefined; +} + +function parseOptions(options, map) { + const parsedOptions = Object.fromEntries( + Object.entries(map).map(([key, { type, default: def }]) => [ + key, getDefaultValue(type, def) + ]) + ); + + + for (let i = 0; i < options.length; i++) { + const option = options[i]; + let [key, value] = option.split('='); + + + // Skip if the key is not in the map or if it is disallowed + if (!(key in map)) { + if ( + !key.startsWith('-') && + (i > process.execArgv.length) + ) notParsedAsOptions.push(key); + continue; + }; + + if (map[key].disallowed) { + throw new ERR_UNSUPPORTED_FEATURE(key); + }; -function getOptionValue (optionName) { - return argv[optionName.slice(2)] // remove leading -- + const { type, valueMustConnect = true } = map[key]; + + if (!value && !valueMustConnect) { + value = options[++i]; + if (value.startsWith('-')) { + throw new ERR_INVALID_ARG_VALUE(key, undefined, 'requires a value'); + } + } + parsedAsOptions.push(option); + switch (type) { + case 'array': + parsedOptions[key] = [...(parsedOptions[key] || []), value]; + break; + case 'boolean': + parsedOptions[key] = true; // Set boolean to true + break; + case 'number': + const numValue = parseFloat(value); + if (isNaN(numValue)) { + throw new Error(`Invalid value for number: ${value}`); + } + parsedOptions[key] = numValue; + break; + case 'string': + parsedOptions[key] = value; // Set the string value + break; + default: + throw new Error(`Unknown type: ${type}`); + } + } + + return parsedOptions; } +const options = parseOptions([ + ...process.execArgv, + ...process.argv.slice(1), +], { + '--import': { type: 'array', valueMustConnect: false }, + '--experimental-test-snapshots': { type: 'boolean' }, + '--experimental-strip-types': { type: 'boolean', disallowed: true }, + '--test': { type: 'boolean' }, + '--experimental-test-coverage': { type: 'boolean' }, + '--test-force-exit': { type: 'boolean' }, + '--enable-source-maps': { type: 'boolean' }, + '--test-update-snapshots': { type: 'boolean' }, + '--watch': { type: 'boolean', disallowed: true }, + '--test-only': { type: 'boolean' }, + '--test-reporter-destination': { type: 'array', valueMustConnect: false }, + '--test-reporter': { type: 'array', valueMustConnect: false }, + '--experimental-test-isolation': { type: 'string' }, + '--test-timeout': { type: 'number' }, + '--test-concurrency': { type: 'number' }, + '--test-shard': { type: 'string' }, + '--test-name-pattern': { type: 'array' }, + '--test-skip-pattern': { type: 'array' }, + '--test-coverage-exclude': { type: 'array' }, + '--test-coverage-include': { type: 'array' }, + '--test-coverage-branches': { type: 'number' }, + '--test-coverage-lines': { type: 'number' }, + '--test-coverage-functions': { type: 'number' }, + '--experimental-test-module-mocks': { type: 'boolean' }, +}); + module.exports = { - argv, - getOptionValue -} + getOptionValue(option) { + return options[option]; + }, + // Useful for determining file arguments + notParsedAsOptions, + parsedAsOptions +}; \ No newline at end of file diff --git a/lib/internal/per_context/primordials.js b/lib/internal/per_context/primordials.js deleted file mode 100644 index 50b76b9..0000000 --- a/lib/internal/per_context/primordials.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict' - -const replaceAll = require('string.prototype.replaceall') - -exports.hardenRegExp = (re) => re -exports.ArrayFrom = (it, mapFn) => Array.from(it, mapFn) -exports.ArrayIsArray = Array.isArray -exports.ArrayPrototypeConcat = (arr, ...el) => arr.concat(...el) -exports.ArrayPrototypeFilter = (arr, fn) => arr.filter(fn) -exports.ArrayPrototypeFind = (arr, fn) => arr.find(fn) -exports.ArrayPrototypeForEach = (arr, fn, thisArg) => arr.forEach(fn, thisArg) -exports.ArrayPrototypeIncludes = (arr, el, fromIndex) => arr.includes(el, fromIndex) -exports.ArrayPrototypeJoin = (arr, str) => arr.join(str) -exports.ArrayPrototypeMap = (arr, mapFn) => arr.map(mapFn) -exports.ArrayPrototypePop = arr => arr.pop() -exports.ArrayPrototypePush = (arr, ...el) => arr.push(...el) -exports.ArrayPrototypeReduce = (arr, fn, originalVal) => arr.reduce(fn, originalVal) -exports.ArrayPrototypeShift = arr => arr.shift() -exports.ArrayPrototypeSlice = (arr, offset) => arr.slice(offset) -exports.ArrayPrototypeSome = (arr, fn) => arr.some(fn) -exports.ArrayPrototypeSort = (arr, fn) => arr.sort(fn) -exports.ArrayPrototypeSplice = (arr, offset, len, ...el) => arr.splice(offset, len, ...el) -exports.ArrayPrototypeUnshift = (arr, ...el) => arr.unshift(...el) -exports.Boolean = Boolean -exports.Error = Error -exports.ErrorCaptureStackTrace = (...args) => Error.captureStackTrace(...args) -exports.FunctionPrototype = Function.prototype -exports.FunctionPrototypeBind = (fn, obj, ...args) => fn.bind(obj, ...args) -exports.FunctionPrototypeCall = (fn, obj, ...args) => fn.call(obj, ...args) -exports.MathMax = (...args) => Math.max(...args) -exports.Number = Number -exports.NumberIsInteger = Number.isInteger -exports.NumberIsNaN = Number.isNaN -exports.NumberParseInt = (str, radix) => Number.parseInt(str, radix) -exports.NumberMIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER -exports.NumberMAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER -exports.ObjectAssign = (target, ...sources) => Object.assign(target, ...sources) -exports.ObjectCreate = obj => Object.create(obj) -exports.ObjectDefineProperties = (obj, props) => Object.defineProperties(obj, props) -exports.ObjectDefineProperty = (obj, key, descr) => Object.defineProperty(obj, key, descr) -exports.ObjectEntries = obj => Object.entries(obj) -exports.ObjectFreeze = obj => Object.freeze(obj) -exports.ObjectGetOwnPropertyDescriptor = (obj, key) => Object.getOwnPropertyDescriptor(obj, key) -exports.ObjectGetPrototypeOf = obj => Object.getPrototypeOf(obj) -exports.ObjectIsExtensible = obj => Object.isExtensible(obj) -exports.ObjectPrototypeHasOwnProperty = (obj, property) => Object.prototype.hasOwnProperty.call(obj, property) -exports.ObjectSeal = (obj) => Object.seal(obj) -exports.ReflectApply = (target, self, args) => Reflect.apply(target, self, args) -exports.Promise = Promise -exports.PromiseAll = iterator => Promise.all(iterator) -exports.PromisePrototypeThen = (promise, thenFn, catchFn) => promise.then(thenFn, catchFn) -exports.PromiseResolve = val => Promise.resolve(val) -exports.PromiseRace = val => Promise.race(val) -exports.Proxy = Proxy -exports.RegExp = RegExp -exports.RegExpPrototypeSymbolSplit = (reg, str) => reg[Symbol.split](str) -exports.SafeArrayIterator = class ArrayIterator {constructor (array) { this.array = array }[Symbol.iterator] () { return this.array.values() }} -exports.SafeMap = Map -exports.SafePromiseAll = (array, mapFn) => Promise.all(mapFn ? array.map(mapFn) : array) -exports.SafePromiseAllReturnArrayLike = (array, mapFn) => Promise.all(mapFn ? array.map(mapFn) : array) -exports.SafePromiseRace = (array, mapFn) => Promise.race(mapFn ? array.map(mapFn) : array) -exports.SafeSet = Set -exports.SafeWeakMap = WeakMap -exports.SafeWeakSet = WeakSet -exports.String = String -exports.StringPrototypeEndsWith = (haystack, needle, index) => haystack.endsWith(needle, index) -exports.StringPrototypeIncludes = (str, needle) => str.includes(needle) -exports.StringPrototypeIndexOf = (str, needle, offset) => str.indexOf(needle, offset) -exports.StringPrototypeMatch = (str, reg) => str.match(reg) -exports.StringPrototypeRepeat = (str, times) => str.repeat(times) -exports.StringPrototypeReplace = (str, search, replacement) => - str.replace(search, replacement) -exports.StringPrototypeReplaceAll = replaceAll -exports.StringPrototypeStartsWith = (haystack, needle, index) => haystack.startsWith(needle, index) -exports.StringPrototypeSlice = (str, ...args) => str.slice(...args) -exports.StringPrototypeSplit = (str, search, limit) => str.split(search, limit) -exports.StringPrototypeSubstring = (str, ...args) => str.substring(...args) -exports.StringPrototypeToUpperCase = str => str.toUpperCase() -exports.StringPrototypeTrim = str => str.trim() -exports.Symbol = Symbol -exports.SymbolFor = repr => Symbol.for(repr) -exports.ReflectApply = (target, self, args) => Reflect.apply(target, self, args) -exports.ReflectConstruct = (target, args, newTarget) => Reflect.construct(target, args, newTarget) -exports.ReflectGet = (target, property, receiver) => Reflect.get(target, property, receiver) -exports.RegExpPrototypeExec = (reg, str) => reg.exec(str) -exports.RegExpPrototypeSymbolReplace = (regexp, str, replacement) => - regexp[Symbol.replace](str, replacement) diff --git a/lib/internal/priority_queue.js b/lib/internal/priority_queue.js new file mode 100644 index 0000000..462d4b4 --- /dev/null +++ b/lib/internal/priority_queue.js @@ -0,0 +1,115 @@ +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; + +const { + Array, +} = primordials; + +// The PriorityQueue is a basic implementation of a binary heap that accepts +// a custom sorting function via its constructor. This function is passed +// the two nodes to compare, similar to the native Array#sort. Crucially +// this enables priority queues that are based on a comparison of more than +// just a single criteria. + +module.exports = class PriorityQueue { + #compare = (a, b) => a - b; + #heap = new Array(64); + #setPosition; + #size = 0; + + constructor(comparator, setPosition) { + if (comparator !== undefined) + this.#compare = comparator; + if (setPosition !== undefined) + this.#setPosition = setPosition; + } + + insert(value) { + const heap = this.#heap; + const pos = ++this.#size; + heap[pos] = value; + + if (heap.length === pos) + heap.length *= 2; + + this.percolateUp(pos); + } + + peek() { + return this.#heap[1]; + } + + peekBottom() { + return this.#heap[this.#size]; + } + + percolateDown(pos) { + const compare = this.#compare; + const setPosition = this.#setPosition; + const heap = this.#heap; + const size = this.#size; + const item = heap[pos]; + + while (pos * 2 <= size) { + let childIndex = pos * 2 + 1; + if (childIndex > size || compare(heap[pos * 2], heap[childIndex]) < 0) + childIndex = pos * 2; + const child = heap[childIndex]; + if (compare(item, child) <= 0) + break; + if (setPosition !== undefined) + setPosition(child, pos); + heap[pos] = child; + pos = childIndex; + } + heap[pos] = item; + if (setPosition !== undefined) + setPosition(item, pos); + } + + percolateUp(pos) { + const heap = this.#heap; + const compare = this.#compare; + const setPosition = this.#setPosition; + const item = heap[pos]; + + while (pos > 1) { + const parent = heap[pos / 2 | 0]; + if (compare(parent, item) <= 0) + break; + heap[pos] = parent; + if (setPosition !== undefined) + setPosition(parent, pos); + pos = pos / 2 | 0; + } + heap[pos] = item; + if (setPosition !== undefined) + setPosition(item, pos); + } + + removeAt(pos) { + const heap = this.#heap; + const size = --this.#size; + heap[pos] = heap[size + 1]; + heap[size + 1] = undefined; + + if (size > 0 && pos <= size) { + if (pos > 1 && this.#compare(heap[pos / 2 | 0], heap[pos]) > 0) + this.percolateUp(pos); + else + this.percolateDown(pos); + } + } + + shift() { + const heap = this.#heap; + const value = heap[1]; + if (value === undefined) + return; + + this.removeAt(1); + + return value; + } +}; \ No newline at end of file diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js deleted file mode 100644 index da3f13e..0000000 --- a/lib/internal/process/pre_execution.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -module.exports = { - prepareMainThreadExecution () {} -} diff --git a/lib/internal/process/task_queues.js b/lib/internal/process/task_queues.js new file mode 100644 index 0000000..530f976 --- /dev/null +++ b/lib/internal/process/task_queues.js @@ -0,0 +1,3 @@ +module.exports = { + queueMicrotask, +} \ No newline at end of file diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js new file mode 100644 index 0000000..85cc816 --- /dev/null +++ b/lib/internal/readline/interface.js @@ -0,0 +1,4 @@ +const { Interface } = require('node:readline'); +module.exports = { + Interface +}; \ No newline at end of file diff --git a/lib/internal/source_map/source_map_cache.js b/lib/internal/source_map/source_map_cache.js new file mode 100644 index 0000000..205c46a --- /dev/null +++ b/lib/internal/source_map/source_map_cache.js @@ -0,0 +1,2 @@ +const { codes: { ERR_UNSUPPORTED_FEATURE } } = require('#internal/errors'); +throw new ERR_UNSUPPORTED_FEATURE('Sourcemap caching') \ No newline at end of file diff --git a/lib/internal/streams/end-of-stream.js b/lib/internal/streams/end-of-stream.js new file mode 100644 index 0000000..7e31bac --- /dev/null +++ b/lib/internal/streams/end-of-stream.js @@ -0,0 +1,329 @@ +const { primordials, internalBinding } = require('#lib/bootstrap'); + +// Ported from https://github.com/mafintosh/end-of-stream with +// permission from the author, Mathias Buus (@mafintosh). + +'use strict'; + +const { + Promise, + PromisePrototypeThen, + SymbolDispose, +} = primordials; + +const { + AbortError, + codes: { + ERR_INVALID_ARG_TYPE, + ERR_STREAM_PREMATURE_CLOSE, + }, +} = require('#internal/errors'); +const { + kEmptyObject, + once, +} = require('#internal/util'); +const { + validateAbortSignal, + validateFunction, + validateObject, + validateBoolean, +} = require('#internal/validators'); + +const { + isClosed, + isReadable, + isReadableNodeStream, + isReadableStream, + isReadableFinished, + isReadableErrored, + isWritable, + isWritableNodeStream, + isWritableStream, + isWritableFinished, + isWritableErrored, + isNodeStream, + willEmitClose: _willEmitClose, + kIsClosedPromise, +} = require('#internal/streams/utils'); +let addAbortListener; + +function isRequest(stream) { + return stream.setHeader && typeof stream.abort === 'function'; +} + +const nop = () => {}; + +function eos(stream, options, callback) { + if (arguments.length === 2) { + callback = options; + options = kEmptyObject; + } else if (options == null) { + options = kEmptyObject; + } else { + validateObject(options, 'options'); + } + validateFunction(callback, 'callback'); + validateAbortSignal(options.signal, 'options.signal'); + + callback = once(callback); + + if (isReadableStream(stream) || isWritableStream(stream)) { + return eosWeb(stream, options, callback); + } + + if (!isNodeStream(stream)) { + throw new ERR_INVALID_ARG_TYPE('stream', ['ReadableStream', 'WritableStream', 'Stream'], stream); + } + + const readable = options.readable ?? isReadableNodeStream(stream); + const writable = options.writable ?? isWritableNodeStream(stream); + + const wState = stream._writableState; + const rState = stream._readableState; + + const onlegacyfinish = () => { + if (!stream.writable) { + onfinish(); + } + }; + + // TODO (ronag): Improve soft detection to include core modules and + // common ecosystem modules that do properly emit 'close' but fail + // this generic check. + let willEmitClose = ( + _willEmitClose(stream) && + isReadableNodeStream(stream) === readable && + isWritableNodeStream(stream) === writable + ); + + let writableFinished = isWritableFinished(stream, false); + const onfinish = () => { + writableFinished = true; + // Stream should not be destroyed here. If it is that + // means that user space is doing something differently and + // we cannot trust willEmitClose. + if (stream.destroyed) { + willEmitClose = false; + } + + if (willEmitClose && (!stream.readable || readable)) { + return; + } + + if (!readable || readableFinished) { + callback.call(stream); + } + }; + + let readableFinished = isReadableFinished(stream, false); + const onend = () => { + readableFinished = true; + // Stream should not be destroyed here. If it is that + // means that user space is doing something differently and + // we cannot trust willEmitClose. + if (stream.destroyed) { + willEmitClose = false; + } + + if (willEmitClose && (!stream.writable || writable)) { + return; + } + + if (!writable || writableFinished) { + callback.call(stream); + } + }; + + const onerror = (err) => { + callback.call(stream, err); + }; + + let closed = isClosed(stream); + + const onclose = () => { + closed = true; + + const errored = isWritableErrored(stream) || isReadableErrored(stream); + + if (errored && typeof errored !== 'boolean') { + return callback.call(stream, errored); + } + + if (readable && !readableFinished && isReadableNodeStream(stream, true)) { + if (!isReadableFinished(stream, false)) + return callback.call(stream, + new ERR_STREAM_PREMATURE_CLOSE()); + } + if (writable && !writableFinished) { + if (!isWritableFinished(stream, false)) + return callback.call(stream, + new ERR_STREAM_PREMATURE_CLOSE()); + } + + callback.call(stream); + }; + + const onclosed = () => { + closed = true; + + const errored = isWritableErrored(stream) || isReadableErrored(stream); + + if (errored && typeof errored !== 'boolean') { + return callback.call(stream, errored); + } + + callback.call(stream); + }; + + const onrequest = () => { + stream.req.on('finish', onfinish); + }; + + if (isRequest(stream)) { + stream.on('complete', onfinish); + if (!willEmitClose) { + stream.on('abort', onclose); + } + if (stream.req) { + onrequest(); + } else { + stream.on('request', onrequest); + } + } else if (writable && !wState) { // legacy streams + stream.on('end', onlegacyfinish); + stream.on('close', onlegacyfinish); + } + + // Not all streams will emit 'close' after 'aborted'. + if (!willEmitClose && typeof stream.aborted === 'boolean') { + stream.on('aborted', onclose); + } + + stream.on('end', onend); + stream.on('finish', onfinish); + if (options.error !== false) { + stream.on('error', onerror); + } + stream.on('close', onclose); + + if (closed) { + process.nextTick(onclose); + } else if (wState?.errorEmitted || rState?.errorEmitted) { + if (!willEmitClose) { + process.nextTick(onclosed); + } + } else if ( + !readable && + (!willEmitClose || isReadable(stream)) && + (writableFinished || isWritable(stream) === false) && + (wState == null || wState.pendingcb === undefined || wState.pendingcb === 0) + ) { + process.nextTick(onclosed); + } else if ( + !writable && + (!willEmitClose || isWritable(stream)) && + (readableFinished || isReadable(stream) === false) + ) { + process.nextTick(onclosed); + } else if ((rState && stream.req && stream.aborted)) { + process.nextTick(onclosed); + } + + const cleanup = () => { + callback = nop; + stream.removeListener('aborted', onclose); + stream.removeListener('complete', onfinish); + stream.removeListener('abort', onclose); + stream.removeListener('request', onrequest); + if (stream.req) stream.req.removeListener('finish', onfinish); + stream.removeListener('end', onlegacyfinish); + stream.removeListener('close', onlegacyfinish); + stream.removeListener('finish', onfinish); + stream.removeListener('end', onend); + stream.removeListener('error', onerror); + stream.removeListener('close', onclose); + }; + + if (options.signal && !closed) { + const abort = () => { + // Keep it because cleanup removes it. + const endCallback = callback; + cleanup(); + endCallback.call( + stream, + new AbortError(undefined, { cause: options.signal.reason })); + }; + if (options.signal.aborted) { + process.nextTick(abort); + } else { + addAbortListener ??= require('#internal/events/abort_listener').addAbortListener; + const disposable = addAbortListener(options.signal, abort); + const originalCallback = callback; + callback = once((...args) => { + disposable[SymbolDispose](); + originalCallback.apply(stream, args); + }); + } + } + + return cleanup; +} + +function eosWeb(stream, options, callback) { + let isAborted = false; + let abort = nop; + if (options.signal) { + abort = () => { + isAborted = true; + callback.call(stream, new AbortError(undefined, { cause: options.signal.reason })); + }; + if (options.signal.aborted) { + process.nextTick(abort); + } else { + addAbortListener ??= require('#internal/events/abort_listener').addAbortListener; + const disposable = addAbortListener(options.signal, abort); + const originalCallback = callback; + callback = once((...args) => { + disposable[SymbolDispose](); + originalCallback.apply(stream, args); + }); + } + } + const resolverFn = (...args) => { + if (!isAborted) { + process.nextTick(() => callback.apply(stream, args)); + } + }; + PromisePrototypeThen( + stream[kIsClosedPromise].promise, + resolverFn, + resolverFn, + ); + return nop; +} + +function finished(stream, opts) { + let autoCleanup = false; + if (opts === null) { + opts = kEmptyObject; + } + if (opts?.cleanup) { + validateBoolean(opts.cleanup, 'cleanup'); + autoCleanup = opts.cleanup; + } + return new Promise((resolve, reject) => { + const cleanup = eos(stream, opts, (err) => { + if (autoCleanup) { + cleanup(); + } + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +module.exports = eos; +module.exports.finished = finished; \ No newline at end of file diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js new file mode 100644 index 0000000..afcc588 --- /dev/null +++ b/lib/internal/streams/readable.js @@ -0,0 +1,3 @@ +const { Readable } = require('node:stream'); + +module.exports = Readable; diff --git a/lib/internal/streams/transform.js b/lib/internal/streams/transform.js new file mode 100644 index 0000000..0b33e15 --- /dev/null +++ b/lib/internal/streams/transform.js @@ -0,0 +1 @@ +module.exports = require('node:stream').Transform; \ No newline at end of file diff --git a/lib/internal/streams/utils.js b/lib/internal/streams/utils.js new file mode 100644 index 0000000..7d5eabf --- /dev/null +++ b/lib/internal/streams/utils.js @@ -0,0 +1,365 @@ +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; + +const { + Symbol, + SymbolAsyncIterator, + SymbolFor, + SymbolIterator, +} = primordials; + +// We need to use SymbolFor to make these globally available +// for interoperability with readable-stream, i.e. readable-stream +// and node core needs to be able to read/write private state +// from each other for proper interoperability. +const kIsDestroyed = SymbolFor('nodejs.stream.destroyed'); +const kIsErrored = SymbolFor('nodejs.stream.errored'); +const kIsReadable = SymbolFor('nodejs.stream.readable'); +const kIsWritable = SymbolFor('nodejs.stream.writable'); +const kIsDisturbed = SymbolFor('nodejs.stream.disturbed'); + +const kOnConstructed = Symbol('kOnConstructed'); + +const kIsClosedPromise = SymbolFor('nodejs.webstream.isClosedPromise'); +const kControllerErrorFunction = SymbolFor('nodejs.webstream.controllerErrorFunction'); + +const kState = Symbol('kState'); +const kObjectMode = 1 << 0; +const kErrorEmitted = 1 << 1; +const kAutoDestroy = 1 << 2; +const kEmitClose = 1 << 3; +const kDestroyed = 1 << 4; +const kClosed = 1 << 5; +const kCloseEmitted = 1 << 6; +const kErrored = 1 << 7; +const kConstructed = 1 << 8; + +function isReadableNodeStream(obj, strict = false) { + return !!( + obj && + typeof obj.pipe === 'function' && + typeof obj.on === 'function' && + ( + !strict || + (typeof obj.pause === 'function' && typeof obj.resume === 'function') + ) && + (!obj._writableState || obj._readableState?.readable !== false) && // Duplex + (!obj._writableState || obj._readableState) // Writable has .pipe. + ); +} + +function isWritableNodeStream(obj) { + return !!( + obj && + typeof obj.write === 'function' && + typeof obj.on === 'function' && + (!obj._readableState || obj._writableState?.writable !== false) // Duplex + ); +} + +function isDuplexNodeStream(obj) { + return !!( + obj && + (typeof obj.pipe === 'function' && obj._readableState) && + typeof obj.on === 'function' && + typeof obj.write === 'function' + ); +} + +function isNodeStream(obj) { + return ( + obj && + ( + obj._readableState || + obj._writableState || + (typeof obj.write === 'function' && typeof obj.on === 'function') || + (typeof obj.pipe === 'function' && typeof obj.on === 'function') + ) + ); +} + +function isReadableStream(obj) { + return !!( + obj && + !isNodeStream(obj) && + typeof obj.pipeThrough === 'function' && + typeof obj.getReader === 'function' && + typeof obj.cancel === 'function' + ); +} + +function isWritableStream(obj) { + return !!( + obj && + !isNodeStream(obj) && + typeof obj.getWriter === 'function' && + typeof obj.abort === 'function' + ); +} + +function isTransformStream(obj) { + return !!( + obj && + !isNodeStream(obj) && + typeof obj.readable === 'object' && + typeof obj.writable === 'object' + ); +} + +function isWebStream(obj) { + return isReadableStream(obj) || isWritableStream(obj) || isTransformStream(obj); +} + +function isIterable(obj, isAsync) { + if (obj == null) return false; + if (isAsync === true) return typeof obj[SymbolAsyncIterator] === 'function'; + if (isAsync === false) return typeof obj[SymbolIterator] === 'function'; + return typeof obj[SymbolAsyncIterator] === 'function' || + typeof obj[SymbolIterator] === 'function'; +} + +function isDestroyed(stream) { + if (!isNodeStream(stream)) return null; + const wState = stream._writableState; + const rState = stream._readableState; + const state = wState || rState; + return !!(stream.destroyed || stream[kIsDestroyed] || state?.destroyed); +} + +// Have been end():d. +function isWritableEnded(stream) { + if (!isWritableNodeStream(stream)) return null; + if (stream.writableEnded === true) return true; + const wState = stream._writableState; + if (wState?.errored) return false; + if (typeof wState?.ended !== 'boolean') return null; + return wState.ended; +} + +// Have emitted 'finish'. +function isWritableFinished(stream, strict) { + if (!isWritableNodeStream(stream)) return null; + if (stream.writableFinished === true) return true; + const wState = stream._writableState; + if (wState?.errored) return false; + if (typeof wState?.finished !== 'boolean') return null; + return !!( + wState.finished || + (strict === false && wState.ended === true && wState.length === 0) + ); +} + +// Have been push(null):d. +function isReadableEnded(stream) { + if (!isReadableNodeStream(stream)) return null; + if (stream.readableEnded === true) return true; + const rState = stream._readableState; + if (!rState || rState.errored) return false; + if (typeof rState?.ended !== 'boolean') return null; + return rState.ended; +} + +// Have emitted 'end'. +function isReadableFinished(stream, strict) { + if (!isReadableNodeStream(stream)) return null; + const rState = stream._readableState; + if (rState?.errored) return false; + if (typeof rState?.endEmitted !== 'boolean') return null; + return !!( + rState.endEmitted || + (strict === false && rState.ended === true && rState.length === 0) + ); +} + +function isReadable(stream) { + if (stream && stream[kIsReadable] != null) return stream[kIsReadable]; + if (typeof stream?.readable !== 'boolean') return null; + if (isDestroyed(stream)) return false; + return isReadableNodeStream(stream) && + stream.readable && + !isReadableFinished(stream); +} + +function isWritable(stream) { + if (stream && stream[kIsWritable] != null) return stream[kIsWritable]; + if (typeof stream?.writable !== 'boolean') return null; + if (isDestroyed(stream)) return false; + return isWritableNodeStream(stream) && + stream.writable && + !isWritableEnded(stream); +} + +function isFinished(stream, opts) { + if (!isNodeStream(stream)) { + return null; + } + + if (isDestroyed(stream)) { + return true; + } + + if (opts?.readable !== false && isReadable(stream)) { + return false; + } + + if (opts?.writable !== false && isWritable(stream)) { + return false; + } + + return true; +} + +function isWritableErrored(stream) { + if (!isNodeStream(stream)) { + return null; + } + + if (stream.writableErrored) { + return stream.writableErrored; + } + + return stream._writableState?.errored ?? null; +} + +function isReadableErrored(stream) { + if (!isNodeStream(stream)) { + return null; + } + + if (stream.readableErrored) { + return stream.readableErrored; + } + + return stream._readableState?.errored ?? null; +} + +function isClosed(stream) { + if (!isNodeStream(stream)) { + return null; + } + + if (typeof stream.closed === 'boolean') { + return stream.closed; + } + + const wState = stream._writableState; + const rState = stream._readableState; + + if ( + typeof wState?.closed === 'boolean' || + typeof rState?.closed === 'boolean' + ) { + return wState?.closed || rState?.closed; + } + + if (typeof stream._closed === 'boolean' && isOutgoingMessage(stream)) { + return stream._closed; + } + + return null; +} + +function isOutgoingMessage(stream) { + return ( + typeof stream._closed === 'boolean' && + typeof stream._defaultKeepAlive === 'boolean' && + typeof stream._removedConnection === 'boolean' && + typeof stream._removedContLen === 'boolean' + ); +} + +function isServerResponse(stream) { + return ( + typeof stream._sent100 === 'boolean' && + isOutgoingMessage(stream) + ); +} + +function isServerRequest(stream) { + return ( + typeof stream._consuming === 'boolean' && + typeof stream._dumped === 'boolean' && + stream.req?.upgradeOrConnect === undefined + ); +} + +function willEmitClose(stream) { + if (!isNodeStream(stream)) return null; + + const wState = stream._writableState; + const rState = stream._readableState; + const state = wState || rState; + + return (!state && isServerResponse(stream)) || !!( + state?.autoDestroy && + state.emitClose && + state.closed === false + ); +} + +function isDisturbed(stream) { + return !!(stream && ( + stream[kIsDisturbed] ?? + (stream.readableDidRead || stream.readableAborted) + )); +} + +function isErrored(stream) { + return !!(stream && ( + stream[kIsErrored] ?? + stream.readableErrored ?? + stream.writableErrored ?? + stream._readableState?.errorEmitted ?? + stream._writableState?.errorEmitted ?? + stream._readableState?.errored ?? + stream._writableState?.errored + )); +} + +module.exports = { + kOnConstructed, + isDestroyed, + kIsDestroyed, + isDisturbed, + kIsDisturbed, + isErrored, + kIsErrored, + isReadable, + kIsReadable, + kIsClosedPromise, + kControllerErrorFunction, + kIsWritable, + isClosed, + isDuplexNodeStream, + isFinished, + isIterable, + isReadableNodeStream, + isReadableStream, + isReadableEnded, + isReadableFinished, + isReadableErrored, + isNodeStream, + isWebStream, + isWritable, + isWritableNodeStream, + isWritableStream, + isWritableEnded, + isWritableFinished, + isWritableErrored, + isServerRequest, + isServerResponse, + willEmitClose, + isTransformStream, + kState, + // bitfields + kObjectMode, + kErrorEmitted, + kAutoDestroy, + kEmitClose, + kDestroyed, + kClosed, + kCloseEmitted, + kErrored, + kConstructed, +}; \ No newline at end of file diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js new file mode 100644 index 0000000..58d23f3 --- /dev/null +++ b/lib/internal/test_runner/coverage.js @@ -0,0 +1,687 @@ +const { primordials, internalBinding = require('#lib/bootstrap'); + +'use strict'; +const { + ArrayFrom, + ArrayPrototypeMap, + ArrayPrototypePush, + JSONParse, + MathFloor, + MathMax, + MathMin, + NumberParseInt, + ObjectAssign, + RegExpPrototypeExec, + RegExpPrototypeSymbolSplit, + SafeMap, + SafeSet, + StringPrototypeIncludes, + StringPrototypeLocaleCompare, + StringPrototypeStartsWith, +} = primordials; +const { + copyFileSync, + mkdirSync, + mkdtempSync, + opendirSync, + readFileSync, + rmSync, +} = require('fs'); +const { setupCoverageHooks } = require('#internal/util'); +const { tmpdir } = require('os'); +const { join, resolve, relative, matchesGlob } = require('path'); +const { fileURLToPath } = require('#internal/url'); +const { kMappings, SourceMap } = require('#internal/source_map/source_map'); +const { + codes: { + ERR_SOURCE_MAP_CORRUPT, + ERR_SOURCE_MAP_MISSING_SOURCE, + }, +} = require('#internal/errors'); +const kCoverageFileRegex = /^coverage-(\d+)-(\d{13})-(\d+)\.json$/; +const kIgnoreRegex = /\/\* node:coverage ignore next (?\d+ )?\*\//; +const kLineEndingRegex = /\r?\n$/u; +const kLineSplitRegex = /(?<=\r?\n)/u; +const kStatusRegex = /\/\* node:coverage (?enable|disable) \*\//; + +class CoverageLine { + constructor(line, startOffset, src, length = src?.length) { + const newlineLength = src == null ? 0 : + RegExpPrototypeExec(kLineEndingRegex, src)?.[0].length ?? 0; + + this.line = line; + this.src = src; + this.startOffset = startOffset; + this.endOffset = startOffset + length - newlineLength; + this.ignore = false; + this.count = this.startOffset === this.endOffset ? 1 : 0; + } +} + +class TestCoverage { + constructor(coverageDirectory, + originalCoverageDirectory, + workingDirectory, + excludeGlobs, + includeGlobs, + sourceMaps, + thresholds) { + this.coverageDirectory = coverageDirectory; + this.originalCoverageDirectory = originalCoverageDirectory; + this.workingDirectory = workingDirectory; + this.excludeGlobs = excludeGlobs; + this.includeGlobs = includeGlobs; + this.sourceMaps = sourceMaps; + this.thresholds = thresholds; + } + + #sourceLines = new SafeMap(); + + getLines(fileUrl, source) { + // Split the file source into lines. Make sure the lines maintain their + // original line endings because those characters are necessary for + // determining offsets in the file. + if (this.#sourceLines.has(fileUrl)) { + return this.#sourceLines.get(fileUrl); + } + + try { + source ??= readFileSync(fileURLToPath(fileUrl), 'utf8'); + } catch { + // The file can no longer be read. It may have been deleted among + // other possibilities. Leave it out of the coverage report. + this.#sourceLines.set(fileUrl, null); + return; + } + + const linesWithBreaks = + RegExpPrototypeSymbolSplit(kLineSplitRegex, source); + let ignoreCount = 0; + let enabled = true; + let offset = 0; + + const lines = ArrayPrototypeMap(linesWithBreaks, (line, i) => { + const startOffset = offset; + const coverageLine = new CoverageLine(i + 1, startOffset, line); + + offset += line.length; + + // Determine if this line is being ignored. + if (ignoreCount > 0) { + ignoreCount--; + coverageLine.ignore = true; + } else if (!enabled) { + coverageLine.ignore = true; + } + + if (!coverageLine.ignore) { + // If this line is not already being ignored, check for ignore + // comments. + const match = RegExpPrototypeExec(kIgnoreRegex, line); + + if (match !== null) { + ignoreCount = NumberParseInt(match.groups?.count ?? 1, 10); + } + } + + // Check for comments to enable/disable coverage no matter what. These + // take precedence over ignore comments. + const match = RegExpPrototypeExec(kStatusRegex, line); + const status = match?.groups?.status; + + if (status) { + ignoreCount = 0; + enabled = status === 'enable'; + } + + return coverageLine; + }); + this.#sourceLines.set(fileUrl, lines); + return lines; + } + + summary() { + internalBinding('profiler').takeCoverage(); + const coverage = this.getCoverageFromDirectory(); + const coverageSummary = { + __proto__: null, + workingDirectory: this.workingDirectory, + files: [], + totals: { + __proto__: null, + totalLineCount: 0, + totalBranchCount: 0, + totalFunctionCount: 0, + coveredLineCount: 0, + coveredBranchCount: 0, + coveredFunctionCount: 0, + coveredLinePercent: 0, + coveredBranchPercent: 0, + coveredFunctionPercent: 0, + }, + thresholds: this.thresholds, + }; + + if (!coverage) { + return coverageSummary; + } + + for (let i = 0; i < coverage.length; ++i) { + const { functions, url } = coverage[i]; + + let totalBranches = 0; + let totalFunctions = 0; + let branchesCovered = 0; + let functionsCovered = 0; + const functionReports = []; + const branchReports = []; + + const lines = this.getLines(url); + if (!lines) { + continue; + } + + + for (let j = 0; j < functions.length; ++j) { + const { isBlockCoverage, ranges } = functions[j]; + + let maxCountPerFunction = 0; + for (let k = 0; k < ranges.length; ++k) { + const range = ranges[k]; + maxCountPerFunction = MathMax(maxCountPerFunction, range.count); + + // Add some useful data to the range. The test runner has read these ranges + // from a file, so we own the data structures and can do what we want. + ObjectAssign(range, mapRangeToLines(range, lines)); + + if (isBlockCoverage) { + ArrayPrototypePush(branchReports, { + __proto__: null, + line: range.lines[0]?.line, + count: range.count, + }); + + if (range.count !== 0 || + range.ignoredLines === range.lines.length) { + branchesCovered++; + } + + totalBranches++; + } + } + + if (j > 0 && ranges.length > 0) { + const range = ranges[0]; + + ArrayPrototypePush(functionReports, { + __proto__: null, + name: functions[j].functionName, + count: maxCountPerFunction, + line: range.lines[0]?.line, + }); + + if (range.count !== 0 || range.ignoredLines === range.lines.length) { + functionsCovered++; + } + + totalFunctions++; + } + } + + let coveredCnt = 0; + const lineReports = []; + + for (let j = 0; j < lines.length; ++j) { + const line = lines[j]; + if (!line.ignore) { + ArrayPrototypePush(lineReports, { + __proto__: null, + line: line.line, + count: line.count, + }); + } + if (line.count > 0 || line.ignore) { + coveredCnt++; + } + } + + ArrayPrototypePush(coverageSummary.files, { + __proto__: null, + path: fileURLToPath(url), + totalLineCount: lines.length, + totalBranchCount: totalBranches, + totalFunctionCount: totalFunctions, + coveredLineCount: coveredCnt, + coveredBranchCount: branchesCovered, + coveredFunctionCount: functionsCovered, + coveredLinePercent: toPercentage(coveredCnt, lines.length), + coveredBranchPercent: toPercentage(branchesCovered, totalBranches), + coveredFunctionPercent: toPercentage(functionsCovered, totalFunctions), + functions: functionReports, + branches: branchReports, + lines: lineReports, + }); + + coverageSummary.totals.totalLineCount += lines.length; + coverageSummary.totals.totalBranchCount += totalBranches; + coverageSummary.totals.totalFunctionCount += totalFunctions; + coverageSummary.totals.coveredLineCount += coveredCnt; + coverageSummary.totals.coveredBranchCount += branchesCovered; + coverageSummary.totals.coveredFunctionCount += functionsCovered; + } + + coverageSummary.totals.coveredLinePercent = toPercentage( + coverageSummary.totals.coveredLineCount, + coverageSummary.totals.totalLineCount, + ); + coverageSummary.totals.coveredBranchPercent = toPercentage( + coverageSummary.totals.coveredBranchCount, + coverageSummary.totals.totalBranchCount, + ); + coverageSummary.totals.coveredFunctionPercent = toPercentage( + coverageSummary.totals.coveredFunctionCount, + coverageSummary.totals.totalFunctionCount, + ); + coverageSummary.files.sort(sortCoverageFiles); + + return coverageSummary; + } + + cleanup() { + // Restore the original value of process.env.NODE_V8_COVERAGE. Then, copy + // all of the created coverage files to the original coverage directory. + internalBinding('profiler').endCoverage(); + + if (this.originalCoverageDirectory === undefined) { + delete process.env.NODE_V8_COVERAGE; + } else { + process.env.NODE_V8_COVERAGE = this.originalCoverageDirectory; + let dir; + + try { + mkdirSync(this.originalCoverageDirectory, { __proto__: null, recursive: true }); + dir = opendirSync(this.coverageDirectory); + + for (let entry; (entry = dir.readSync()) !== null;) { + const src = join(this.coverageDirectory, entry.name); + const dst = join(this.originalCoverageDirectory, entry.name); + copyFileSync(src, dst); + } + } finally { + if (dir) { + dir.closeSync(); + } + } + } + + try { + rmSync(this.coverageDirectory, { __proto__: null, recursive: true }); + } catch { + // Ignore cleanup errors. + } + } + + getCoverageFromDirectory() { + const result = new SafeMap(); + let dir; + + try { + dir = opendirSync(this.coverageDirectory); + + for (let entry; (entry = dir.readSync()) !== null;) { + if (RegExpPrototypeExec(kCoverageFileRegex, entry.name) === null) { + continue; + } + + const coverageFile = join(this.coverageDirectory, entry.name); + const coverage = JSONParse(readFileSync(coverageFile, 'utf8')); + this.mergeCoverage(result, this.mapCoverageWithSourceMap(coverage)); + } + + return ArrayFrom(result.values()); + } finally { + if (dir) { + dir.closeSync(); + } + } + } + + + mapCoverageWithSourceMap(coverage) { + const { result } = coverage; + const sourceMapCache = coverage['source-map-cache']; + if (!this.sourceMaps || !sourceMapCache) { + return result; + } + const newResult = new SafeMap(); + for (let i = 0; i < result.length; ++i) { + const script = result[i]; + const { url, functions } = script; + + if (this.shouldSkipFileCoverage(url) || sourceMapCache[url] == null) { + newResult.set(url, script); + continue; + } + const { data, lineLengths } = sourceMapCache[url]; + if (!data) throw new ERR_SOURCE_MAP_CORRUPT(url); + let offset = 0; + const executedLines = ArrayPrototypeMap(lineLengths, (length, i) => { + const coverageLine = new CoverageLine(i + 1, offset, null, length + 1); + offset += length + 1; + return coverageLine; + }); + if (data.sourcesContent != null) { + for (let j = 0; j < data.sources.length; ++j) { + this.getLines(data.sources[j], data.sourcesContent[j]); + } + } + const sourceMap = new SourceMap(data, { __proto__: null, lineLengths }); + + for (let j = 0; j < functions.length; ++j) { + const { ranges, functionName, isBlockCoverage } = functions[j]; + if (ranges == null) { + continue; + } + let newUrl; + const newRanges = []; + for (let k = 0; k < ranges.length; ++k) { + const { startOffset, endOffset, count } = ranges[k]; + const { lines } = mapRangeToLines(ranges[k], executedLines); + + let startEntry = sourceMap + .findEntry(lines[0].line - 1, MathMax(0, startOffset - lines[0].startOffset)); + const endEntry = sourceMap + .findEntry(lines[lines.length - 1].line - 1, (endOffset - lines[lines.length - 1].startOffset) - 1); + if (!startEntry.originalSource && endEntry.originalSource && + lines[0].line === 1 && startOffset === 0 && lines[0].startOffset === 0) { + // Edge case when the first line is not mappable + const { 2: originalSource, 3: originalLine, 4: originalColumn } = sourceMap[kMappings][0]; + startEntry = { __proto__: null, originalSource, originalLine, originalColumn }; + } + + if (!startEntry.originalSource || startEntry.originalSource !== endEntry.originalSource) { + // The range is not mappable. Skip it. + continue; + } + + newUrl ??= startEntry?.originalSource; + const mappedLines = this.getLines(newUrl); + if (!mappedLines) { + throw new ERR_SOURCE_MAP_MISSING_SOURCE(newUrl, url); + } + const mappedStartOffset = this.entryToOffset(startEntry, mappedLines); + const mappedEndOffset = this.entryToOffset(endEntry, mappedLines) + 1; + for (let l = startEntry.originalLine; l <= endEntry.originalLine; l++) { + mappedLines[l].count = count; + } + + ArrayPrototypePush(newRanges, { + __proto__: null, startOffset: mappedStartOffset, endOffset: mappedEndOffset, count, + }); + } + + if (!newUrl) { + // No mappable ranges. Skip the function. + continue; + } + const newScript = newResult.get(newUrl) ?? { __proto__: null, url: newUrl, functions: [] }; + ArrayPrototypePush(newScript.functions, { __proto__: null, functionName, ranges: newRanges, isBlockCoverage }); + newResult.set(newUrl, newScript); + } + } + + return ArrayFrom(newResult.values()); + } + + entryToOffset(entry, lines) { + const line = MathMax(entry.originalLine, 0); + return MathMin(lines[line].startOffset + entry.originalColumn, lines[line].endOffset); + } + + mergeCoverage(merged, coverage) { + for (let i = 0; i < coverage.length; ++i) { + const newScript = coverage[i]; + const { url } = newScript; + + if (this.shouldSkipFileCoverage(url)) { + continue; + } + + const oldScript = merged.get(url); + + if (oldScript === undefined) { + merged.set(url, newScript); + } else { + mergeCoverageScripts(oldScript, newScript); + } + } + } + + shouldSkipFileCoverage(url) { + // This check filters out core modules, which start with 'node:' in + // coverage reports, as well as any invalid coverages which have been + // observed on Windows. + if (!StringPrototypeStartsWith(url, 'file:')) return true; + + const absolutePath = fileURLToPath(url); + const relativePath = relative(this.workingDirectory, absolutePath); + + // This check filters out files that match the exclude globs. + if (this.excludeGlobs?.length > 0) { + for (let i = 0; i < this.excludeGlobs.length; ++i) { + if (matchesGlob(relativePath, this.excludeGlobs[i]) || + matchesGlob(absolutePath, this.excludeGlobs[i])) return true; + } + } + + // This check filters out files that do not match the include globs. + if (this.includeGlobs?.length > 0) { + for (let i = 0; i < this.includeGlobs.length; ++i) { + if (matchesGlob(relativePath, this.includeGlobs[i]) || + matchesGlob(absolutePath, this.includeGlobs[i])) return false; + } + return true; + } + + // This check filters out the node_modules/ directory, unless it is explicitly included. + return StringPrototypeIncludes(url, '/node_modules/'); + } +} + +function toPercentage(covered, total) { + return total === 0 ? 100 : (covered / total) * 100; +} + +function sortCoverageFiles(a, b) { + return StringPrototypeLocaleCompare(a.path, b.path); +} + +function setupCoverage(options) { + let originalCoverageDirectory = process.env.NODE_V8_COVERAGE; + + // If NODE_V8_COVERAGE was already specified, convert it to an absolute path + // and store it for later. The test runner will use a temporary directory + // so that no preexisting coverage files interfere with the results of the + // coverage report. Then, once the coverage is computed, move the coverage + // files back to the original NODE_V8_COVERAGE directory. + originalCoverageDirectory &&= resolve(options.cwd, originalCoverageDirectory); + + const coverageDirectory = mkdtempSync(join(tmpdir(), 'node-coverage-')); + const enabled = setupCoverageHooks(coverageDirectory); + + if (!enabled) { + return null; + } + + // Ensure that NODE_V8_COVERAGE is set so that coverage can propagate to + // child processes. + process.env.NODE_V8_COVERAGE = coverageDirectory; + + return new TestCoverage( + coverageDirectory, + originalCoverageDirectory, + options.cwd, + options.coverageExcludeGlobs, + options.coverageIncludeGlobs, + options.sourceMaps, + { + __proto__: null, + line: options.lineCoverage, + branch: options.branchCoverage, + function: options.functionCoverage, + }, + ); +} + +function mapRangeToLines(range, lines) { + const { startOffset, endOffset, count } = range; + const mappedLines = []; + let ignoredLines = 0; + let start = 0; + let end = lines.length; + let mid; + + while (start <= end) { + mid = MathFloor((start + end) / 2); + let line = lines[mid]; + + if (startOffset >= line?.startOffset && startOffset <= line?.endOffset) { + while (endOffset > line?.startOffset) { + // If the range is not covered, and the range covers the entire line, + // then mark that line as not covered. + if (startOffset <= line.startOffset && endOffset >= line.endOffset) { + line.count = count; + } + + ArrayPrototypePush(mappedLines, line); + + if (line.ignore) { + ignoredLines++; + } + + mid++; + line = lines[mid]; + } + + break; + } else if (startOffset >= line?.endOffset) { + start = mid + 1; + } else { + end = mid - 1; + } + } + + return { __proto__: null, lines: mappedLines, ignoredLines }; +} + +function mergeCoverageScripts(oldScript, newScript) { + // Merge the functions from the new coverage into the functions from the + // existing (merged) coverage. + for (let i = 0; i < newScript.functions.length; ++i) { + const newFn = newScript.functions[i]; + let found = false; + + for (let j = 0; j < oldScript.functions.length; ++j) { + const oldFn = oldScript.functions[j]; + + if (newFn.functionName === oldFn.functionName && + newFn.ranges?.[0].startOffset === oldFn.ranges?.[0].startOffset && + newFn.ranges?.[0].endOffset === oldFn.ranges?.[0].endOffset) { + // These are the same functions. + found = true; + + // If newFn is block level coverage, then it will: + // - Replace oldFn if oldFn is not block level coverage. + // - Merge with oldFn if it is also block level coverage. + // If newFn is not block level coverage, then it has no new data. + if (newFn.isBlockCoverage) { + if (oldFn.isBlockCoverage) { + // Merge the oldFn ranges with the newFn ranges. + mergeCoverageRanges(oldFn, newFn); + } else { + // Replace oldFn with newFn. + oldFn.isBlockCoverage = true; + oldFn.ranges = newFn.ranges; + } + } + + break; + } + } + + if (!found) { + // This is a new function to track. This is possible because V8 can + // generate a different list of functions depending on which code paths + // are executed. For example, if a code path dynamically creates a + // function, but that code path is not executed then the function does + // not show up in the coverage report. Unfortunately, this also means + // that the function counts in the coverage summary can never be + // guaranteed to be 100% accurate. + ArrayPrototypePush(oldScript.functions, newFn); + } + } +} + +function mergeCoverageRanges(oldFn, newFn) { + const mergedRanges = new SafeSet(); + + // Keep all of the existing covered ranges. + for (let i = 0; i < oldFn.ranges.length; ++i) { + const oldRange = oldFn.ranges[i]; + + if (oldRange.count > 0) { + mergedRanges.add(oldRange); + } + } + + // Merge in the new ranges where appropriate. + for (let i = 0; i < newFn.ranges.length; ++i) { + const newRange = newFn.ranges[i]; + let exactMatch = false; + + for (let j = 0; j < oldFn.ranges.length; ++j) { + const oldRange = oldFn.ranges[j]; + + if (doesRangeEqualOtherRange(newRange, oldRange)) { + // These are the same ranges, so keep the existing one. + oldRange.count += newRange.count; + mergedRanges.add(oldRange); + exactMatch = true; + break; + } + + // Look at ranges representing missing coverage and add ranges that + // represent the intersection. + if (oldRange.count === 0 && newRange.count === 0) { + if (doesRangeContainOtherRange(oldRange, newRange)) { + // The new range is completely within the old range. Discard the + // larger (old) range, and keep the smaller (new) range. + mergedRanges.add(newRange); + } else if (doesRangeContainOtherRange(newRange, oldRange)) { + // The old range is completely within the new range. Discard the + // larger (new) range, and keep the smaller (old) range. + mergedRanges.add(oldRange); + } + } + } + + // Add new ranges that do not represent missing coverage. + if (newRange.count > 0 && !exactMatch) { + mergedRanges.add(newRange); + } + } + + oldFn.ranges = ArrayFrom(mergedRanges); +} + +function doesRangeEqualOtherRange(range, otherRange) { + return range.startOffset === otherRange.startOffset && + range.endOffset === otherRange.endOffset; +} + +function doesRangeContainOtherRange(range, otherRange) { + return range.startOffset <= otherRange.startOffset && + range.endOffset >= otherRange.endOffset; +} + +module.exports = { setupCoverage, TestCoverage }; diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 45dd90e..43aec61 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -1,173 +1,357 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/lib/internal/test_runner/harness.js -'use strict' +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; const { ArrayPrototypeForEach, + ArrayPrototypePush, + FunctionPrototypeBind, + PromiseResolve, + PromiseWithResolvers, SafeMap, - SafeWeakSet -} = require('#internal/per_context/primordials') + SafePromiseAllReturnVoid, +} = primordials; +const { getCallerLocation } = internalBinding('util'); const { createHook, - executionAsyncId -} = require('async_hooks') + executionAsyncId, +} = require('async_hooks'); +const { relative } = require('path'); const { codes: { - ERR_TEST_FAILURE - } -} = require('#internal/errors') -const { kEmptyObject } = require('#internal/util') -const { getOptionValue } = require('#internal/options') -const { kCancelledByParent, Test, ItTest, Suite } = require('#internal/test_runner/test') -const { setupTestReporters } = require('#internal/test_runner/utils') -const { bigint: hrtime } = process.hrtime - -const isTestRunnerCli = getOptionValue('--test') -const testResources = new SafeMap() -const wasRootSetup = new SafeWeakSet() - -function createTestTree (options = kEmptyObject) { - return setup(new Test({ __proto__: null, ...options, name: '' })) + ERR_TEST_FAILURE, + }, +} = require('#internal/errors'); +const { exitCodes: { kGenericUserError } } = internalBinding('errors'); +const { kCancelledByParent, Test, Suite } = require('#internal/test_runner/test'); +const { + parseCommandLine, + reporterScope, + shouldColorizeTestFiles, +} = require('#internal/test_runner/utils'); +const { queueMicrotask } = require('#internal/process/task_queues'); +const { bigint: hrtime } = process.hrtime; +const resolvedPromise = PromiseResolve(); +const testResources = new SafeMap(); +let globalRoot; + +testResources.set(reporterScope.asyncId(), reporterScope); + +function createTestTree(rootTestOptions, globalOptions) { + const buildPhaseDeferred = PromiseWithResolvers(); + const isFilteringByName = globalOptions.testNamePatterns || + globalOptions.testSkipPatterns; + const isFilteringByOnly = (globalOptions.isolation === 'process' || process.env.NODE_TEST_CONTEXT) ? + globalOptions.only : true; + const harness = { + __proto__: null, + buildPromise: buildPhaseDeferred.promise, + buildSuites: [], + isWaitingForBuildPhase: false, + bootstrapPromise: resolvedPromise, + watching: false, + config: globalOptions, + coverage: null, + resetCounters() { + harness.counters = { + __proto__: null, + tests: 0, + failed: 0, + passed: 0, + cancelled: 0, + skipped: 0, + todo: 0, + topLevel: 0, + suites: 0, + }; + }, + success: true, + counters: null, + shouldColorizeTestFiles: shouldColorizeTestFiles(globalOptions.destinations), + teardown: null, + snapshotManager: null, + isFilteringByName, + isFilteringByOnly, + async waitForBuildPhase() { + if (harness.buildSuites.length > 0) { + await SafePromiseAllReturnVoid(harness.buildSuites); + } + + buildPhaseDeferred.resolve(); + }, + }; + + harness.resetCounters(); + globalRoot = new Test({ + __proto__: null, + ...rootTestOptions, + harness, + name: '', + }); + setupProcessState(globalRoot, globalOptions, harness); + globalRoot.startTime = hrtime(); + return globalRoot; } -function createProcessEventHandler (eventName, rootTest) { +function createProcessEventHandler(eventName, rootTest) { return (err) => { - // Check if this error is coming from a test. If it is, fail the test. - const test = testResources.get(executionAsyncId()) + if (rootTest.harness.bootstrapPromise) { + // Something went wrong during the asynchronous portion of bootstrapping + // the test runner. Since the test runner is not setup properly, we can't + // do anything but throw the error. + throw err; + } - if (!test) { - // Node.js 14.x crashes if the error is throw here. - if (process.version.startsWith('v14.')) return - throw err + const test = testResources.get(executionAsyncId()); + + // Check if this error is coming from a reporter. If it is, throw it. + if (test === reporterScope) { + throw err; } - if (test.finished) { - // If the test is already finished, report this as a top level - // diagnostic since this is a malformed test. - const msg = `Warning: Test "${test.name}" generated asynchronous ` + - 'activity after the test ended. This activity created the error ' + - `"${err}" and would have caused the test to fail, but instead ` + - `triggered an ${eventName} event.` - - rootTest.diagnostic(msg) - process.exitCode = 1 - return + // Check if this error is coming from a test or test hook. If it is, fail the test. + if (!test || test.finished || test.hookType) { + // If the test is already finished or the resource that created the error + // is not mapped to a Test, report this as a top level diagnostic. + let msg; + + if (test) { + const name = test.hookType ? `Test hook "${test.hookType}"` : `Test "${test.name}"`; + let locInfo = ''; + if (test.loc) { + const relPath = relative(rootTest.config.cwd, test.loc.file); + locInfo = ` at ${relPath}:${test.loc.line}:${test.loc.column}`; + } + + msg = `Error: ${name}${locInfo} generated asynchronous ` + + 'activity after the test ended. This activity created the error ' + + `"${err}" and would have caused the test to fail, but instead ` + + `triggered an ${eventName} event.`; + } else { + msg = 'Error: A resource generated asynchronous activity after ' + + `the test ended. This activity created the error "${err}" which ` + + `triggered an ${eventName} event, caught by the test runner.`; + } + + rootTest.diagnostic(msg); + rootTest.harness.success = false; + process.exitCode = kGenericUserError; + return; } - test.fail(new ERR_TEST_FAILURE(err, eventName)) - test.postRun() + test.fail(new ERR_TEST_FAILURE(err, eventName)); + test.abortController.abort(); + }; +} + +function configureCoverage(rootTest, globalOptions) { + if (!globalOptions.coverage) { + return null; + } + + const { setupCoverage } = require('#internal/test_runner/coverage'); + + try { + return setupCoverage(globalOptions); + } catch (err) { + const msg = `Warning: Code coverage could not be enabled. ${err}`; + + rootTest.diagnostic(msg); + rootTest.harness.success = false; + process.exitCode = kGenericUserError; } } -function setup (root) { - if (wasRootSetup.has(root)) { - return root +function collectCoverage(rootTest, coverage) { + if (!coverage) { + return null; + } + + let summary = null; + + try { + summary = coverage.summary(); + } catch (err) { + rootTest.diagnostic(`Warning: Could not report code coverage. ${err}`); + rootTest.harness.success = false; + process.exitCode = kGenericUserError; + } + + try { + coverage.cleanup(); + } catch (err) { + rootTest.diagnostic(`Warning: Could not clean up code coverage. ${err}`); + rootTest.harness.success = false; + process.exitCode = kGenericUserError; } + + return summary; +} + +function setupProcessState(root, globalOptions) { const hook = createHook({ - init (asyncId, type, triggerAsyncId, resource) { + __proto__: null, + init(asyncId, type, triggerAsyncId, resource) { if (resource instanceof Test) { - testResources.set(asyncId, resource) - return + testResources.set(asyncId, resource); + return; } - const parent = testResources.get(triggerAsyncId) + const parent = testResources.get(triggerAsyncId); if (parent !== undefined) { - testResources.set(asyncId, parent) + testResources.set(asyncId, parent); } }, - destroy (asyncId) { - testResources.delete(asyncId) - } - }) + destroy(asyncId) { + testResources.delete(asyncId); + }, + }); - hook.enable() + hook.enable(); const exceptionHandler = - createProcessEventHandler('uncaughtException', root) + createProcessEventHandler('uncaughtException', root); const rejectionHandler = - createProcessEventHandler('unhandledRejection', root) - - const exitHandler = () => { + createProcessEventHandler('unhandledRejection', root); + const coverage = configureCoverage(root, globalOptions); + const exitHandler = async () => { + if (root.subtests.length === 0 && (root.hooks.before.length > 0 || root.hooks.after.length > 0)) { + // Run global before/after hooks in case there are no tests + await root.run(); + } root.postRun(new ERR_TEST_FAILURE( 'Promise resolution is still pending but the event loop has already resolved', - kCancelledByParent)) + kCancelledByParent)); - hook.disable() - process.removeListener('unhandledRejection', rejectionHandler) - process.removeListener('uncaughtException', exceptionHandler) - } + hook.disable(); + process.removeListener('uncaughtException', exceptionHandler); + process.removeListener('unhandledRejection', rejectionHandler); + process.removeListener('beforeExit', exitHandler); + if (globalOptions.isTestRunner) { + process.removeListener('SIGINT', terminationHandler); + process.removeListener('SIGTERM', terminationHandler); + } + }; const terminationHandler = () => { - exitHandler() - process.exit() - } + exitHandler(); + process.exit(); + }; - process.on('uncaughtException', exceptionHandler) - process.on('unhandledRejection', rejectionHandler) - process.on('beforeExit', exitHandler) - // TODO(MoLow): Make it configurable to hook when isTestRunnerCli === false. - if (isTestRunnerCli) { - process.on('SIGINT', terminationHandler) - process.on('SIGTERM', terminationHandler) + process.on('uncaughtException', exceptionHandler); + process.on('unhandledRejection', rejectionHandler); + process.on('beforeExit', exitHandler); + // TODO(MoLow): Make it configurable to hook when isTestRunner === false. + if (globalOptions.isTestRunner) { + process.on('SIGINT', terminationHandler); + process.on('SIGTERM', terminationHandler); } - root.startTime = hrtime() - - wasRootSetup.add(root) - return root + root.harness.coverage = FunctionPrototypeBind(collectCoverage, null, root, coverage); + root.harness.teardown = exitHandler; } -let globalRoot -function getGlobalRoot () { +function lazyBootstrapRoot() { if (!globalRoot) { - globalRoot = createTestTree() - globalRoot.reporter.once('test:fail', () => { - process.exitCode = 1 - }) - setupTestReporters(globalRoot.reporter) + // This is where the test runner is bootstrapped when node:test is used + // without the --test flag or the run() API. + const entryFile = process.argv?.[1]; + const rootTestOptions = { + __proto__: null, + entryFile, + loc: entryFile ? [1, 1, entryFile] : undefined, + }; + const globalOptions = parseCommandLine(); + globalOptions.cwd = process.cwd(); + createTestTree(rootTestOptions, globalOptions); + globalRoot.reporter.on('test:summary', (data) => { + if (!data.success) { + process.exitCode = kGenericUserError; + } + }); + globalRoot.harness.bootstrapPromise = globalOptions.setup(globalRoot.reporter); } - return globalRoot + return globalRoot; } -function test (name, options, fn) { - const subtest = getGlobalRoot().createSubtest(Test, name, options, fn) - return subtest.start() -} +async function startSubtestAfterBootstrap(subtest) { + if (subtest.root.harness.buildPromise) { + if (subtest.root.harness.bootstrapPromise) { + await subtest.root.harness.bootstrapPromise; + subtest.root.harness.bootstrapPromise = null; + } -function runInParentContext (Factory) { - function run (name, options, fn, overrides) { - const parent = testResources.get(executionAsyncId()) || getGlobalRoot() - const subtest = parent.createSubtest(Factory, name, options, fn, overrides) - if (parent === getGlobalRoot()) { - subtest.start() + if (subtest.buildSuite) { + ArrayPrototypePush(subtest.root.harness.buildSuites, subtest.buildSuite); + } + + if (!subtest.root.harness.isWaitingForBuildPhase) { + subtest.root.harness.isWaitingForBuildPhase = true; + queueMicrotask(() => { + subtest.root.harness.waitForBuildPhase(); + }); } - } - const cb = (name, options, fn) => { - run(name, options, fn) + await subtest.root.harness.buildPromise; + subtest.root.harness.buildPromise = null; } - ArrayPrototypeForEach(['skip', 'todo'], (keyword) => { - cb[keyword] = (name, options, fn) => { - run(name, options, fn, { [keyword]: true }) + await subtest.start(); +} + +function runInParentContext(Factory) { + function run(name, options, fn, overrides) { + const parent = testResources.get(executionAsyncId()) || lazyBootstrapRoot(); + const subtest = parent.createSubtest(Factory, name, options, fn, overrides); + if (parent instanceof Suite) { + return PromiseResolve(); } - }) - return cb + + return startSubtestAfterBootstrap(subtest); + } + + const test = (name, options, fn) => { + const overrides = { + __proto__: null, + loc: getCallerLocation(), + }; + + return run(name, options, fn, overrides); + }; + ArrayPrototypeForEach(['skip', 'todo', 'only'], (keyword) => { + test[keyword] = (name, options, fn) => { + const overrides = { + __proto__: null, + [keyword]: true, + loc: getCallerLocation(), + }; + + return run(name, options, fn, overrides); + }; + }); + return test; } -function hook (hook) { +function hook(hook) { return (fn, options) => { - const parent = testResources.get(executionAsyncId()) || getGlobalRoot() - parent.createHook(hook, fn, options) - } + const parent = testResources.get(executionAsyncId()) || lazyBootstrapRoot(); + parent.createHook(hook, fn, { + __proto__: null, + ...options, + parent, + hookType: hook, + loc: getCallerLocation(), + }); + }; } module.exports = { createTestTree, - test, - describe: runInParentContext(Suite), - it: runInParentContext(ItTest), + test: runInParentContext(Test), + suite: runInParentContext(Suite), before: hook('before'), after: hook('after'), beforeEach: hook('beforeEach'), - afterEach: hook('afterEach') -} + afterEach: hook('afterEach'), + startSubtestAfterBootstrap, +}; diff --git a/lib/internal/test_runner/mock.js b/lib/internal/test_runner/mock.js deleted file mode 100644 index af46e47..0000000 --- a/lib/internal/test_runner/mock.js +++ /dev/null @@ -1,371 +0,0 @@ -// https://github.com/nodejs/node/blob/929aada39d0f418193ca03cc360ced8c5b4ce553/lib/internal/test_runner/mock.js -'use strict' -const { - ArrayPrototypePush, - ArrayPrototypeSlice, - Error, - FunctionPrototypeCall, - ObjectDefineProperty, - ObjectGetOwnPropertyDescriptor, - ObjectGetPrototypeOf, - Proxy, - ReflectApply, - ReflectConstruct, - ReflectGet, - SafeMap -} = require('#internal/per_context/primordials') -const { - codes: { - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE - } -} = require('#internal/errors') -const { kEmptyObject } = require('#internal/util') -const { - validateBoolean, - validateFunction, - validateInteger, - validateObject -} = require('#internal/validators') - -function kDefaultFunction () {} - -class MockFunctionContext { - #calls - #mocks - #implementation - #restore - #times - - constructor (implementation, restore, times) { - this.#calls = [] - this.#mocks = new SafeMap() - this.#implementation = implementation - this.#restore = restore - this.#times = times - } - - get calls () { - return ArrayPrototypeSlice(this.#calls, 0) - } - - callCount () { - return this.#calls.length - } - - mockImplementation (implementation) { - validateFunction(implementation, 'implementation') - this.#implementation = implementation - } - - mockImplementationOnce (implementation, onCall) { - validateFunction(implementation, 'implementation') - const nextCall = this.#calls.length - const call = onCall ?? nextCall - validateInteger(call, 'onCall', nextCall) - this.#mocks.set(call, implementation) - } - - restore () { - const { descriptor, object, original, methodName } = this.#restore - - if (typeof methodName === 'string') { - // This is an object method spy. - ObjectDefineProperty(object, methodName, descriptor) - } else { - // This is a bare function spy. There isn't much to do here but make - // the mock call the original function. - this.#implementation = original - } - } - - trackCall (call) { - ArrayPrototypePush(this.#calls, call) - } - - nextImpl () { - const nextCall = this.#calls.length - const mock = this.#mocks.get(nextCall) - const impl = mock ?? this.#implementation - - if (nextCall + 1 === this.#times) { - this.restore() - } - - this.#mocks.delete(nextCall) - return impl - } -} - -const { nextImpl, restore, trackCall } = MockFunctionContext.prototype -delete MockFunctionContext.prototype.trackCall -delete MockFunctionContext.prototype.nextImpl - -class MockTracker { - #mocks = [] - - fn ( - original = function () {}, - implementation = original, - options = kEmptyObject - ) { - if (original !== null && typeof original === 'object') { - options = original - original = function () {} - implementation = original - } else if (implementation !== null && typeof implementation === 'object') { - options = implementation - implementation = original - } - - validateFunction(original, 'original') - validateFunction(implementation, 'implementation') - validateObject(options, 'options') - const { times = Infinity } = options - validateTimes(times, 'options.times') - const ctx = new MockFunctionContext(implementation, { original }, times) - return this.#setupMock(ctx, original) - } - - method ( - objectOrFunction, - methodName, - implementation = kDefaultFunction, - options = kEmptyObject - ) { - validateStringOrSymbol(methodName, 'methodName') - if (typeof objectOrFunction !== 'function') { - validateObject(objectOrFunction, 'object') - } - - if (implementation !== null && typeof implementation === 'object') { - options = implementation - implementation = kDefaultFunction - } - - validateFunction(implementation, 'implementation') - validateObject(options, 'options') - - const { - getter = false, - setter = false, - times = Infinity - } = options - - validateBoolean(getter, 'options.getter') - validateBoolean(setter, 'options.setter') - validateTimes(times, 'options.times') - - if (setter && getter) { - throw new ERR_INVALID_ARG_VALUE( - 'options.setter', setter, "cannot be used with 'options.getter'" - ) - } - const descriptor = findMethodOnPrototypeChain(objectOrFunction, methodName) - - let original - - if (getter) { - original = descriptor?.get - } else if (setter) { - original = descriptor?.set - } else { - original = descriptor?.value - } - - if (typeof original !== 'function') { - throw new ERR_INVALID_ARG_VALUE( - 'methodName', original, 'must be a method' - ) - } - - const restore = { descriptor, object: objectOrFunction, methodName } - const impl = implementation === kDefaultFunction - ? original - : implementation - const ctx = new MockFunctionContext(impl, restore, times) - const mock = this.#setupMock(ctx, original) - const mockDescriptor = { - __proto__: null, - configurable: descriptor.configurable, - enumerable: descriptor.enumerable - } - - if (getter) { - mockDescriptor.get = mock - mockDescriptor.set = descriptor.set - } else if (setter) { - mockDescriptor.get = descriptor.get - mockDescriptor.set = mock - } else { - mockDescriptor.writable = descriptor.writable - mockDescriptor.value = mock - } - - ObjectDefineProperty(objectOrFunction, methodName, mockDescriptor) - - return mock - } - - getter ( - object, - methodName, - implementation = kDefaultFunction, - options = kEmptyObject - ) { - if (implementation !== null && typeof implementation === 'object') { - options = implementation - implementation = kDefaultFunction - } else { - validateObject(options, 'options') - } - - const { getter = true } = options - - if (getter === false) { - throw new ERR_INVALID_ARG_VALUE( - 'options.getter', getter, 'cannot be false' - ) - } - - return this.method(object, methodName, implementation, { - ...options, - getter - }) - } - - setter ( - object, - methodName, - implementation = kDefaultFunction, - options = kEmptyObject - ) { - if (implementation !== null && typeof implementation === 'object') { - options = implementation - implementation = kDefaultFunction - } else { - validateObject(options, 'options') - } - - const { setter = true } = options - - if (setter === false) { - throw new ERR_INVALID_ARG_VALUE( - 'options.setter', setter, 'cannot be false' - ) - } - - return this.method(object, methodName, implementation, { - ...options, - setter - }) - } - - reset () { - this.restoreAll() - this.#mocks = [] - } - - restoreAll () { - for (let i = 0; i < this.#mocks.length; i++) { - FunctionPrototypeCall(restore, this.#mocks[i]) - } - } - - #setupMock (ctx, fnToMatch) { - const mock = new Proxy(fnToMatch, { - __proto__: null, - apply (_fn, thisArg, argList) { - const fn = FunctionPrototypeCall(nextImpl, ctx) - let result - let error - - try { - result = ReflectApply(fn, thisArg, argList) - } catch (err) { - error = err - throw err - } finally { - FunctionPrototypeCall(trackCall, ctx, { - arguments: argList, - error, - result, - // eslint-disable-next-line no-restricted-syntax - stack: new Error(), - target: undefined, - this: thisArg - }) - } - - return result - }, - construct (target, argList, newTarget) { - const realTarget = FunctionPrototypeCall(nextImpl, ctx) - let result - let error - - try { - result = ReflectConstruct(realTarget, argList, newTarget) - } catch (err) { - error = err - throw err - } finally { - FunctionPrototypeCall(trackCall, ctx, { - arguments: argList, - error, - result, - // eslint-disable-next-line no-restricted-syntax - stack: new Error(), - target, - this: result - }) - } - - return result - }, - get (target, property, receiver) { - if (property === 'mock') { - return ctx - } - - return ReflectGet(target, property, receiver) - } - }) - - ArrayPrototypePush(this.#mocks, ctx) - return mock - } -} - -function validateStringOrSymbol (value, name) { - if (typeof value !== 'string' && typeof value !== 'symbol') { - throw new ERR_INVALID_ARG_TYPE(name, ['string', 'symbol'], value) - } -} - -function validateTimes (value, name) { - if (value === Infinity) { - return - } - - validateInteger(value, name, 1) -} - -function findMethodOnPrototypeChain (instance, methodName) { - let host = instance - let descriptor - - while (host !== null) { - descriptor = ObjectGetOwnPropertyDescriptor(host, methodName) - - if (descriptor) { - break - } - - host = ObjectGetPrototypeOf(host) - } - - return descriptor -} - -module.exports = { MockTracker } diff --git a/lib/internal/test_runner/mock/loader.js b/lib/internal/test_runner/mock/loader.js new file mode 100644 index 0000000..c09eb6c --- /dev/null +++ b/lib/internal/test_runner/mock/loader.js @@ -0,0 +1,203 @@ +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; +const { + JSONStringify, + SafeMap, + globalThis: { + Atomics: { + notify: AtomicsNotify, + store: AtomicsStore, + }, + }, +} = primordials; +const { + kBadExportsMessage, + kMockSearchParam, + kMockSuccess, + kMockExists, + kMockUnknownMessage, +} = require('#internal/test_runner/mock/mock'); +const { URL, URLParse } = require('#internal/url'); +let debug = require('#internal/util/debuglog').debuglog('test_runner', (fn) => { + debug = fn; +}); + +// TODO(cjihrig): The mocks need to be thread aware because the exports are +// evaluated on the thread that creates the mock. Before marking this API as +// stable, one of the following issues needs to be implemented: +// https://github.com/nodejs/node/issues/49472 +// or https://github.com/nodejs/node/issues/52219 + +const mocks = new SafeMap(); + +async function initialize(data) { + data?.port.on('message', ({ type, payload }) => { + debug('mock loader received message type "%s" with payload %o', type, payload); + + if (type === 'node:test:register') { + const { baseURL } = payload; + const mock = mocks.get(baseURL); + + if (mock?.active) { + debug('already mocking "%s"', baseURL); + sendAck(payload.ack, kMockExists); + return; + } + + const localVersion = mock?.localVersion ?? 0; + + debug('new mock version %d for "%s"', localVersion, baseURL); + mocks.set(baseURL, { + __proto__: null, + active: true, + cache: payload.cache, + exportNames: payload.exportNames, + format: payload.format, + hasDefaultExport: payload.hasDefaultExport, + localVersion, + url: baseURL, + }); + sendAck(payload.ack); + } else if (type === 'node:test:unregister') { + const mock = mocks.get(payload.baseURL); + + if (mock !== undefined) { + mock.active = false; + mock.localVersion++; + } + + sendAck(payload.ack); + } else { + sendAck(payload.ack, kMockUnknownMessage); + } + }); +} + +async function resolve(specifier, context, nextResolve) { + debug('resolve hook entry, specifier = "%s", context = %o', specifier, context); + + const nextResolveResult = await nextResolve(specifier, context); + const mockSpecifier = nextResolveResult.url; + + const mock = mocks.get(mockSpecifier); + debug('resolve hook, specifier = "%s", mock = %o', specifier, mock); + + if (mock?.active !== true) { + return nextResolveResult; + } + + const url = new URL(mockSpecifier); + url.searchParams.set(kMockSearchParam, mock.localVersion); + + if (!mock.cache) { + // With ESM, we can't remove modules from the cache. Bump the module's + // version instead so that the next import will be uncached. + mock.localVersion++; + } + + const { href } = url; + debug('resolve hook finished, url = "%s"', href); + return { __proto__: null, url: href, format: nextResolveResult.format }; +} + +async function load(url, context, nextLoad) { + debug('load hook entry, url = "%s", context = %o', url, context); + const parsedURL = URLParse(url); + if (parsedURL) { + parsedURL.searchParams.delete(kMockSearchParam); + } + + const baseURL = parsedURL ? parsedURL.href : url; + const mock = mocks.get(baseURL); + + const original = await nextLoad(url, context); + debug('load hook, mock = %o', mock); + if (mock?.active !== true) { + return original; + } + + // Treat builtins as commonjs because customization hooks do not allow a + // core module to be replaced. + // Also collapse 'commonjs-sync' and 'require-commonjs' to 'commonjs'. + const format = ( + original.format === 'builtin' || + original.format === 'commonjs-sync' || + original.format === 'require-commonjs') ? 'commonjs' : original.format; + + const result = { + __proto__: null, + format, + shortCircuit: true, + source: await createSourceFromMock(mock, format), + }; + + debug('load hook finished, result = %o', result); + return result; +} + +async function createSourceFromMock(mock, format) { + // Create mock implementation from provided exports. + const { exportNames, hasDefaultExport, url } = mock; + const useESM = format === 'module' || format === 'module-typescript'; + const source = `${testImportSource(useESM)} +if (!$__test.mock._mockExports.has(${JSONStringify(url)})) { + throw new Error(${JSONStringify(`mock exports not found for "${url}"`)}); +} + +const $__exports = $__test.mock._mockExports.get(${JSONStringify(url)}); +${defaultExportSource(useESM, hasDefaultExport)} +${namedExportsSource(useESM, exportNames)} +`; + + return source; +} + +function testImportSource(useESM) { + if (useESM) { + return "import $__test from 'node:test';"; + } + + return "const $__test = require('node:test');"; +} + +function defaultExportSource(useESM, hasDefaultExport) { + if (!hasDefaultExport) { + return ''; + } else if (useESM) { + return 'export default $__exports.defaultExport;'; + } + + return 'module.exports = $__exports.defaultExport;'; +} + +function namedExportsSource(useESM, exportNames) { + let source = ''; + + if (!useESM && exportNames.length > 0) { + source += ` +if (module.exports === null || typeof module.exports !== 'object') { + throw new Error('${JSONStringify(kBadExportsMessage)}'); +} +`; + } + + for (let i = 0; i < exportNames.length; ++i) { + const name = exportNames[i]; + + if (useESM) { + source += `export let ${name} = $__exports.namedExports[${JSONStringify(name)}];\n`; + } else { + source += `module.exports[${JSONStringify(name)}] = $__exports.namedExports[${JSONStringify(name)}];\n`; + } + } + + return source; +} + +function sendAck(buf, status = kMockSuccess) { + AtomicsStore(buf, 0, status); + AtomicsNotify(buf, 0); +} + +module.exports = { initialize, load, resolve }; diff --git a/lib/internal/test_runner/mock/mock.js b/lib/internal/test_runner/mock/mock.js new file mode 100644 index 0000000..7a80599 --- /dev/null +++ b/lib/internal/test_runner/mock/mock.js @@ -0,0 +1,806 @@ +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; +const { + ArrayPrototypePush, + ArrayPrototypeSlice, + Error, + FunctionPrototypeBind, + FunctionPrototypeCall, + Int32Array, + ObjectDefineProperty, + ObjectGetOwnPropertyDescriptor, + ObjectGetPrototypeOf, + ObjectKeys, + Proxy, + ReflectApply, + ReflectConstruct, + ReflectGet, + SafeMap, + StringPrototypeSlice, + StringPrototypeStartsWith, + globalThis: { + Atomics: { + store: AtomicsStore, + wait: AtomicsWait, + }, + SharedArrayBuffer, + }, +} = primordials; +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_STATE, + }, +} = require('#internal/errors'); +const esmLoader = require('#internal/modules/esm/loader'); +const { getOptionValue } = require('#internal/options'); +const { + fileURLToPath, + isURL, + pathToFileURL, + toPathIfFileURL, + URL, +} = require('#internal/url'); +const { + emitExperimentalWarning, + getStructuredStack, + kEmptyObject, +} = require('#internal/util'); +let debug = require('#internal/util/debuglog').debuglog('test_runner', (fn) => { + debug = fn; +}); +const { + validateBoolean, + validateFunction, + validateInteger, + validateObject, + validateOneOf, +} = require('#internal/validators'); +const { MockTimers } = require('#internal/test_runner/mock/mock_timers'); +const { strictEqual, notStrictEqual } = require('assert'); +const { Module } = require('#internal/modules/cjs/loader'); +const { MessageChannel } = require('worker_threads'); +const { _load, _nodeModulePaths, _resolveFilename, isBuiltin } = Module; +function kDefaultFunction() {} +const enableModuleMocking = getOptionValue('--experimental-test-module-mocks'); +const kMockSearchParam = 'node-test-mock'; +const kMockSuccess = 1; +const kMockExists = 2; +const kMockUnknownMessage = 3; +const kWaitTimeout = 5_000; +const kBadExportsMessage = 'Cannot create mock because named exports ' + + 'cannot be applied to the provided default export.'; +const kSupportedFormats = ['builtin', 'commonjs', 'module', 'module-typescript', 'commonjs-typescript']; +let sharedModuleState; + +class MockFunctionContext { + #calls; + #mocks; + #implementation; + #restore; + #times; + + constructor(implementation, restore, times) { + this.#calls = []; + this.#mocks = new SafeMap(); + this.#implementation = implementation; + this.#restore = restore; + this.#times = times; + } + + /** + * Gets an array of recorded calls made to the mock function. + * @returns {Array} An array of recorded calls. + */ + get calls() { + return ArrayPrototypeSlice(this.#calls, 0); + } + + /** + * Retrieves the number of times the mock function has been called. + * @returns {number} The call count. + */ + callCount() { + return this.#calls.length; + } + + /** + * Sets a new implementation for the mock function. + * @param {Function} implementation - The new implementation for the mock function. + */ + mockImplementation(implementation) { + validateFunction(implementation, 'implementation'); + this.#implementation = implementation; + } + + /** + * Replaces the implementation of the function only once. + * @param {Function} implementation - The substitute function. + * @param {number} [onCall] - The call index to be replaced. + */ + mockImplementationOnce(implementation, onCall) { + validateFunction(implementation, 'implementation'); + const nextCall = this.#calls.length; + const call = onCall ?? nextCall; + validateInteger(call, 'onCall', nextCall); + this.#mocks.set(call, implementation); + } + + /** + * Restores the original function that was mocked. + */ + restore() { + const { descriptor, object, original, methodName } = this.#restore; + + if (typeof methodName === 'string') { + // This is an object method spy. + ObjectDefineProperty(object, methodName, descriptor); + } else { + // This is a bare function spy. There isn't much to do here but make + // the mock call the original function. + this.#implementation = original; + } + } + + /** + * Resets the recorded calls to the mock function + */ + resetCalls() { + this.#calls = []; + } + + /** + * Tracks a call made to the mock function. + * @param {object} call - The call details. + */ + trackCall(call) { + ArrayPrototypePush(this.#calls, call); + } + + /** + * Gets the next implementation to use for the mock function. + * @returns {Function} The next implementation. + */ + nextImpl() { + const nextCall = this.#calls.length; + const mock = this.#mocks.get(nextCall); + const impl = mock ?? this.#implementation; + + if (nextCall + 1 === this.#times) { + this.restore(); + } + + this.#mocks.delete(nextCall); + return impl; + } +} + +const { + nextImpl, + restore: restoreFn, + trackCall, +} = MockFunctionContext.prototype; +delete MockFunctionContext.prototype.trackCall; +delete MockFunctionContext.prototype.nextImpl; + +class MockModuleContext { + #restore; + #sharedState; + + constructor({ + baseURL, + cache, + caller, + defaultExport, + format, + fullPath, + hasDefaultExport, + namedExports, + sharedState, + }) { + const ack = new Int32Array(new SharedArrayBuffer(4)); + const config = { + __proto__: null, + cache, + defaultExport, + hasDefaultExport, + namedExports, + caller: toPathIfFileURL(caller), + }; + + sharedState.mockMap.set(baseURL, config); + sharedState.mockMap.set(fullPath, config); + + this.#sharedState = sharedState; + this.#restore = { + __proto__: null, + ack, + baseURL, + cached: fullPath in Module._cache, + format, + fullPath, + value: Module._cache[fullPath], + }; + + sharedState.loaderPort.postMessage({ + __proto__: null, + type: 'node:test:register', + payload: { + __proto__: null, + ack, + baseURL, + cache, + exportNames: ObjectKeys(namedExports), + hasDefaultExport, + format, + }, + }); + waitForAck(ack); + delete Module._cache[fullPath]; + sharedState.mockExports.set(baseURL, { + __proto__: null, + defaultExport, + namedExports, + }); + } + + restore() { + if (this.#restore === undefined) { + return; + } + + // Delete the mock CJS cache entry. If the module was previously in the + // cache then restore the old value. + delete Module._cache[this.#restore.fullPath]; + + if (this.#restore.cached) { + Module._cache[this.#restore.fullPath] = this.#restore.value; + } + + AtomicsStore(this.#restore.ack, 0, 0); + this.#sharedState.loaderPort.postMessage({ + __proto__: null, + type: 'node:test:unregister', + payload: { + __proto__: null, + ack: this.#restore.ack, + baseURL: this.#restore.baseURL, + }, + }); + waitForAck(this.#restore.ack); + + this.#sharedState.mockMap.delete(this.#restore.baseURL); + this.#sharedState.mockMap.delete(this.#restore.fullPath); + this.#restore = undefined; + } +} + +const { restore: restoreModule } = MockModuleContext.prototype; + +class MockTracker { + #mocks = []; + #timers; + + /** + * Returns the mock timers of this MockTracker instance. + * @returns {MockTimers} The mock timers instance. + */ + get timers() { + this.#timers ??= new MockTimers(); + return this.#timers; + } + + /** + * Creates a mock function tracker. + * @param {Function} [original] - The original function to be tracked. + * @param {Function} [implementation] - An optional replacement function for the original one. + * @param {object} [options] - Additional tracking options. + * @param {number} [options.times=Infinity] - The maximum number of times the mock function can be called. + * @returns {ProxyConstructor} The mock function tracker. + */ + fn( + original = function() {}, + implementation = original, + options = kEmptyObject, + ) { + if (original !== null && typeof original === 'object') { + options = original; + original = function() {}; + implementation = original; + } else if (implementation !== null && typeof implementation === 'object') { + options = implementation; + implementation = original; + } + + validateFunction(original, 'original'); + validateFunction(implementation, 'implementation'); + validateObject(options, 'options'); + const { times = Infinity } = options; + validateTimes(times, 'options.times'); + const ctx = new MockFunctionContext(implementation, { __proto__: null, original }, times); + return this.#setupMock(ctx, original); + } + + /** + * Creates a method tracker for a specified object or function. + * @param {(object | Function)} objectOrFunction - The object or function containing the method to be tracked. + * @param {string} methodName - The name of the method to be tracked. + * @param {Function} [implementation] - An optional replacement function for the original method. + * @param {object} [options] - Additional tracking options. + * @param {boolean} [options.getter=false] - Indicates whether this is a getter method. + * @param {boolean} [options.setter=false] - Indicates whether this is a setter method. + * @param {number} [options.times=Infinity] - The maximum number of times the mock method can be called. + * @returns {ProxyConstructor} The mock method tracker. + */ + method( + objectOrFunction, + methodName, + implementation = kDefaultFunction, + options = kEmptyObject, + ) { + validateStringOrSymbol(methodName, 'methodName'); + if (typeof objectOrFunction !== 'function') { + validateObject(objectOrFunction, 'object'); + } + + if (implementation !== null && typeof implementation === 'object') { + options = implementation; + implementation = kDefaultFunction; + } + + validateFunction(implementation, 'implementation'); + validateObject(options, 'options'); + + const { + getter = false, + setter = false, + times = Infinity, + } = options; + + validateBoolean(getter, 'options.getter'); + validateBoolean(setter, 'options.setter'); + validateTimes(times, 'options.times'); + + if (setter && getter) { + throw new ERR_INVALID_ARG_VALUE( + 'options.setter', setter, "cannot be used with 'options.getter'", + ); + } + const descriptor = findMethodOnPrototypeChain(objectOrFunction, methodName); + + let original; + + if (getter) { + original = descriptor?.get; + } else if (setter) { + original = descriptor?.set; + } else { + original = descriptor?.value; + } + + if (typeof original !== 'function') { + throw new ERR_INVALID_ARG_VALUE( + 'methodName', original, 'must be a method', + ); + } + + const restore = { __proto__: null, descriptor, object: objectOrFunction, methodName }; + const impl = implementation === kDefaultFunction ? + original : implementation; + const ctx = new MockFunctionContext(impl, restore, times); + const mock = this.#setupMock(ctx, original); + const mockDescriptor = { + __proto__: null, + configurable: descriptor.configurable, + enumerable: descriptor.enumerable, + }; + + if (getter) { + mockDescriptor.get = mock; + mockDescriptor.set = descriptor.set; + } else if (setter) { + mockDescriptor.get = descriptor.get; + mockDescriptor.set = mock; + } else { + mockDescriptor.writable = descriptor.writable; + mockDescriptor.value = mock; + } + + ObjectDefineProperty(objectOrFunction, methodName, mockDescriptor); + + return mock; + } + + /** + * Mocks a getter method of an object. + * This is a syntax sugar for the MockTracker.method with options.getter set to true + * @param {object} object - The target object. + * @param {string} methodName - The name of the getter method to be mocked. + * @param {Function} [implementation] - An optional replacement function for the targeted method. + * @param {object} [options] - Additional tracking options. + * @param {boolean} [options.getter=true] - Indicates whether this is a getter method. + * @param {boolean} [options.setter=false] - Indicates whether this is a setter method. + * @param {number} [options.times=Infinity] - The maximum number of times the mock method can be called. + * @returns {ProxyConstructor} The mock method tracker. + */ + getter( + object, + methodName, + implementation = kDefaultFunction, + options = kEmptyObject, + ) { + if (implementation !== null && typeof implementation === 'object') { + options = implementation; + implementation = kDefaultFunction; + } else { + validateObject(options, 'options'); + } + + const { getter = true } = options; + + if (getter === false) { + throw new ERR_INVALID_ARG_VALUE( + 'options.getter', getter, 'cannot be false', + ); + } + + return this.method(object, methodName, implementation, { + __proto__: null, + ...options, + getter, + }); + } + + /** + * Mocks a setter method of an object. + * This function is a syntax sugar for MockTracker.method with options.setter set to true. + * @param {object} object - The target object. + * @param {string} methodName - The setter method to be mocked. + * @param {Function} [implementation] - An optional replacement function for the targeted method. + * @param {object} [options] - Additional tracking options. + * @param {boolean} [options.getter=false] - Indicates whether this is a getter method. + * @param {boolean} [options.setter=true] - Indicates whether this is a setter method. + * @param {number} [options.times=Infinity] - The maximum number of times the mock method can be called. + * @returns {ProxyConstructor} The mock method tracker. + */ + setter( + object, + methodName, + implementation = kDefaultFunction, + options = kEmptyObject, + ) { + if (implementation !== null && typeof implementation === 'object') { + options = implementation; + implementation = kDefaultFunction; + } else { + validateObject(options, 'options'); + } + + const { setter = true } = options; + + if (setter === false) { + throw new ERR_INVALID_ARG_VALUE( + 'options.setter', setter, 'cannot be false', + ); + } + + return this.method(object, methodName, implementation, { + __proto__: null, + ...options, + setter, + }); + } + + module(specifier, options = kEmptyObject) { + emitExperimentalWarning('Module mocking'); + if (typeof specifier !== 'string') { + if (!isURL(specifier)) + throw new ERR_INVALID_ARG_TYPE('specifier', ['string', 'URL'], specifier); + specifier = `${specifier}`; + } + validateObject(options, 'options'); + debug('module mock entry, specifier = "%s", options = %o', specifier, options); + + const { + cache = false, + namedExports = kEmptyObject, + defaultExport, + } = options; + const hasDefaultExport = 'defaultExport' in options; + + validateBoolean(cache, 'options.cache'); + validateObject(namedExports, 'options.namedExports'); + + const sharedState = setupSharedModuleState(); + const mockSpecifier = StringPrototypeStartsWith(specifier, 'node:') ? + StringPrototypeSlice(specifier, 5) : specifier; + + // Get the file that called this function. We need four stack frames: + // vm context -> getStructuredStack() -> this function -> actual caller. + const filename = getStructuredStack()[3]?.getFileName(); + // If the caller is already a file URL, use it as is. Otherwise, convert it. + const hasFileProtocol = StringPrototypeStartsWith(filename, 'file://'); + const caller = hasFileProtocol ? filename : pathToFileURL(filename).href; + const { format, url } = sharedState.moduleLoader.resolveSync( + mockSpecifier, caller, null, + ); + debug('module mock, url = "%s", format = "%s", caller = "%s"', url, format, caller); + if (format) { // Format is not yet known for ambiguous files when detection is enabled. + validateOneOf(format, 'format', kSupportedFormats); + } + const baseURL = URL.parse(url); + + if (!baseURL) { + throw new ERR_INVALID_ARG_VALUE( + 'specifier', specifier, 'cannot compute URL', + ); + } + + if (baseURL.searchParams.has(kMockSearchParam)) { + throw new ERR_INVALID_STATE( + `Cannot mock '${specifier}.' The module is already mocked.`, + ); + } + + const fullPath = StringPrototypeStartsWith(url, 'file://') ? + fileURLToPath(url) : null; + const ctx = new MockModuleContext({ + __proto__: null, + baseURL: baseURL.href, + cache, + caller, + defaultExport, + format, + fullPath, + hasDefaultExport, + namedExports, + sharedState, + specifier: mockSpecifier, + }); + + ArrayPrototypePush(this.#mocks, { + __proto__: null, + ctx, + restore: restoreModule, + }); + return ctx; + } + + /** + * Resets the mock tracker, restoring all mocks and clearing timers. + */ + reset() { + this.restoreAll(); + this.#timers?.reset(); + this.#mocks = []; + } + + /** + * Restore all mocks created by this MockTracker instance. + */ + restoreAll() { + for (let i = 0; i < this.#mocks.length; i++) { + const { ctx, restore } = this.#mocks[i]; + + FunctionPrototypeCall(restore, ctx); + } + } + + #setupMock(ctx, fnToMatch) { + const mock = new Proxy(fnToMatch, { + __proto__: null, + apply(_fn, thisArg, argList) { + const fn = FunctionPrototypeCall(nextImpl, ctx); + let result; + let error; + + try { + result = ReflectApply(fn, thisArg, argList); + } catch (err) { + error = err; + throw err; + } finally { + FunctionPrototypeCall(trackCall, ctx, { + __proto__: null, + arguments: argList, + error, + result, + // eslint-disable-next-line no-restricted-syntax + stack: new Error(), + target: undefined, + this: thisArg, + }); + } + + return result; + }, + construct(target, argList, newTarget) { + const realTarget = FunctionPrototypeCall(nextImpl, ctx); + let result; + let error; + + try { + result = ReflectConstruct(realTarget, argList, newTarget); + } catch (err) { + error = err; + throw err; + } finally { + FunctionPrototypeCall(trackCall, ctx, { + __proto__: null, + arguments: argList, + error, + result, + // eslint-disable-next-line no-restricted-syntax + stack: new Error(), + target, + this: result, + }); + } + + return result; + }, + get(target, property, receiver) { + if (property === 'mock') { + return ctx; + } + + return ReflectGet(target, property, receiver); + }, + }); + + ArrayPrototypePush(this.#mocks, { + __proto__: null, + ctx, + restore: restoreFn, + }); + return mock; + } +} + +function setupSharedModuleState() { + if (sharedModuleState === undefined) { + const { mock } = require('#node:test'); + const mockExports = new SafeMap(); + const { port1, port2 } = new MessageChannel(); + const moduleLoader = esmLoader.getOrInitializeCascadedLoader(); + + moduleLoader.register( + 'internal/test_runner/mock/loader', + 'node:', + { __proto__: null, port: port2 }, + [port2], + true, + ); + + sharedModuleState = { + __proto__: null, + loaderPort: port1, + mockExports, + mockMap: new SafeMap(), + moduleLoader, + }; + mock._mockExports = mockExports; + Module._load = FunctionPrototypeBind(cjsMockModuleLoad, sharedModuleState); + } + + return sharedModuleState; +} + +function cjsMockModuleLoad(request, parent, isMain) { + let resolved; + + if (isBuiltin(request)) { + resolved = ensureNodeScheme(request); + } else { + resolved = _resolveFilename(request, parent, isMain); + } + + const config = this.mockMap.get(resolved); + if (config === undefined) { + return _load(request, parent, isMain); + } + + const { + cache, + caller, + defaultExport, + hasDefaultExport, + namedExports, + } = config; + + if (cache && Module._cache[resolved]) { + // The CJS cache entry is deleted when the mock is configured. If it has + // been repopulated, return the exports from that entry. + return Module._cache[resolved].exports; + } + + // eslint-disable-next-line node-core/set-proto-to-null-in-object + const modExports = hasDefaultExport ? defaultExport : {}; + const exportNames = ObjectKeys(namedExports); + + if ((typeof modExports !== 'object' || modExports === null) && + exportNames.length > 0) { + // eslint-disable-next-line no-restricted-syntax + throw new Error(kBadExportsMessage); + } + + for (let i = 0; i < exportNames.length; ++i) { + const name = exportNames[i]; + const descriptor = ObjectGetOwnPropertyDescriptor(namedExports, name); + ObjectDefineProperty(modExports, name, descriptor); + } + + if (cache) { + const entry = new Module(resolved, caller); + + entry.exports = modExports; + entry.filename = resolved; + entry.loaded = true; + entry.paths = _nodeModulePaths(entry.path); + Module._cache[resolved] = entry; + } + + return modExports; +} + +function validateStringOrSymbol(value, name) { + if (typeof value !== 'string' && typeof value !== 'symbol') { + throw new ERR_INVALID_ARG_TYPE(name, ['string', 'symbol'], value); + } +} + +function validateTimes(value, name) { + if (value === Infinity) { + return; + } + + validateInteger(value, name, 1); +} + +function findMethodOnPrototypeChain(instance, methodName) { + let host = instance; + let descriptor; + + while (host !== null) { + descriptor = ObjectGetOwnPropertyDescriptor(host, methodName); + + if (descriptor) { + break; + } + + host = ObjectGetPrototypeOf(host); + } + + return descriptor; +} + +function waitForAck(buf) { + const result = AtomicsWait(buf, 0, 0, kWaitTimeout); + + notStrictEqual(result, 'timed-out', 'test mocking synchronization failed'); + strictEqual(buf[0], kMockSuccess); +} + +function ensureNodeScheme(specifier) { + if (!StringPrototypeStartsWith(specifier, 'node:')) { + return `node:${specifier}`; + } + + return specifier; +} + +if (!enableModuleMocking) { + delete MockTracker.prototype.module; +} + +module.exports = { + ensureNodeScheme, + kBadExportsMessage, + kMockSearchParam, + kMockSuccess, + kMockExists, + kMockUnknownMessage, + MockTracker, +}; diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js new file mode 100644 index 0000000..2a722a6 --- /dev/null +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -0,0 +1,795 @@ +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; + +const { + ArrayPrototypeAt, + ArrayPrototypeForEach, + ArrayPrototypeIncludes, + DatePrototypeGetTime, + DatePrototypeToString, + FunctionPrototypeApply, + FunctionPrototypeBind, + FunctionPrototypeToString, + NumberIsNaN, + ObjectDefineProperties, + ObjectDefineProperty, + ObjectGetOwnPropertyDescriptor, + ObjectGetOwnPropertyDescriptors, + Promise, + Symbol, + SymbolAsyncIterator, + SymbolDispose, + globalThis, +} = primordials; + +const { + validateAbortSignal, + validateNumber, + validateStringArray, +} = require('#internal/validators'); + +const { + AbortError, + codes: { + ERR_INVALID_ARG_VALUE, + ERR_INVALID_STATE, + }, +} = require('#internal/errors'); + +const { TIMEOUT_MAX } = require('#internal/timers'); + +const PriorityQueue = require('#internal/priority_queue'); +const nodeTimers = require('timers'); +const nodeTimersPromises = require('timers/promises'); +const EventEmitter = require('events'); + +let kResistStopPropagation; +// Internal reference to the MockTimers class inside MockDate +let kMock; +// Initial epoch to which #now should be set to +const kInitialEpoch = 0; + +function compareTimersLists(a, b) { + return (a.runAt - b.runAt) || (a.id - b.id); +} + +function setPosition(node, pos) { + node.priorityQueuePosition = pos; +} + +function abortIt(signal) { + return new AbortError(undefined, { __proto__: null, cause: signal.reason }); +} + +/** + * @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date', 'scheduler.wait')[]} Supported timers + */ +const SUPPORTED_APIS = ['setTimeout', 'setInterval', 'setImmediate', 'Date', 'scheduler.wait']; +const TIMERS_DEFAULT_INTERVAL = { + __proto__: null, + setImmediate: -1, +}; + +class Timeout { + constructor(opts) { + this.id = opts.id; + this.callback = opts.callback; + this.runAt = opts.runAt; + this.interval = opts.interval; + this.args = opts.args; + } + + hasRef() { + return true; + } + + ref() { + return this; + } + + unref() { + return this; + } + + refresh() { + return this; + } +} + +class MockTimers { + #realSetTimeout; + #realClearTimeout; + #realSetInterval; + #realClearInterval; + #realSetImmediate; + #realClearImmediate; + + #realPromisifiedSetTimeout; + #realPromisifiedSetInterval; + #realTimersPromisifiedSchedulerWait; + + #realTimersSetTimeout; + #realTimersClearTimeout; + #realTimersSetInterval; + #realTimersClearInterval; + #realTimersSetImmediate; + #realTimersClearImmediate; + #realPromisifiedSetImmediate; + + #nativeDateDescriptor; + + #timersInContext = []; + #isEnabled = false; + #currentTimer = 1; + #now = kInitialEpoch; + + #executionQueue = new PriorityQueue(compareTimersLists, setPosition); + + #setTimeout = FunctionPrototypeBind(this.#createTimer, this, false); + #clearTimeout = FunctionPrototypeBind(this.#clearTimer, this); + #setInterval = FunctionPrototypeBind(this.#createTimer, this, true); + #clearInterval = FunctionPrototypeBind(this.#clearTimer, this); + #clearImmediate = FunctionPrototypeBind(this.#clearTimer, this); + + #restoreSetImmediate() { + ObjectDefineProperty( + globalThis, + 'setImmediate', + this.#realSetImmediate, + ); + ObjectDefineProperty( + globalThis, + 'clearImmediate', + this.#realClearImmediate, + ); + ObjectDefineProperty( + nodeTimers, + 'setImmediate', + this.#realTimersSetImmediate, + ); + ObjectDefineProperty( + nodeTimers, + 'clearImmediate', + this.#realTimersClearImmediate, + ); + ObjectDefineProperty( + nodeTimersPromises, + 'setImmediate', + this.#realPromisifiedSetImmediate, + ); + } + + #restoreOriginalSetInterval() { + ObjectDefineProperty( + globalThis, + 'setInterval', + this.#realSetInterval, + ); + ObjectDefineProperty( + globalThis, + 'clearInterval', + this.#realClearInterval, + ); + ObjectDefineProperty( + nodeTimers, + 'setInterval', + this.#realTimersSetInterval, + ); + ObjectDefineProperty( + nodeTimers, + 'clearInterval', + this.#realTimersClearInterval, + ); + ObjectDefineProperty( + nodeTimersPromises, + 'setInterval', + this.#realPromisifiedSetInterval, + ); + } + + #restoreOriginalSchedulerWait() { + nodeTimersPromises.scheduler.wait = FunctionPrototypeBind( + this.#realTimersPromisifiedSchedulerWait, + this, + ); + } + + #restoreOriginalSetTimeout() { + ObjectDefineProperty( + globalThis, + 'setTimeout', + this.#realSetTimeout, + ); + ObjectDefineProperty( + globalThis, + 'clearTimeout', + this.#realClearTimeout, + ); + ObjectDefineProperty( + nodeTimers, + 'setTimeout', + this.#realTimersSetTimeout, + ); + ObjectDefineProperty( + nodeTimers, + 'clearTimeout', + this.#realTimersClearTimeout, + ); + ObjectDefineProperty( + nodeTimersPromises, + 'setTimeout', + this.#realPromisifiedSetTimeout, + ); + } + + #storeOriginalSetImmediate() { + this.#realSetImmediate = ObjectGetOwnPropertyDescriptor( + globalThis, + 'setImmediate', + ); + this.#realClearImmediate = ObjectGetOwnPropertyDescriptor( + globalThis, + 'clearImmediate', + ); + this.#realTimersSetImmediate = ObjectGetOwnPropertyDescriptor( + nodeTimers, + 'setImmediate', + ); + this.#realTimersClearImmediate = ObjectGetOwnPropertyDescriptor( + nodeTimers, + 'clearImmediate', + ); + this.#realPromisifiedSetImmediate = ObjectGetOwnPropertyDescriptor( + nodeTimersPromises, + 'setImmediate', + ); + } + + #storeOriginalSetInterval() { + this.#realSetInterval = ObjectGetOwnPropertyDescriptor( + globalThis, + 'setInterval', + ); + this.#realClearInterval = ObjectGetOwnPropertyDescriptor( + globalThis, + 'clearInterval', + ); + this.#realTimersSetInterval = ObjectGetOwnPropertyDescriptor( + nodeTimers, + 'setInterval', + ); + this.#realTimersClearInterval = ObjectGetOwnPropertyDescriptor( + nodeTimers, + 'clearInterval', + ); + this.#realPromisifiedSetInterval = ObjectGetOwnPropertyDescriptor( + nodeTimersPromises, + 'setInterval', + ); + } + + #storeOriginalSchedulerWait() { + + this.#realTimersPromisifiedSchedulerWait = FunctionPrototypeBind( + nodeTimersPromises.scheduler.wait, + this, + ); + } + + #storeOriginalSetTimeout() { + this.#realSetTimeout = ObjectGetOwnPropertyDescriptor( + globalThis, + 'setTimeout', + ); + this.#realClearTimeout = ObjectGetOwnPropertyDescriptor( + globalThis, + 'clearTimeout', + ); + this.#realTimersSetTimeout = ObjectGetOwnPropertyDescriptor( + nodeTimers, + 'setTimeout', + ); + this.#realTimersClearTimeout = ObjectGetOwnPropertyDescriptor( + nodeTimers, + 'clearTimeout', + ); + this.#realPromisifiedSetTimeout = ObjectGetOwnPropertyDescriptor( + nodeTimersPromises, + 'setTimeout', + ); + } + + #createTimer(isInterval, callback, delay, ...args) { + if (delay > TIMEOUT_MAX) { + delay = 1; + } + + const timerId = this.#currentTimer++; + const opts = { + __proto__: null, + id: timerId, + callback, + runAt: this.#now + delay, + interval: isInterval ? delay : undefined, + args, + }; + + const timer = new Timeout(opts); + this.#executionQueue.insert(timer); + return timer; + } + + #clearTimer(timer) { + if (timer?.priorityQueuePosition !== undefined) { + this.#executionQueue.removeAt(timer.priorityQueuePosition); + timer.priorityQueuePosition = undefined; + } + } + + #createDate() { + kMock ??= Symbol('MockTimers'); + const NativeDateConstructor = this.#nativeDateDescriptor.value; + /** + * Function to mock the Date constructor, treats cases as per ECMA-262 + * and returns a Date object with a mocked implementation + * @typedef {Date} MockDate + * @returns {MockDate} a mocked Date object + */ + function MockDate(year, month, date, hours, minutes, seconds, ms) { + const mockTimersSource = MockDate[kMock]; + const nativeDate = mockTimersSource.#nativeDateDescriptor.value; + + // As of the fake-timers implementation for Sinon + // ref https://github.com/sinonjs/fake-timers/blob/a4c757f80840829e45e0852ea1b17d87a998388e/src/fake-timers-src.js#L456 + // This covers the Date constructor called as a function ref. + // ECMA-262 Edition 5.1 section 15.9.2. + // and ECMA-262 Edition 14 Section 21.4.2.1 + // replaces 'this instanceof MockDate' with a more reliable check + // from ECMA-262 Edition 14 Section 13.3.12.1 NewTarget + if (!new.target) { + return DatePrototypeToString(new nativeDate(mockTimersSource.#now)); + } + + // Cases where Date is called as a constructor + // This is intended as a defensive implementation to avoid + // having unexpected returns + switch (arguments.length) { + case 0: + return new nativeDate(MockDate[kMock].#now); + case 1: + return new nativeDate(year); + case 2: + return new nativeDate(year, month); + case 3: + return new nativeDate(year, month, date); + case 4: + return new nativeDate(year, month, date, hours); + case 5: + return new nativeDate(year, month, date, hours, minutes); + case 6: + return new nativeDate(year, month, date, hours, minutes, seconds); + default: + return new nativeDate(year, month, date, hours, minutes, seconds, ms); + } + } + + // Prototype is read-only, and non assignable through Object.defineProperties + // eslint-disable-next-line no-unused-vars -- used to get the prototype out of the object + const { prototype, ...dateProps } = ObjectGetOwnPropertyDescriptors(NativeDateConstructor); + + // Binds all the properties of Date to the MockDate function + ObjectDefineProperties( + MockDate, + dateProps, + ); + + MockDate.now = function now() { + return MockDate[kMock].#now; + }; + + // This is just to print the function { native code } in the console + // when the user prints the function and not the internal code + MockDate.toString = function toString() { + return FunctionPrototypeToString(MockDate[kMock].#nativeDateDescriptor.value); + }; + + // We need to pollute the prototype of this + ObjectDefineProperties(MockDate, { + __proto__: null, + [kMock]: { + __proto__: null, + enumerable: false, + configurable: false, + writable: false, + value: this, + }, + + isMock: { + __proto__: null, + enumerable: true, + configurable: false, + writable: false, + value: true, + }, + }); + + MockDate.prototype = NativeDateConstructor.prototype; + MockDate.parse = NativeDateConstructor.parse; + MockDate.UTC = NativeDateConstructor.UTC; + MockDate.prototype.toUTCString = NativeDateConstructor.prototype.toUTCString; + return MockDate; + } + + async * #setIntervalPromisified(interval, result, options) { + const context = this; + const emitter = new EventEmitter(); + if (options?.signal) { + validateAbortSignal(options.signal, 'options.signal'); + + if (options.signal.aborted) { + throw abortIt(options.signal); + } + + const onAbort = (reason) => { + emitter.emit('data', { __proto__: null, aborted: true, reason }); + }; + + kResistStopPropagation ??= require('#internal/event_target').kResistStopPropagation; + options.signal.addEventListener('abort', onAbort, { + __proto__: null, + once: true, + [kResistStopPropagation]: true, + }); + } + + const eventIt = EventEmitter.on(emitter, 'data'); + const callback = () => { + emitter.emit('data', result); + }; + + const timer = this.#createTimer(true, callback, interval, options); + const clearListeners = () => { + emitter.removeAllListeners(); + context.#clearTimer(timer); + }; + const iterator = { + __proto__: null, + [SymbolAsyncIterator]() { + return this; + }, + async next() { + const result = await eventIt.next(); + const value = ArrayPrototypeAt(result.value, 0); + if (value?.aborted) { + iterator.return(); + throw abortIt(options.signal); + } + + return { + __proto__: null, + done: result.done, + value, + }; + }, + async return() { + clearListeners(); + return eventIt.return(); + }, + }; + yield* iterator; + } + + #setImmediate(callback, ...args) { + return this.#createTimer( + false, + callback, + TIMERS_DEFAULT_INTERVAL.setImmediate, + ...args, + ); + } + + #promisifyTimer({ timerFn, clearFn, ms, result, options }) { + return new Promise((resolve, reject) => { + if (options?.signal) { + try { + validateAbortSignal(options.signal, 'options.signal'); + } catch (err) { + return reject(err); + } + + if (options.signal.aborted) { + return reject(abortIt(options.signal)); + } + } + + const onabort = () => { + clearFn(timer); + return reject(abortIt(options.signal)); + }; + + const timer = timerFn(() => { + return resolve(result); + }, ms); + + if (options?.signal) { + kResistStopPropagation ??= require('#internal/event_target').kResistStopPropagation; + options.signal.addEventListener('abort', onabort, { + __proto__: null, + once: true, + [kResistStopPropagation]: true, + }); + } + }); + } + + #setImmediatePromisified(result, options) { + return this.#promisifyTimer({ + __proto__: null, + timerFn: FunctionPrototypeBind(this.#setImmediate, this), + clearFn: FunctionPrototypeBind(this.#clearImmediate, this), + ms: TIMERS_DEFAULT_INTERVAL.setImmediate, + result, + options, + }); + } + + #setTimeoutPromisified(ms, result, options) { + return this.#promisifyTimer({ + __proto__: null, + timerFn: FunctionPrototypeBind(this.#setTimeout, this), + clearFn: FunctionPrototypeBind(this.#clearTimeout, this), + ms, + result, + options, + }); + } + + #assertTimersAreEnabled() { + if (!this.#isEnabled) { + throw new ERR_INVALID_STATE( + 'You should enable MockTimers first by calling the .enable function', + ); + } + } + + #assertTimeArg(time) { + if (time < 0) { + throw new ERR_INVALID_ARG_VALUE('time', 'positive integer', time); + } + } + + #isValidDateWithGetTime(maybeDate) { + // Validation inspired on https://github.com/inspect-js/is-date-object/blob/main/index.js#L3-L11 + try { + DatePrototypeGetTime(maybeDate); + return true; + } catch { + return false; + } + } + + #toggleEnableTimers(activate) { + const options = { + __proto__: null, + toFake: { + '__proto__': null, + 'scheduler.wait': () => { + this.#storeOriginalSchedulerWait(); + + nodeTimersPromises.scheduler.wait = (delay, options) => + this.#setTimeoutPromisified(delay, undefined, options); + }, + 'setTimeout': () => { + this.#storeOriginalSetTimeout(); + + globalThis.setTimeout = this.#setTimeout; + globalThis.clearTimeout = this.#clearTimeout; + + nodeTimers.setTimeout = this.#setTimeout; + nodeTimers.clearTimeout = this.#clearTimeout; + + nodeTimersPromises.setTimeout = FunctionPrototypeBind( + this.#setTimeoutPromisified, + this, + ); + }, + 'setInterval': () => { + this.#storeOriginalSetInterval(); + + globalThis.setInterval = this.#setInterval; + globalThis.clearInterval = this.#clearInterval; + + nodeTimers.setInterval = this.#setInterval; + nodeTimers.clearInterval = this.#clearInterval; + + nodeTimersPromises.setInterval = FunctionPrototypeBind( + this.#setIntervalPromisified, + this, + ); + }, + 'setImmediate': () => { + this.#storeOriginalSetImmediate(); + + // setImmediate functions needs to bind MockTimers + // otherwise it will throw an error when called + // "Receiver must be an instance of MockTimers" + // because #setImmediate is the only function here + // that calls #createTimer and it's not bound to MockTimers + globalThis.setImmediate = FunctionPrototypeBind( + this.#setImmediate, + this, + ); + globalThis.clearImmediate = this.#clearImmediate; + + nodeTimers.setImmediate = FunctionPrototypeBind( + this.#setImmediate, + this, + ); + nodeTimers.clearImmediate = this.#clearImmediate; + nodeTimersPromises.setImmediate = FunctionPrototypeBind( + this.#setImmediatePromisified, + this, + ); + }, + 'Date': () => { + this.#nativeDateDescriptor = ObjectGetOwnPropertyDescriptor(globalThis, 'Date'); + globalThis.Date = this.#createDate(); + }, + }, + toReal: { + '__proto__': null, + 'scheduler.wait': () => { + this.#restoreOriginalSchedulerWait(); + }, + 'setTimeout': () => { + this.#restoreOriginalSetTimeout(); + }, + 'setInterval': () => { + this.#restoreOriginalSetInterval(); + }, + 'setImmediate': () => { + this.#restoreSetImmediate(); + }, + 'Date': () => { + ObjectDefineProperty(globalThis, 'Date', this.#nativeDateDescriptor); + }, + }, + }; + + const target = activate ? options.toFake : options.toReal; + ArrayPrototypeForEach(this.#timersInContext, (timer) => target[timer]()); + this.#isEnabled = activate; + } + + /** + * Advances the virtual time of MockTimers by the specified duration (in milliseconds). + * This method simulates the passage of time and triggers any scheduled timers that are due. + * @param {number} [time=1] - The amount of time (in milliseconds) to advance the virtual time. + * @throws {ERR_INVALID_STATE} If MockTimers are not enabled. + * @throws {ERR_INVALID_ARG_VALUE} If a negative time value is provided. + */ + tick(time = 1) { + this.#assertTimersAreEnabled(); + this.#assertTimeArg(time); + + this.#now += time; + let timer = this.#executionQueue.peek(); + while (timer) { + if (timer.runAt > this.#now) break; + FunctionPrototypeApply(timer.callback, undefined, timer.args); + + // Check if the timeout was cleared by calling clearTimeout inside its own callback + const afterCallback = this.#executionQueue.peek(); + if (afterCallback?.id === timer.id) { + this.#executionQueue.shift(); + timer.priorityQueuePosition = undefined; + } + + if (timer.interval !== undefined) { + timer.runAt += timer.interval; + this.#executionQueue.insert(timer); + } + + timer = this.#executionQueue.peek(); + } + } + + /** + * @typedef {{apis: SUPPORTED_APIS;now: number | Date;}} EnableOptions Options to enable the timers + * @property {SUPPORTED_APIS} apis List of timers to enable, defaults to all + * @property {number | Date} now The epoch to which the timers should be set to, defaults to 0 + */ + /** + * Enables the MockTimers replacing the native timers with the fake ones. + * @param {EnableOptions} [options] + */ + enable(options = { __proto__: null, apis: SUPPORTED_APIS, now: 0 }) { + const internalOptions = { __proto__: null, ...options }; + if (this.#isEnabled) { + throw new ERR_INVALID_STATE('MockTimers is already enabled!'); + } + + if (NumberIsNaN(internalOptions.now)) { + throw new ERR_INVALID_ARG_VALUE('now', internalOptions.now, `epoch must be a positive integer received ${internalOptions.now}`); + } + + internalOptions.now ||= 0; + + internalOptions.apis ||= SUPPORTED_APIS; + + validateStringArray(internalOptions.apis, 'options.apis'); + // Check that the timers passed are supported + ArrayPrototypeForEach(internalOptions.apis, (timer) => { + if (!ArrayPrototypeIncludes(SUPPORTED_APIS, timer)) { + throw new ERR_INVALID_ARG_VALUE( + 'options.apis', + timer, + `option ${timer} is not supported`, + ); + } + }); + this.#timersInContext = internalOptions.apis; + + // Checks if the second argument is the initial time + if (this.#isValidDateWithGetTime(internalOptions.now)) { + this.#now = DatePrototypeGetTime(internalOptions.now); + } else if (validateNumber(internalOptions.now, 'initialTime') === undefined) { + this.#assertTimeArg(internalOptions.now); + this.#now = internalOptions.now; + } + + this.#toggleEnableTimers(true); + } + + /** + * Sets the current time to the given epoch. + * @param {number} time The epoch to set the current time to. + */ + setTime(time = kInitialEpoch) { + validateNumber(time, 'time'); + this.#assertTimeArg(time); + this.#assertTimersAreEnabled(); + + this.#now = time; + } + + /** + * An alias for `this.reset()`, allowing the disposal of the `MockTimers` instance. + */ + [SymbolDispose]() { + this.reset(); + } + + /** + * Resets MockTimers, disabling any enabled timers and clearing the execution queue. + * Does nothing if MockTimers are not enabled. + */ + reset() { + // Ignore if not enabled + if (!this.#isEnabled) return; + + this.#toggleEnableTimers(false); + this.#timersInContext = []; + this.#now = kInitialEpoch; + + let timer = this.#executionQueue.peek(); + while (timer) { + this.#executionQueue.shift(); + timer = this.#executionQueue.peek(); + } + } + + /** + * Runs all scheduled timers until there are no more pending timers. + * @throws {ERR_INVALID_STATE} If MockTimers are not enabled. + */ + runAll() { + this.#assertTimersAreEnabled(); + const longestTimer = this.#executionQueue.peekBottom(); + if (!longestTimer) return; + this.tick(longestTimer.runAt - this.#now); + } +} + +module.exports = { MockTimers }; diff --git a/lib/internal/test_runner/reporter/dot.js b/lib/internal/test_runner/reporter/dot.js index a4ac33b..784a5ca 100644 --- a/lib/internal/test_runner/reporter/dot.js +++ b/lib/internal/test_runner/reporter/dot.js @@ -1,18 +1,42 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/lib/test/reporter/dot.js -'use strict' +const { primordials, internalBinding } = require('#lib/bootstrap'); -module.exports = async function * dot (source) { - let count = 0 - for await (const { type } of source) { +'use strict'; +const { + ArrayPrototypePush, + MathMax, +} = primordials; +const colors = require('#internal/util/colors'); +const { formatTestReport } = require('#internal/test_runner/reporter/utils'); + +module.exports = async function* dot(source) { + let count = 0; + let columns = getLineLength(); + const failedTests = []; + for await (const { type, data } of source) { if (type === 'test:pass') { - yield '.' + yield `${colors.green}.${colors.reset}`; } if (type === 'test:fail') { - yield 'X' + yield `${colors.red}X${colors.reset}`; + ArrayPrototypePush(failedTests, data); } - if ((type === 'test:fail' || type === 'test:pass') && ++count % 20 === 0) { - yield '\n' + if ((type === 'test:fail' || type === 'test:pass') && ++count === columns) { + yield '\n'; + + // Getting again in case the terminal was resized. + columns = getLineLength(); + count = 0; } } - yield '\n' + yield '\n'; + if (failedTests.length > 0) { + yield `\n${colors.red}Failed tests:${colors.white}\n\n`; + for (const test of failedTests) { + yield formatTestReport('test:fail', test); + } + } +}; + +function getLineLength() { + return MathMax(process.stdout.columns ?? 20, 20); } diff --git a/lib/internal/test_runner/reporter/junit.js b/lib/internal/test_runner/reporter/junit.js new file mode 100644 index 0000000..c6bcf65 --- /dev/null +++ b/lib/internal/test_runner/reporter/junit.js @@ -0,0 +1,160 @@ +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; +const { + ArrayPrototypeFilter, + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypePush, + ArrayPrototypeSome, + NumberPrototypeToFixed, + ObjectEntries, + RegExpPrototypeSymbolReplace, + String, + StringPrototypeRepeat, +} = primordials; + +const { inspectWithNoCustomRetry } = require('#internal/errors'); +const { hostname } = require('os'); + +const inspectOptions = { __proto__: null, colors: false, breakLength: Infinity }; +const HOSTNAME = hostname(); + +function escapeAttribute(s = '') { + return escapeContent(RegExpPrototypeSymbolReplace(/"/g, RegExpPrototypeSymbolReplace(/\n/g, s, ''), '"')); +} + +function escapeContent(s = '') { + return RegExpPrototypeSymbolReplace(/\n`; + } + const attrsString = ArrayPrototypeJoin(ArrayPrototypeMap(ObjectEntries(attrs) + , ({ 0: key, 1: value }) => `${key}="${escapeAttribute(String(value))}"`) + , ' '); + if (!children?.length) { + return `${indent}<${tag} ${attrsString}/>\n`; + } + const childrenString = ArrayPrototypeJoin(ArrayPrototypeMap(children ?? [], treeToXML), ''); + return `${indent}<${tag} ${attrsString}>\n${childrenString}${indent}\n`; +} + +function isFailure(node) { + return (node?.children && ArrayPrototypeSome(node.children, (c) => c.tag === 'failure')) || node?.attrs?.failures; +} + +function isSkipped(node) { + return (node?.children && ArrayPrototypeSome(node.children, (c) => c.tag === 'skipped')) || node?.attrs?.failures; +} + +module.exports = async function* junitReporter(source) { + yield '\n'; + yield '\n'; + let currentSuite = null; + const roots = []; + + function startTest(event) { + const originalSuite = currentSuite; + currentSuite = { + __proto__: null, + attrs: { __proto__: null, name: event.data.name }, + nesting: event.data.nesting, + parent: currentSuite, + children: [], + }; + if (originalSuite?.children) { + ArrayPrototypePush(originalSuite.children, currentSuite); + } + if (!currentSuite.parent) { + ArrayPrototypePush(roots, currentSuite); + } + } + + for await (const event of source) { + switch (event.type) { + case 'test:start': { + startTest(event); + break; + } + case 'test:pass': + case 'test:fail': { + if (!currentSuite) { + startTest({ __proto__: null, data: { __proto__: null, name: 'root', nesting: 0 } }); + } + if (currentSuite.attrs.name !== event.data.name || + currentSuite.nesting !== event.data.nesting) { + startTest(event); + } + const currentTest = currentSuite; + if (currentSuite?.nesting === event.data.nesting) { + currentSuite = currentSuite.parent; + } + currentTest.attrs.time = NumberPrototypeToFixed(event.data.details.duration_ms / 1000, 6); + const nonCommentChildren = ArrayPrototypeFilter(currentTest.children, (c) => c.comment == null); + if (nonCommentChildren.length > 0) { + currentTest.tag = 'testsuite'; + currentTest.attrs.disabled = 0; + currentTest.attrs.errors = 0; + currentTest.attrs.tests = nonCommentChildren.length; + currentTest.attrs.failures = ArrayPrototypeFilter(currentTest.children, isFailure).length; + currentTest.attrs.skipped = ArrayPrototypeFilter(currentTest.children, isSkipped).length; + currentTest.attrs.hostname = HOSTNAME; + } else { + currentTest.tag = 'testcase'; + currentTest.attrs.classname = event.data.classname ?? 'test'; + if (event.data.skip) { + ArrayPrototypePush(currentTest.children, { + __proto__: null, nesting: event.data.nesting + 1, tag: 'skipped', + attrs: { __proto__: null, type: 'skipped', message: event.data.skip }, + }); + } + if (event.data.todo) { + ArrayPrototypePush(currentTest.children, { + __proto__: null, nesting: event.data.nesting + 1, tag: 'skipped', + attrs: { __proto__: null, type: 'todo', message: event.data.todo }, + }); + } + if (event.type === 'test:fail') { + const error = event.data.details?.error; + currentTest.children.push({ + __proto__: null, + nesting: event.data.nesting + 1, + tag: 'failure', + attrs: { __proto__: null, type: error?.failureType || error?.code, message: error?.message ?? '' }, + children: [inspectWithNoCustomRetry(error, inspectOptions)], + }); + currentTest.failures = 1; + currentTest.attrs.failure = error?.message ?? ''; + } + } + break; + } + case 'test:diagnostic': { + const parent = currentSuite?.children ?? roots; + ArrayPrototypePush(parent, { + __proto__: null, nesting: event.data.nesting, comment: event.data.message, + }); + break; + } default: + break; + } + } + for (const suite of roots) { + yield treeToXML(suite); + } + yield '\n'; +}; diff --git a/lib/internal/test_runner/reporter/lcov.js b/lib/internal/test_runner/reporter/lcov.js new file mode 100644 index 0000000..1822311 --- /dev/null +++ b/lib/internal/test_runner/reporter/lcov.js @@ -0,0 +1,107 @@ +'use strict'; + +const { relative } = require('path'); +const Transform = require('#internal/streams/transform'); + +// This reporter is based on the LCOV format, as described here: +// https://ltp.sourceforge.net/coverage/lcov/geninfo.1.php +// Excerpts from this documentation are included in the comments that make up +// the _transform function below. +class LcovReporter extends Transform { + constructor(options) { + super({ ...options, writableObjectMode: true, __proto__: null }); + } + + _transform(event, _encoding, callback) { + if (event.type !== 'test:coverage') { + return callback(null); + } + let lcov = ''; + // A tracefile is made up of several human-readable lines of text, divided + // into sections. If available, a tracefile begins with the testname which + // is stored in the following format: + // ## TN:\ + lcov += 'TN:\n'; + const { + data: { + summary: { workingDirectory }, + }, + } = event; + try { + for (let i = 0; i < event.data.summary.files.length; i++) { + const file = event.data.summary.files[i]; + // For each source file referenced in the .da file, there is a section + // containing filename and coverage data: + // ## SF:\ + lcov += `SF:${relative(workingDirectory, file.path)}\n`; + + // Following is a list of line numbers for each function name found in + // the source file: + // ## FN:\,\ + // + // After, there is a list of execution counts for each instrumented + // function: + // ## FNDA:\,\ + // + // This loop adds the FN lines to the lcov variable as it goes and + // gathers the FNDA lines to be added later. This way we only loop + // through the list of functions once. + let fnda = ''; + for (let j = 0; j < file.functions.length; j++) { + const func = file.functions[j]; + const name = func.name || `anonymous_${j}`; + lcov += `FN:${func.line},${name}\n`; + fnda += `FNDA:${func.count},${name}\n`; + } + lcov += fnda; + + // This list is followed by two lines containing the number of + // functions found and hit: + // ## FNF:\ + // ## FNH:\ + lcov += `FNF:${file.totalFunctionCount}\n`; + lcov += `FNH:${file.coveredFunctionCount}\n`; + + // Branch coverage information is stored which one line per branch: + // ## BRDA:\,\,\,\ + // Block number and branch number are gcc internal IDs for the branch. + // Taken is either '-' if the basic block containing the branch was + // never executed or a number indicating how often that branch was + // taken. + for (let j = 0; j < file.branches.length; j++) { + lcov += `BRDA:${file.branches[j].line},${j},0,${file.branches[j].count}\n`; + } + + // Branch coverage summaries are stored in two lines: + // ## BRF:\ + // ## BRH:\ + lcov += `BRF:${file.totalBranchCount}\n`; + lcov += `BRH:${file.coveredBranchCount}\n`; + + // Then there is a list of execution counts for each instrumented line + // (i.e. a line which resulted in executable code): + // ## DA:\,\[,\] + const sortedLines = file.lines.toSorted((a, b) => a.line - b.line); + for (let j = 0; j < sortedLines.length; j++) { + lcov += `DA:${sortedLines[j].line},${sortedLines[j].count}\n`; + } + + // At the end of a section, there is a summary about how many lines + // were found and how many were actually instrumented: + // ## LH:\ + // ## LF:\ + lcov += `LH:${file.coveredLineCount}\n`; + lcov += `LF:${file.totalLineCount}\n`; + + // Each sections ends with: + // end_of_record + lcov += 'end_of_record\n'; + } + } catch (error) { + return callback(error); + } + return callback(null, lcov); + } +} + +module.exports = LcovReporter; diff --git a/lib/internal/test_runner/reporter/spec.js b/lib/internal/test_runner/reporter/spec.js index 5eb3b17..926237b 100644 --- a/lib/internal/test_runner/reporter/spec.js +++ b/lib/internal/test_runner/reporter/spec.js @@ -1,110 +1,107 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/lib/test/reporter/spec.js -'use strict' +const { primordials, internalBinding } = require('#lib/bootstrap'); +'use strict'; const { ArrayPrototypeJoin, ArrayPrototypePop, + ArrayPrototypePush, ArrayPrototypeShift, ArrayPrototypeUnshift, - hardenRegExp, - RegExpPrototypeSymbolSplit, - SafeMap, - StringPrototypeRepeat -} = require('#internal/per_context/primordials') -const assert = require('assert') -const Transform = require('stream').Transform -const { inspectWithNoCustomRetry } = require('#internal/errors') -const { green, blue, red, white, gray } = require('#internal/util/colors') - -const inspectOptions = { __proto__: null, colors: true, breakLength: Infinity } +} = primordials; +const assert = require('assert'); +const Transform = require('#internal/streams/transform'); +const colors = require('#internal/util/colors'); +const { kSubtestsFailed } = require('#internal/test_runner/test'); +const { getCoverageReport } = require('#internal/test_runner/utils'); +const { relative } = require('path'); +const { + formatTestReport, + indent, + reporterColorMap, + reporterUnicodeSymbolMap, +} = require('#internal/test_runner/reporter/utils'); -const colors = { - __proto__: null, - 'test:fail': red, - 'test:pass': green, - 'test:diagnostic': blue -} -const symbols = { - __proto__: null, - 'test:fail': '\u2716 ', - 'test:pass': '\u2714 ', - 'test:diagnostic': '\u2139 ', - 'arrow:right': '\u25B6 ' -} class SpecReporter extends Transform { - #stack = [] - #reported = [] - #indentMemo = new SafeMap() + #stack = []; + #reported = []; + #failedTests = []; + #cwd = process.cwd(); - constructor () { - super({ writableObjectMode: true }) + constructor() { + super({ __proto__: null, writableObjectMode: true }); + colors.refresh(); } - #indent (nesting) { - let value = this.#indentMemo.get(nesting) - if (value === undefined) { - value = StringPrototypeRepeat(' ', nesting) - this.#indentMemo.set(nesting, value) + #handleTestReportEvent(type, data) { + const subtest = ArrayPrototypeShift(this.#stack); // This is the matching `test:start` event + if (subtest) { + assert(subtest.type === 'test:start'); + assert(subtest.data.nesting === data.nesting); + assert(subtest.data.name === data.name); } - - return value - } - - #formatError (error, indent) { - if (!error) return '' - const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error - const message = ArrayPrototypeJoin( - RegExpPrototypeSymbolSplit( - hardenRegExp(/\r?\n/), - inspectWithNoCustomRetry(err, inspectOptions) - ), `\n${indent} `) - return `\n${indent} ${message}\n` + let prefix = ''; + while (this.#stack.length) { + // Report all the parent `test:start` events + const parent = ArrayPrototypePop(this.#stack); + assert(parent.type === 'test:start'); + const msg = parent.data; + ArrayPrototypeUnshift(this.#reported, msg); + prefix += `${indent(msg.nesting)}${reporterUnicodeSymbolMap['arrow:right']}${msg.name}\n`; + } + let hasChildren = false; + if (this.#reported[0] && this.#reported[0].nesting === data.nesting && this.#reported[0].name === data.name) { + ArrayPrototypeShift(this.#reported); + hasChildren = true; + } + const indentation = indent(data.nesting); + return `${formatTestReport(type, data, prefix, indentation, hasChildren)}\n`; } - - #handleEvent ({ type, data }) { - const color = colors[type] ?? white - const symbol = symbols[type] ?? ' ' - + #handleEvent({ type, data }) { switch (type) { case 'test:fail': - case 'test:pass': { - const subtest = ArrayPrototypeShift(this.#stack) // This is the matching `test:start` event - if (subtest) { - assert(subtest.type === 'test:start') - assert(subtest.data.nesting === data.nesting) - assert(subtest.data.name === data.name) - } - let prefix = '' - while (this.#stack.length) { - // Report all the parent `test:start` events - const parent = ArrayPrototypePop(this.#stack) - assert(parent.type === 'test:start') - const msg = parent.data - ArrayPrototypeUnshift(this.#reported, msg) - prefix += `${this.#indent(msg.nesting)}${symbols['arrow:right']}${msg.name}\n` - } - const indent = this.#indent(data.nesting) - const duration_ms = data.details?.duration_ms ? ` ${gray}(${data.details.duration_ms}ms)${white}` : '' // eslint-disable-line camelcase - const title = `${data.name}${duration_ms}` // eslint-disable-line camelcase - if (this.#reported[0] && this.#reported[0].nesting === data.nesting && this.#reported[0].name === data.name) { - // If this test has had children - it was already reporter, so slightly modify the output - ArrayPrototypeShift(this.#reported) - return `${prefix}${indent}${color}${symbols['arrow:right']}${white}${title}\n\n` + if (data.details?.error?.failureType !== kSubtestsFailed) { + ArrayPrototypePush(this.#failedTests, data); } - const error = this.#formatError(data.details?.error, indent) - return `${prefix}${indent}${color}${symbol}${title}${error}${white}\n` - } + return this.#handleTestReportEvent(type, data); + case 'test:pass': + return this.#handleTestReportEvent(type, data); case 'test:start': - ArrayPrototypeUnshift(this.#stack, { __proto__: null, data, type }) - break + ArrayPrototypeUnshift(this.#stack, { __proto__: null, data, type }); + break; + case 'test:stderr': + case 'test:stdout': + return data.message; case 'test:diagnostic': - return `${color}${this.#indent(data.nesting)}${symbol}${data.message}${white}\n` + return `${reporterColorMap[type]}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`; + case 'test:coverage': + return getCoverageReport(indent(data.nesting), data.summary, + reporterUnicodeSymbolMap['test:coverage'], colors.blue, true); } } + _transform({ type, data }, encoding, callback) { + callback(null, this.#handleEvent({ __proto__: null, type, data })); + } + _flush(callback) { + if (this.#failedTests.length === 0) { + callback(null, ''); + return; + } + const results = [`\n${reporterColorMap['test:fail']}${reporterUnicodeSymbolMap['test:fail']}failing tests:${colors.white}\n`]; + for (let i = 0; i < this.#failedTests.length; i++) { + const test = this.#failedTests[i]; + const formattedErr = formatTestReport('test:fail', test); + + if (test.file) { + const relPath = relative(this.#cwd, test.file); + const location = `test at ${relPath}:${test.line}:${test.column}`; - _transform ({ type, data }, encoding, callback) { - callback(null, this.#handleEvent({ type, data })) + ArrayPrototypePush(results, location); + } + + ArrayPrototypePush(results, formattedErr); + } + callback(null, ArrayPrototypeJoin(results, '\n')); } } -module.exports = SpecReporter +module.exports = SpecReporter; diff --git a/lib/internal/test_runner/reporter/tap.js b/lib/internal/test_runner/reporter/tap.js index 8ff91fa..0524a40 100644 --- a/lib/internal/test_runner/reporter/tap.js +++ b/lib/internal/test_runner/reporter/tap.js @@ -1,153 +1,196 @@ -// https://github.com/nodejs/node/blob/2f38c74e263ed2e7f3b087efb9adee2442dd25c4/lib/internal/test_runner/tap_stream.js - -'use strict' +const { primordials, internalBinding } = require('#lib/bootstrap'); +'use strict'; const { ArrayPrototypeForEach, ArrayPrototypeJoin, ArrayPrototypePush, + DatePrototypeToISOString, ObjectEntries, RegExpPrototypeSymbolReplace, + RegExpPrototypeSymbolSplit, SafeMap, + SafeSet, + StringPrototypeRepeat, StringPrototypeReplaceAll, - StringPrototypeSplit, - StringPrototypeRepeat -} = require('#internal/per_context/primordials') -const { inspectWithNoCustomRetry } = require('#internal/errors') -const { isError, kEmptyObject } = require('#internal/util') -const kDefaultIndent = ' ' // 4 spaces -const kFrameStartRegExp = /^ {4}at / -const kLineBreakRegExp = /\n|\r\n/ -const kDefaultTAPVersion = 13 -const inspectOptions = { colors: false, breakLength: Infinity } -let testModule // Lazy loaded due to circular dependency. - -function lazyLoadTest () { - testModule = testModule ?? require('#internal/test_runner/test') - return testModule +} = primordials; +const { inspectWithNoCustomRetry } = require('#internal/errors'); +const { isError, kEmptyObject } = require('#internal/util'); +const { getCoverageReport } = require('#internal/test_runner/utils'); +const kDefaultIndent = ' '; // 4 spaces +const kFrameStartRegExp = /^ {4}at /; +const kLineBreakRegExp = /\n|\r\n/; +const kDefaultTAPVersion = 13; +const inspectOptions = { __proto__: null, colors: false, breakLength: Infinity }; +let testModule; // Lazy loaded due to circular dependency. + +function lazyLoadTest() { + testModule ??= require('#internal/test_runner/test'); + return testModule; } -async function * tapReporter (source) { - yield `TAP version ${kDefaultTAPVersion}\n` + +async function * tapReporter(source) { + yield `TAP version ${kDefaultTAPVersion}\n`; for await (const { type, data } of source) { switch (type) { - case 'test:fail': - yield reportTest(data.nesting, data.testNumber, 'not ok', data.name, data.skip, data.todo) - yield reportDetails(data.nesting, data.details) - break - case 'test:pass': - yield reportTest(data.nesting, data.testNumber, 'ok', data.name, data.skip, data.todo) - yield reportDetails(data.nesting, data.details) - break + case 'test:fail': { + yield reportTest(data.nesting, data.testNumber, 'not ok', data.name, data.skip, data.todo); + const location = data.file ? `${data.file}:${data.line}:${data.column}` : null; + yield reportDetails(data.nesting, data.details, location); + break; + } case 'test:pass': + yield reportTest(data.nesting, data.testNumber, 'ok', data.name, data.skip, data.todo); + yield reportDetails(data.nesting, data.details, null); + break; case 'test:plan': - yield `${indent(data.nesting)}1..${data.count}\n` - break + yield `${indent(data.nesting)}1..${data.count}\n`; + break; case 'test:start': - yield `${indent(data.nesting)}# Subtest: ${tapEscape(data.name)}\n` - break - case 'test:diagnostic': - yield `${indent(data.nesting)}# ${tapEscape(data.message)}\n` - break + yield `${indent(data.nesting)}# Subtest: ${tapEscape(data.name)}\n`; + break; + case 'test:stderr': + case 'test:stdout': { + const lines = RegExpPrototypeSymbolSplit(kLineBreakRegExp, data.message); + for (let i = 0; i < lines.length; i++) { + if (lines[i].length === 0) continue; + yield `# ${tapEscape(lines[i])}\n`; + } + break; + } case 'test:diagnostic': + yield `${indent(data.nesting)}# ${tapEscape(data.message)}\n`; + break; + case 'test:coverage': + yield getCoverageReport(indent(data.nesting), data.summary, '# ', '', true); + break; } } } -function reportTest (nesting, testNumber, status, name, skip, todo) { - let line = `${indent(nesting)}${status} ${testNumber}` +function reportTest(nesting, testNumber, status, name, skip, todo) { + let line = `${indent(nesting)}${status} ${testNumber}`; if (name) { - line += ` ${tapEscape(`- ${name}`)}` + line += ` ${tapEscape(`- ${name}`)}`; } if (skip !== undefined) { - line += ` # SKIP${typeof skip === 'string' && skip.length ? ` ${tapEscape(skip)}` : ''}` + line += ` # SKIP${typeof skip === 'string' && skip.length ? ` ${tapEscape(skip)}` : ''}`; } else if (todo !== undefined) { - line += ` # TODO${typeof todo === 'string' && todo.length ? ` ${tapEscape(todo)}` : ''}` + line += ` # TODO${typeof todo === 'string' && todo.length ? ` ${tapEscape(todo)}` : ''}`; } - line += '\n' + line += '\n'; - return line + return line; } -function reportDetails (nesting, data = kEmptyObject) { - const { error, duration_ms } = data // eslint-disable-line camelcase - const _indent = indent(nesting) - let details = `${_indent} ---\n` +function reportDetails(nesting, data = kEmptyObject, location) { + const { error, duration_ms } = data; + const _indent = indent(nesting); + let details = `${_indent} ---\n`; + + details += jsToYaml(_indent, 'duration_ms', duration_ms); + details += jsToYaml(_indent, 'type', data.type); + + if (location) { + details += jsToYaml(_indent, 'location', location); + } - details += jsToYaml(_indent, 'duration_ms', duration_ms) - details += jsToYaml(_indent, null, error) - details += `${_indent} ...\n` - return details + details += jsToYaml(_indent, null, error, new SafeSet()); + details += `${_indent} ...\n`; + return details; } -const memo = new SafeMap() -function indent (nesting) { - let value = memo.get(nesting) +const memo = new SafeMap(); +function indent(nesting) { + let value = memo.get(nesting); if (value === undefined) { - value = StringPrototypeRepeat(kDefaultIndent, nesting) - memo.set(nesting, value) + value = StringPrototypeRepeat(kDefaultIndent, nesting); + memo.set(nesting, value); } - return value + return value; } + // In certain places, # and \ need to be escaped as \# and \\. -function tapEscape (input) { - let result = StringPrototypeReplaceAll(input, '\b', '\\b') - result = StringPrototypeReplaceAll(result, '\f', '\\f') - result = StringPrototypeReplaceAll(result, '\t', '\\t') - result = StringPrototypeReplaceAll(result, '\n', '\\n') - result = StringPrototypeReplaceAll(result, '\r', '\\r') - result = StringPrototypeReplaceAll(result, '\v', '\\v') - result = StringPrototypeReplaceAll(result, '\\', '\\\\') - result = StringPrototypeReplaceAll(result, '#', '\\#') - return result +function tapEscape(input) { + let result = StringPrototypeReplaceAll(input, '\b', '\\b'); + result = StringPrototypeReplaceAll(result, '\f', '\\f'); + result = StringPrototypeReplaceAll(result, '\t', '\\t'); + result = StringPrototypeReplaceAll(result, '\n', '\\n'); + result = StringPrototypeReplaceAll(result, '\r', '\\r'); + result = StringPrototypeReplaceAll(result, '\v', '\\v'); + result = StringPrototypeReplaceAll(result, '\\', '\\\\'); + result = StringPrototypeReplaceAll(result, '#', '\\#'); + return result; } -function jsToYaml (indent, name, value) { - if (value === null || value === undefined) { - return '' +function jsToYaml(indent, name, value, seen) { + if (value === undefined) { + return ''; } - if (typeof value !== 'object') { - const prefix = `${indent} ${name}: ` + const prefix = `${indent} ${name}:`; + if (value === null) { + return `${prefix} ~\n`; + } + + if (typeof value !== 'object') { if (typeof value !== 'string') { - return `${prefix}${inspectWithNoCustomRetry(value, inspectOptions)}\n` + return `${prefix} ${inspectWithNoCustomRetry(value, inspectOptions)}\n`; } - const lines = StringPrototypeSplit(value, kLineBreakRegExp) + const lines = RegExpPrototypeSymbolSplit(kLineBreakRegExp, value); if (lines.length === 1) { - return `${prefix}${inspectWithNoCustomRetry(value, inspectOptions)}\n` + return `${prefix} ${inspectWithNoCustomRetry(value, inspectOptions)}\n`; } - let str = `${prefix}|-\n` + let str = `${prefix} |-\n`; for (let i = 0; i < lines.length; i++) { - str += `${indent} ${lines[i]}\n` + str += `${indent} ${lines[i]}\n`; } - return str + return str; } - const entries = ObjectEntries(value) - const isErrorObj = isError(value) - let result = '' + seen.add(value); + const entries = ObjectEntries(value); + const isErrorObj = isError(value); + let propsIndent = indent; + let result = ''; + + if (name != null) { + result += prefix; + console.log(value) + if (internalBinding('types').isDate(value)) { + // YAML uses the ISO-8601 standard to express dates. + result += ' ' + DatePrototypeToISOString(value); + } + result += '\n'; + propsIndent += ' '; + } for (let i = 0; i < entries.length; i++) { - const { 0: key, 1: value } = entries[i] + const { 0: key, 1: value } = entries[i]; if (isErrorObj && (key === 'cause' || key === 'code')) { - continue + continue; + } + if (seen.has(value)) { + result += `${propsIndent} ${key}: \n`; + continue; } - result += jsToYaml(indent, key, value) + result += jsToYaml(propsIndent, key, value, seen); } if (isErrorObj) { - const { kTestCodeFailure, kUnwrapErrors } = lazyLoadTest() + const { kUnwrapErrors } = lazyLoadTest(); const { cause, code, @@ -156,78 +199,86 @@ function jsToYaml (indent, name, value) { expected, actual, operator, - stack - } = value - let errMsg = message ?? '' - let errStack = stack - let errCode = code - let errExpected = expected - let errActual = actual - let errOperator = operator - let errIsAssertion = isAssertionLike(value) + stack, + name, + } = value; + let errMsg = message ?? ''; + let errName = name; + let errStack = stack; + let errCode = code; + let errExpected = expected; + let errActual = actual; + let errOperator = operator; + let errIsAssertion = isAssertionLike(value); // If the ERR_TEST_FAILURE came from an error provided by user code, // then try to unwrap the original error message and stack. if (code === 'ERR_TEST_FAILURE' && kUnwrapErrors.has(failureType)) { - errStack = cause?.stack ?? errStack - errCode = cause?.code ?? errCode + errStack = cause?.stack ?? errStack; + errCode = cause?.code ?? errCode; + errName = cause?.name ?? errName; + errMsg = cause?.message ?? errMsg; + if (isAssertionLike(cause)) { - errExpected = cause.expected - errActual = cause.actual - errOperator = cause.operator ?? errOperator - errIsAssertion = true - } - if (failureType === kTestCodeFailure) { - errMsg = cause?.message ?? errMsg + errExpected = cause.expected; + errActual = cause.actual; + errOperator = cause.operator ?? errOperator; + errIsAssertion = true; } } - result += jsToYaml(indent, 'error', errMsg) + result += jsToYaml(indent, 'error', errMsg, seen); if (errCode) { - result += jsToYaml(indent, 'code', errCode) + result += jsToYaml(indent, 'code', errCode, seen); + } + if (errName && errName !== 'Error') { + result += jsToYaml(indent, 'name', errName, seen); } if (errIsAssertion) { - result += jsToYaml(indent, 'expected', errExpected) - result += jsToYaml(indent, 'actual', errActual) + // Note that we're deliberately creating shallow copies of the `seen` + // set here in order to isolate the discovery of circular references + // within the expected and actual properties respectively. + result += jsToYaml(indent, 'expected', errExpected, new SafeSet(seen)); + result += jsToYaml(indent, 'actual', errActual, new SafeSet(seen)); if (errOperator) { - result += jsToYaml(indent, 'operator', errOperator) + result += jsToYaml(indent, 'operator', errOperator, seen); } } if (typeof errStack === 'string') { - const frames = [] + const frames = []; ArrayPrototypeForEach( - StringPrototypeSplit(errStack, kLineBreakRegExp), + RegExpPrototypeSymbolSplit(kLineBreakRegExp, errStack), (frame) => { const processed = RegExpPrototypeSymbolReplace( kFrameStartRegExp, frame, - '' - ) + '', + ); if (processed.length > 0 && processed.length !== frame.length) { - ArrayPrototypePush(frames, processed) + ArrayPrototypePush(frames, processed); } - } - ) + }, + ); if (frames.length > 0) { - const frameDelimiter = `\n${indent} ` + const frameDelimiter = `\n${indent} `; - result += `${indent} stack: |-${frameDelimiter}` - result += `${ArrayPrototypeJoin(frames, frameDelimiter)}\n` + result += `${indent} stack: |-${frameDelimiter}`; + result += `${ArrayPrototypeJoin(frames, frameDelimiter)}\n`; } } } - return result + return result; } -function isAssertionLike (value) { - return value && typeof value === 'object' && 'expected' in value && 'actual' in value +function isAssertionLike(value) { + return value && typeof value === 'object' && 'expected' in value && 'actual' in value; } -module.exports = tapReporter +module.exports = tapReporter; diff --git a/lib/internal/test_runner/reporter/utils.js b/lib/internal/test_runner/reporter/utils.js new file mode 100644 index 0000000..ad40dbd --- /dev/null +++ b/lib/internal/test_runner/reporter/utils.js @@ -0,0 +1,93 @@ +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; +const { + ArrayPrototypeJoin, + RegExpPrototypeSymbolSplit, + SafeMap, + StringPrototypeRepeat, + hardenRegExp, +} = primordials; +const colors = require('#internal/util/colors'); +const { inspectWithNoCustomRetry } = require('#internal/errors'); +const indentMemo = new SafeMap(); + +const inspectOptions = { + __proto__: null, + colors: colors.shouldColorize(process.stdout), + breakLength: Infinity, +}; + +const reporterUnicodeSymbolMap = { + '__proto__': null, + 'test:fail': '\u2716 ', + 'test:pass': '\u2714 ', + 'test:diagnostic': '\u2139 ', + 'test:coverage': '\u2139 ', + 'arrow:right': '\u25B6 ', + 'hyphen:minus': '\uFE63 ', +}; + +const reporterColorMap = { + '__proto__': null, + get 'test:fail'() { + return colors.red; + }, + get 'test:pass'() { + return colors.green; + }, + get 'test:diagnostic'() { + return colors.blue; + }, +}; + +function indent(nesting) { + let value = indentMemo.get(nesting); + if (value === undefined) { + value = StringPrototypeRepeat(' ', nesting); + indentMemo.set(nesting, value); + } + return value; +} + +function formatError(error, indent) { + if (!error) return ''; + const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error; + const message = ArrayPrototypeJoin( + RegExpPrototypeSymbolSplit( + hardenRegExp(/\r?\n/), + inspectWithNoCustomRetry(err, inspectOptions), + ), `\n${indent} `); + return `\n${indent} ${message}\n`; +} + +function formatTestReport(type, data, prefix = '', indent = '', hasChildren = false) { + let color = reporterColorMap[type] ?? colors.white; + let symbol = reporterUnicodeSymbolMap[type] ?? ' '; + const { skip, todo } = data; + const duration_ms = data.details?.duration_ms ? ` ${colors.gray}(${data.details.duration_ms}ms)${colors.white}` : ''; + let title = `${data.name}${duration_ms}`; + + if (skip !== undefined) { + title += ` # ${typeof skip === 'string' && skip.length ? skip : 'SKIP'}`; + } else if (todo !== undefined) { + title += ` # ${typeof todo === 'string' && todo.length ? todo : 'TODO'}`; + } + const error = formatError(data.details?.error, indent); + const err = hasChildren ? + (!error || data.details?.error?.failureType === 'subtestsFailed' ? '' : `\n${error}`) : + error; + if (skip !== undefined) { + color = colors.gray; + symbol = reporterUnicodeSymbolMap['hyphen:minus']; + } + return `${prefix}${indent}${color}${symbol}${title}${colors.white}${err}`; +} + +module.exports = { + __proto__: null, + reporterUnicodeSymbolMap, + reporterColorMap, + formatTestReport, + indent, +}; diff --git a/lib/internal/test_runner/reporter/v8-serializer.js b/lib/internal/test_runner/reporter/v8-serializer.js new file mode 100644 index 0000000..1fadc20 --- /dev/null +++ b/lib/internal/test_runner/reporter/v8-serializer.js @@ -0,0 +1,47 @@ +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; + +const { + TypedArrayPrototypeGetLength, +} = primordials; +const { DefaultSerializer } = require('v8'); +const { Buffer } = require('buffer'); +const { serializeError } = require('#internal/error_serdes'); + + +module.exports = async function* v8Reporter(source) { + const serializer = new DefaultSerializer(); + serializer.writeHeader(); + const headerLength = TypedArrayPrototypeGetLength(serializer.releaseBuffer()); + + for await (const item of source) { + const originalError = item.data.details?.error; + if (originalError) { + // Error is overridden with a serialized version, so that it can be + // deserialized in the parent process. + // Error is restored after serialization. + item.data.details.error = serializeError(originalError); + } + serializer.writeHeader(); + // Add 4 bytes, to later populate with message length + serializer.writeRawBytes(Buffer.allocUnsafe(4)); + serializer.writeHeader(); + serializer.writeValue(item); + + if (originalError) { + item.data.details.error = originalError; + } + + const serializedMessage = serializer.releaseBuffer(); + const serializedMessageLength = serializedMessage.length - (4 + headerLength); + + serializedMessage.set([ + serializedMessageLength >> 24 & 0xFF, + serializedMessageLength >> 16 & 0xFF, + serializedMessageLength >> 8 & 0xFF, + serializedMessageLength & 0xFF, + ], headerLength); + yield serializedMessage; + } +}; diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 898d319..8cc2765 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -1,304 +1,815 @@ -// https://github.com/nodejs/node/blob/1118db718c8429f5f343aca90ccb570244e282b4/lib/internal/test_runner/runner.js -'use strict' +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; + const { - ArrayFrom, + ArrayIsArray, + ArrayPrototypeEvery, ArrayPrototypeFilter, + ArrayPrototypeFind, ArrayPrototypeForEach, ArrayPrototypeIncludes, + ArrayPrototypeJoin, + ArrayPrototypeMap, ArrayPrototypePush, + ArrayPrototypePushApply, + ArrayPrototypeShift, ArrayPrototypeSlice, ArrayPrototypeSome, ArrayPrototypeSort, ObjectAssign, PromisePrototypeThen, + PromiseResolve, + PromiseWithResolvers, + SafeMap, SafePromiseAll, + SafePromiseAllReturnVoid, + SafePromiseAllSettledReturnVoid, SafeSet, StringPrototypeIndexOf, StringPrototypeSlice, - StringPrototypeStartsWith -} = require('#internal/per_context/primordials') - -const { spawn } = require('child_process') -const { readdirSync, statSync } = require('fs') -// TODO(aduh95): switch to internal/readline/interface when backporting to Node.js 16.x is no longer a concern. -const { createInterface } = require('readline') + StringPrototypeStartsWith, + Symbol, + TypedArrayPrototypeGetLength, + TypedArrayPrototypeSubarray, +} = primordials; + +const { spawn } = require('child_process'); +const { finished } = require('#internal/streams/end-of-stream'); +const { resolve, sep, isAbsolute } = require('path'); +const { DefaultDeserializer, DefaultSerializer } = require('v8'); +/* NOTE(RedYetiDev): The following line has been modified for node-core-test */ +const { getOptionValue, parsedAsOptions } = require('#internal/options'); +const { Interface } = require('#internal/readline/interface'); +const { deserializeError } = require('#internal/error_serdes'); +const { Buffer } = require('buffer'); +const { FilesWatcher } = require('#internal/watch_mode/files_watcher'); +const console = require('#internal/console/global'); const { codes: { - ERR_TEST_FAILURE - } -} = require('#internal/errors') -const { toArray } = require('readable-stream/lib/internal/streams/operators').promiseReturningOperators -const { validateArray } = require('#internal/validators') -const { getInspectPort, isUsingInspector, isInspectorMessage } = require('#internal/util/inspector') -const { kEmptyObject } = require('#internal/util') -const { createTestTree } = require('#internal/test_runner/harness') -const { kSubtestsFailed, Test } = require('#internal/test_runner/test') -const { TapParser } = require('#internal/test_runner/tap_parser') -const { YAMLToJs } = require('#internal/test_runner/yaml_to_js') -const { TokenKind } = require('#internal/test_runner/tap_lexer') + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_TEST_FAILURE, + }, +} = require('#internal/errors'); +const esmLoader = require('#internal/modules/esm/loader'); const { - isSupportedFileType, - doesPathMatchFilter -} = require('#internal/test_runner/utils') -const { basename, join, resolve } = require('path') -const { once } = require('events') - -const kFilterArgs = ['--test'] -const kFilterArgValues = ['--test-reporter', '--test-reporter-destination'] -const kDiagnosticsFilterArgs = ['tests', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms'] - -// TODO(cjihrig): Replace this with recursive readdir once it lands. -function processPath (path, testFiles, options) { - const stats = statSync(path) - - if (stats.isFile()) { - if (options.userSupplied || - (options.underTestDir && isSupportedFileType(path)) || - doesPathMatchFilter(path)) { - testFiles.add(path) - } - } else if (stats.isDirectory()) { - const name = basename(path) - - if (!options.userSupplied && name === 'node_modules') { - return - } - - // 'test' directories get special treatment. Recursively add all .js, - // .cjs, and .mjs files in the 'test' directory. - const isTestDir = name === 'test' - const { underTestDir } = options - const entries = readdirSync(path) - - if (isTestDir) { - options.underTestDir = true - } - - options.userSupplied = false - - for (let i = 0; i < entries.length; i++) { - processPath(join(path, entries[i]), testFiles, options) - } + validateArray, + validateBoolean, + validateFunction, + validateObject, + validateOneOf, + validateInteger, + validateString, + validateStringArray, +} = require('#internal/validators'); +const { getInspectPort, isUsingInspector, isInspectorMessage } = require('#internal/util/inspector'); +const { isRegExp } = require('#internal/util/types'); +const { pathToFileURL } = require('#internal/url'); +const { + kEmptyObject, +} = require('#internal/util'); +const { kEmitMessage } = require('#internal/test_runner/tests_stream'); +const { + createTestTree, + startSubtestAfterBootstrap, +} = require('#internal/test_runner/harness'); +const { + kAborted, + kCancelledByParent, + kSubtestsFailed, + kTestCodeFailure, + kTestTimeoutFailure, + Test, +} = require('#internal/test_runner/test'); - options.underTestDir = underTestDir +const { + convertStringToRegExp, + countCompletedTest, + kDefaultPattern, + parseCommandLine, +} = require('#internal/test_runner/utils'); +const { Glob } = require('#internal/fs/glob'); +const { once } = require('events'); +const { + triggerUncaughtException, + exitCodes: { kGenericUserError }, +} = internalBinding('errors'); +let debug = require('#internal/util/debuglog').debuglog('test_runner', (fn) => { + debug = fn; +}); + +const kIsolatedProcessName = Symbol('kIsolatedProcessName'); +const kFilterArgs = ['--test', '--experimental-test-coverage', '--watch']; +const kFilterArgValues = ['--test-reporter', '--test-reporter-destination']; +const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms']; + +const kCanceledTests = new SafeSet() + .add(kCancelledByParent).add(kAborted).add(kTestTimeoutFailure); + +let kResistStopPropagation; + +function createTestFileList(patterns, cwd) { + const hasUserSuppliedPattern = patterns != null; + if (!patterns || patterns.length === 0) { + patterns = [kDefaultPattern]; } -} - -function createTestFileList () { - const cwd = process.cwd() - const hasUserSuppliedPaths = process.argv.length > 1 - const testPaths = hasUserSuppliedPaths - ? ArrayPrototypeSlice(process.argv, 1) - : [cwd] - const testFiles = new SafeSet() - - try { - for (let i = 0; i < testPaths.length; i++) { - const absolutePath = resolve(testPaths[i]) - - processPath(absolutePath, testFiles, { userSupplied: true }) - } - } catch (err) { - if (err?.code === 'ENOENT') { - console.error(`Could not find '${err.path}'`) - process.exit(1) - } - - throw err + const glob = new Glob(patterns, { + __proto__: null, + cwd, + exclude: (name) => name === 'node_modules', + }); + const results = glob.globSync(); + + if (hasUserSuppliedPattern && results.length === 0 && ArrayPrototypeEvery(glob.matchers, (m) => !m.hasMagic())) { + console.error(`Could not find '${ArrayPrototypeJoin(patterns, ', ')}'`); + process.exit(kGenericUserError); } - return ArrayPrototypeSort(ArrayFrom(testFiles)) + return ArrayPrototypeSort(results); } -function filterExecArgv (arg, i, arr) { +function filterExecArgv(arg, i, arr) { return !ArrayPrototypeIncludes(kFilterArgs, arg) && - !ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`)) + !ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`)); } -function getRunArgs ({ path, inspectPort }) { - const argv = ArrayPrototypeFilter(process.execArgv, filterExecArgv) +function getRunArgs(path, { forceExit, + inspectPort, + testNamePatterns, + testSkipPatterns, + only, + argv: suppliedArgs, + execArgv, + cwd }) { + const argv = ArrayPrototypeFilter(process.execArgv, filterExecArgv); + if (forceExit === true) { + ArrayPrototypePush(argv, '--test-force-exit'); + } if (isUsingInspector()) { - ArrayPrototypePush(argv, `--inspect-port=${getInspectPort(inspectPort)}`) + ArrayPrototypePush(argv, `--inspect-port=${getInspectPort(inspectPort)}`); + } + if (testNamePatterns != null) { + ArrayPrototypeForEach(testNamePatterns, (pattern) => ArrayPrototypePush(argv, `--test-name-pattern=${pattern}`)); + } + if (testSkipPatterns != null) { + ArrayPrototypeForEach(testSkipPatterns, (pattern) => ArrayPrototypePush(argv, `--test-skip-pattern=${pattern}`)); + } + if (only === true) { + ArrayPrototypePush(argv, '--test-only'); + } + + ArrayPrototypePushApply(argv, execArgv); + + if (path === kIsolatedProcessName) { + ArrayPrototypePush(argv, '--test'); + ArrayPrototypePushApply(argv, ArrayPrototypeSlice(process.argv, 1)); + } else { + ArrayPrototypePush(argv, path); } - ArrayPrototypePush(argv, path) - return argv + ArrayPrototypePushApply(argv, suppliedArgs); + + /* NOTE(RedYetiDev): The following line has been modified for node-core-test */ + return argv.concat(parsedAsOptions.filter((a) => filterExecArgv(a))); } +const serializer = new DefaultSerializer(); +serializer.writeHeader(); +const v8Header = serializer.releaseBuffer(); +const kV8HeaderLength = TypedArrayPrototypeGetLength(v8Header); +const kSerializedSizeHeader = 4 + kV8HeaderLength; + class FileTest extends Test { - #buffer = [] - #checkNestedComment ({ comment }) { - const firstSpaceIndex = StringPrototypeIndexOf(comment, ' ') - if (firstSpaceIndex === -1) return false - const secondSpaceIndex = StringPrototypeIndexOf(comment, ' ', firstSpaceIndex + 1) + // This class maintains two buffers: + #reportBuffer = []; // Parsed items waiting for this.isClearToSend() + #rawBuffer = []; // Raw data waiting to be parsed + #rawBufferSize = 0; + #reportedChildren = 0; + failedSubtests = false; + + constructor(options) { + super(options); + this.loc ??= { + __proto__: null, + line: 1, + column: 1, + file: resolve(this.name), + }; + } + + #skipReporting() { + return this.#reportedChildren > 0 && (!this.error || this.error.failureType === kSubtestsFailed); + } + #checkNestedComment(comment) { + const firstSpaceIndex = StringPrototypeIndexOf(comment, ' '); + if (firstSpaceIndex === -1) return false; + const secondSpaceIndex = StringPrototypeIndexOf(comment, ' ', firstSpaceIndex + 1); return secondSpaceIndex === -1 && - ArrayPrototypeIncludes(kDiagnosticsFilterArgs, StringPrototypeSlice(comment, 0, firstSpaceIndex)) + ArrayPrototypeIncludes(kDiagnosticsFilterArgs, StringPrototypeSlice(comment, 0, firstSpaceIndex)); + } + #handleReportItem(item) { + const isTopLevel = item.data.nesting === 0; + if (isTopLevel) { + if (item.type === 'test:plan' && this.#skipReporting()) { + return; + } + if (item.type === 'test:diagnostic' && this.#checkNestedComment(item.data.message)) { + return; + } + } + if (item.data.details?.error) { + item.data.details.error = deserializeError(item.data.details.error); + } + if (item.type === 'test:pass' || item.type === 'test:fail') { + item.data.testNumber = isTopLevel ? (this.root.harness.counters.topLevel + 1) : item.data.testNumber; + countCompletedTest({ + __proto__: null, + name: item.data.name, + finished: true, + skipped: item.data.skip !== undefined, + isTodo: item.data.todo !== undefined, + passed: item.type === 'test:pass', + cancelled: kCanceledTests.has(item.data.details?.error?.failureType), + nesting: item.data.nesting, + reportedType: item.data.details?.type, + }, this.root.harness); + } + this.reporter[kEmitMessage](item.type, item.data); + } + #accumulateReportItem(item) { + if (item.type !== 'test:pass' && item.type !== 'test:fail') { + return; + } + this.#reportedChildren++; + if (item.data.nesting === 0 && item.type === 'test:fail') { + this.failedSubtests = true; + } + } + #drainReportBuffer() { + if (this.#reportBuffer.length > 0) { + ArrayPrototypeForEach(this.#reportBuffer, (ast) => this.#handleReportItem(ast)); + this.#reportBuffer = []; + } + } + addToReport(item) { + this.#accumulateReportItem(item); + if (!this.isClearToSend()) { + ArrayPrototypePush(this.#reportBuffer, item); + return; + } + this.#drainReportBuffer(); + this.#handleReportItem(item); + } + reportStarted() {} + drain() { + this.#drainRawBuffer(); + this.#drainReportBuffer(); } + report() { + this.drain(); + const skipReporting = this.#skipReporting(); + if (!skipReporting) { + super.reportStarted(); + super.report(); + } + } + parseMessage(readData) { + let dataLength = TypedArrayPrototypeGetLength(readData); + if (dataLength === 0) return; + const partialV8Header = readData[dataLength - 1] === v8Header[0]; + + if (partialV8Header) { + // This will break if v8Header length (2 bytes) is changed. + // However it is covered by tests. + readData = TypedArrayPrototypeSubarray(readData, 0, dataLength - 1); + dataLength--; + } - #handleReportItem ({ kind, node, comments, nesting = 0 }) { - nesting += 1 + if (this.#rawBuffer[0] && TypedArrayPrototypeGetLength(this.#rawBuffer[0]) < kSerializedSizeHeader) { + this.#rawBuffer[0] = Buffer.concat([this.#rawBuffer[0], readData]); + } else { + ArrayPrototypePush(this.#rawBuffer, readData); + } + this.#rawBufferSize += dataLength; + this.#processRawBuffer(); - if (comments) { - ArrayPrototypeForEach(comments, (comment) => this.reporter.diagnostic(nesting, this.name, comment)) + if (partialV8Header) { + ArrayPrototypePush(this.#rawBuffer, TypedArrayPrototypeSubarray(v8Header, 0, 1)); + this.#rawBufferSize++; + } + } + #drainRawBuffer() { + while (this.#rawBuffer.length > 0) { + this.#processRawBuffer(); + } + } + #processRawBuffer() { + // This method is called when it is known that there is at least one message + let bufferHead = this.#rawBuffer[0]; + let headerIndex = bufferHead.indexOf(v8Header); + let nonSerialized = Buffer.alloc(0); + + while (bufferHead && headerIndex !== 0) { + const nonSerializedData = headerIndex === -1 ? + bufferHead : + bufferHead.slice(0, headerIndex); + nonSerialized = Buffer.concat([nonSerialized, nonSerializedData]); + this.#rawBufferSize -= TypedArrayPrototypeGetLength(nonSerializedData); + if (headerIndex === -1) { + ArrayPrototypeShift(this.#rawBuffer); + } else { + this.#rawBuffer[0] = TypedArrayPrototypeSubarray(bufferHead, headerIndex); + } + bufferHead = this.#rawBuffer[0]; + headerIndex = bufferHead?.indexOf(v8Header); } - switch (kind) { - case TokenKind.TAP_VERSION: - // TODO(manekinekko): handle TAP version coming from the parser. - // this.reporter.version(node.version); - break - case TokenKind.TAP_PLAN: - this.reporter.plan(nesting, this.name, node.end - node.start + 1) - break + if (TypedArrayPrototypeGetLength(nonSerialized) > 0) { + this.addToReport({ + __proto__: null, + type: 'test:stdout', + data: { __proto__: null, file: this.name, message: nonSerialized.toString('utf-8') }, + }); + } - case TokenKind.TAP_SUBTEST_POINT: - this.reporter.start(nesting, this.name, node.name) - break + while (bufferHead?.length >= kSerializedSizeHeader) { + // We call `readUInt32BE` manually here, because this is faster than first converting + // it to a buffer and using `readUInt32BE` on that. + const fullMessageSize = ( + bufferHead[kV8HeaderLength] << 24 | + bufferHead[kV8HeaderLength + 1] << 16 | + bufferHead[kV8HeaderLength + 2] << 8 | + bufferHead[kV8HeaderLength + 3] + ) + kSerializedSizeHeader; - case TokenKind.TAP_TEST_POINT: - // eslint-disable-next-line no-case-declarations - const { todo, skip, pass } = node.status - // eslint-disable-next-line no-case-declarations - let directive + if (this.#rawBufferSize < fullMessageSize) break; - if (skip) { - directive = this.reporter.getSkip(node.reason) - } else if (todo) { - directive = this.reporter.getTodo(node.reason) - } else { - directive = kEmptyObject - } + const concatenatedBuffer = this.#rawBuffer.length === 1 ? + this.#rawBuffer[0] : Buffer.concat(this.#rawBuffer, this.#rawBufferSize); - if (pass) { - this.reporter.ok( - nesting, - this.name, - node.id, - node.description, - YAMLToJs(node.diagnostics), - directive - ) - } else { - this.reporter.fail( - nesting, - this.name, - node.id, - node.description, - YAMLToJs(node.diagnostics), - directive - ) - } - break + const deserializer = new DefaultDeserializer( + TypedArrayPrototypeSubarray(concatenatedBuffer, kSerializedSizeHeader, fullMessageSize), + ); - case TokenKind.COMMENT: - if (nesting === 1 && this.#checkNestedComment(node)) { - // Ignore file top level diagnostics - break - } - this.reporter.diagnostic(nesting, this.name, node.comment) - break + bufferHead = TypedArrayPrototypeSubarray(concatenatedBuffer, fullMessageSize); + this.#rawBufferSize = TypedArrayPrototypeGetLength(bufferHead); + this.#rawBuffer = this.#rawBufferSize !== 0 ? [bufferHead] : []; - case TokenKind.UNKNOWN: - this.reporter.diagnostic(nesting, this.name, node.value) - break + deserializer.readHeader(); + const item = deserializer.readValue(); + this.addToReport(item); } } - - addToReport (ast) { - if (!this.isClearToSend()) { - ArrayPrototypePush(this.#buffer, ast) - return - } - this.reportStarted() - this.#handleReportItem(ast) - } - - report () { - this.reportStarted() - ArrayPrototypeForEach(this.#buffer, (ast) => this.#handleReportItem(ast)) - super.report() - } } -function runTestFile (path, root, inspectPort, filesWatcher) { - const subtest = root.createSubtest(FileTest, path, async (t) => { - const args = getRunArgs({ path, inspectPort }) - const stdio = ['pipe', 'pipe', 'pipe'] - const env = { ...process.env } - if (filesWatcher) { - stdio.push('ipc') - env.WATCH_REPORT_DEPENDENCIES = '1' +function runTestFile(path, filesWatcher, opts) { + const watchMode = filesWatcher != null; + const testPath = path === kIsolatedProcessName ? '' : path; + const testOpts = { __proto__: null, signal: opts.signal }; + const subtest = opts.root.createSubtest(FileTest, testPath, testOpts, async (t) => { + const args = getRunArgs(path, opts); + const stdio = ['pipe', 'pipe', 'pipe']; + const env = { __proto__: null, ...process.env, NODE_TEST_CONTEXT: 'child-v8' }; + if (watchMode) { + stdio.push('ipc'); + env.WATCH_REPORT_DEPENDENCIES = '1'; + } + if (opts.root.harness.shouldColorizeTestFiles) { + env.FORCE_COLOR = '1'; } - const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8', env, stdio }) - - let err + const child = spawn( + process.execPath, args, + { + __proto__: null, + signal: t.signal, + encoding: 'utf8', + env, + stdio, + cwd: opts.cwd, + }, + ); + if (watchMode) { + filesWatcher.runningProcesses.set(path, child); + filesWatcher.watcher.watchChildProcessModules(child, path); + } - filesWatcher?.watchChildProcessModules(child, path) + let err; child.on('error', (error) => { - err = error - }) - - if (isUsingInspector()) { - const rl = createInterface({ input: child.stderr }) - rl.on('line', (line) => { - if (isInspectorMessage(line)) { - process.stderr.write(line + '\n') - } - }) - } - - const parser = new TapParser() - child.stderr.pipe(parser).on('data', (ast) => { - if (ast.lexeme && isInspectorMessage(ast.lexeme)) { - process.stderr.write(ast.lexeme + '\n') + err = error; + }); + + child.stdout.on('data', (data) => { + subtest.parseMessage(data); + }); + + const rl = new Interface({ __proto__: null, input: child.stderr }); + rl.on('line', (line) => { + if (isInspectorMessage(line)) { + process.stderr.write(line + '\n'); + return; } - }) - child.stdout.pipe(parser).on('data', (ast) => { - subtest.addToReport(ast) - }) + // stderr cannot be treated as TAP, per the spec. However, we want to + // surface stderr lines to improve the DX. Inject each line into the + // test output as an unknown token as if it came from the TAP parser. + subtest.addToReport({ + __proto__: null, + type: 'test:stderr', + data: { __proto__: null, file: path, message: line + '\n' }, + }); + }); const { 0: { 0: code, 1: signal } } = await SafePromiseAll([ - once(child, 'exit', { signal: t.signal }), - toArray.call(child.stdout, { signal: t.signal }) - ]) + once(child, 'exit', { __proto__: null, signal: t.signal }), + finished(child.stdout, { __proto__: null, signal: t.signal }), + ]); + + if (watchMode) { + filesWatcher.runningProcesses.delete(path); + filesWatcher.runningSubtests.delete(path); + (async () => { + try { + await subTestEnded; + } finally { + if (filesWatcher.runningSubtests.size === 0) { + opts.root.reporter[kEmitMessage]('test:watch:drained'); + opts.root.postRun(); + } + } + })(); + } if (code !== 0 || signal !== null) { if (!err) { - err = ObjectAssign(new ERR_TEST_FAILURE('test failed', kSubtestsFailed), { + const failureType = subtest.failedSubtests ? kSubtestsFailed : kTestCodeFailure; + err = ObjectAssign(new ERR_TEST_FAILURE('test failed', failureType), { __proto__: null, exitCode: code, - signal, + signal: signal, // The stack will not be useful since the failures came from tests // in a child process. - stack: undefined - }) + stack: undefined, + }); } - - throw err + throw err; } - }) - return subtest.start() + }); + const subTestEnded = subtest.start(); + return subTestEnded; } -function run (options) { - if (options === null || typeof options !== 'object') { - options = kEmptyObject +function watchFiles(testFiles, opts) { + const runningProcesses = new SafeMap(); + const runningSubtests = new SafeMap(); + const watcherMode = opts.hasFiles ? 'filter' : 'all'; + const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: watcherMode, signal: opts.signal }); + if (!opts.hasFiles) { + watcher.watchPath(opts.cwd); + } + const filesWatcher = { __proto__: null, watcher, runningProcesses, runningSubtests }; + opts.root.harness.watching = true; + + async function restartTestFile(file) { + const runningProcess = runningProcesses.get(file); + if (runningProcess) { + runningProcess.kill(); + await once(runningProcess, 'exit'); + } + if (!runningSubtests.size) { + // Reset the topLevel counter + opts.root.harness.counters.topLevel = 0; + } + await runningSubtests.get(file); + runningSubtests.set(file, runTestFile(file, filesWatcher, opts)); + } + + // Watch for changes in current filtered files + watcher.on('changed', ({ owners, eventType }) => { + if (!opts.hasFiles && (eventType === 'rename' || eventType === 'change')) { + const updatedTestFiles = createTestFileList(opts.globPatterns, opts.cwd); + const newFileName = ArrayPrototypeFind(updatedTestFiles, (x) => !ArrayPrototypeIncludes(testFiles, x)); + const previousFileName = ArrayPrototypeFind(testFiles, (x) => !ArrayPrototypeIncludes(updatedTestFiles, x)); + + testFiles = updatedTestFiles; + + // When file renamed (created / deleted) we need to update the watcher + if (newFileName) { + owners = new SafeSet().add(newFileName); + const resolveFileName = isAbsolute(newFileName) ? newFileName : resolve(opts.cwd, newFileName); + watcher.filterFile(resolveFileName, owners); + } + + if (!newFileName && previousFileName) { + return; // Avoid rerunning files when file deleted + } + } + + if (opts.isolation === 'none') { + PromisePrototypeThen(restartTestFile(kIsolatedProcessName), undefined, (error) => { + triggerUncaughtException(error, true /* fromPromise */); + }); + } else { + watcher.unfilterFilesOwnedBy(owners); + PromisePrototypeThen(SafePromiseAllReturnVoid(testFiles, async (file) => { + if (!owners.has(file)) { + return; + } + + await restartTestFile(file); + }, undefined, (error) => { + triggerUncaughtException(error, true /* fromPromise */); + })); + } + }); + if (opts.signal) { + kResistStopPropagation ??= require('#internal/event_target').kResistStopPropagation; + opts.signal.addEventListener( + 'abort', + () => { + opts.root.harness.watching = false; + opts.root.postRun(); + }, + { __proto__: null, once: true, [kResistStopPropagation]: true }, + ); } - const { concurrency, timeout, signal, files, inspectPort } = options + + return filesWatcher; +} + +function run(options = kEmptyObject) { + validateObject(options, 'options'); + + let { + testNamePatterns, + testSkipPatterns, + shard, + coverageExcludeGlobs, + coverageIncludeGlobs, + } = options; + const { + concurrency, + timeout, + signal, + files, + forceExit, + inspectPort, + isolation = 'process', + watch, + setup, + only, + globPatterns, + coverage = false, + lineCoverage = 0, + branchCoverage = 0, + functionCoverage = 0, + execArgv = [], + argv = [], + cwd = process.cwd(), + } = options; if (files != null) { - validateArray(files, 'options.files') + validateArray(files, 'options.files'); + } + if (watch != null) { + validateBoolean(watch, 'options.watch'); } + if (forceExit != null) { + validateBoolean(forceExit, 'options.forceExit'); - const root = createTestTree({ concurrency, timeout, signal }) - const testFiles = files ?? createTestFileList() + if (forceExit && watch) { + throw new ERR_INVALID_ARG_VALUE( + 'options.forceExit', watch, 'is not supported with watch mode', + ); + } + } + if (only != null) { + validateBoolean(only, 'options.only'); + } + if (globPatterns != null) { + validateArray(globPatterns, 'options.globPatterns'); + } + + validateString(cwd, 'options.cwd'); + + if (globPatterns?.length > 0 && files?.length > 0) { + throw new ERR_INVALID_ARG_VALUE( + 'options.globPatterns', globPatterns, 'is not supported when specifying \'options.files\'', + ); + } + + if (shard != null) { + validateObject(shard, 'options.shard'); + // Avoid re-evaluating the shard object in case it's a getter + shard = { __proto__: null, index: shard.index, total: shard.total }; + + validateInteger(shard.total, 'options.shard.total', 1); + validateInteger(shard.index, 'options.shard.index', 1, shard.total); + + if (watch) { + throw new ERR_INVALID_ARG_VALUE('options.shard', watch, 'shards not supported with watch mode'); + } + } + if (setup != null) { + validateFunction(setup, 'options.setup'); + } + if (testNamePatterns != null) { + if (!ArrayIsArray(testNamePatterns)) { + testNamePatterns = [testNamePatterns]; + } + + testNamePatterns = ArrayPrototypeMap(testNamePatterns, (value, i) => { + if (isRegExp(value)) { + return value; + } + const name = `options.testNamePatterns[${i}]`; + if (typeof value === 'string') { + return convertStringToRegExp(value, name); + } + throw new ERR_INVALID_ARG_TYPE(name, ['string', 'RegExp'], value); + }); + } + if (testSkipPatterns != null) { + if (!ArrayIsArray(testSkipPatterns)) { + testSkipPatterns = [testSkipPatterns]; + } + + testSkipPatterns = ArrayPrototypeMap(testSkipPatterns, (value, i) => { + if (isRegExp(value)) { + return value; + } + const name = `options.testSkipPatterns[${i}]`; + if (typeof value === 'string') { + return convertStringToRegExp(value, name); + } + throw new ERR_INVALID_ARG_TYPE(name, ['string', 'RegExp'], value); + }); + } + validateOneOf(isolation, 'options.isolation', ['process', 'none']); + validateBoolean(coverage, 'options.coverage'); + if (coverageExcludeGlobs != null) { + if (!ArrayIsArray(coverageExcludeGlobs)) { + coverageExcludeGlobs = [coverageExcludeGlobs]; + } + validateStringArray(coverageExcludeGlobs, 'options.coverageExcludeGlobs'); + } + if (coverageIncludeGlobs != null) { + if (!ArrayIsArray(coverageIncludeGlobs)) { + coverageIncludeGlobs = [coverageIncludeGlobs]; + } + validateStringArray(coverageIncludeGlobs, 'options.coverageIncludeGlobs'); + } + validateInteger(lineCoverage, 'options.lineCoverage', 0, 100); + validateInteger(branchCoverage, 'options.branchCoverage', 0, 100); + validateInteger(functionCoverage, 'options.functionCoverage', 0, 100); + + validateStringArray(argv, 'options.argv'); + validateStringArray(execArgv, 'options.execArgv'); + + const rootTestOptions = { __proto__: null, concurrency, timeout, signal }; + const globalOptions = { + __proto__: null, + // parseCommandLine() should not be used here. However, The existing run() + // behavior has relied on it, so removing it must be done in a semver major. + ...parseCommandLine(), + setup, // This line can be removed when parseCommandLine() is removed here. + coverage, + coverageExcludeGlobs, + coverageIncludeGlobs, + lineCoverage: lineCoverage, + branchCoverage: branchCoverage, + functionCoverage: functionCoverage, + cwd, + }; + const root = createTestTree(rootTestOptions, globalOptions); + let testFiles = files ?? createTestFileList(globPatterns, cwd); + + if (shard) { + testFiles = ArrayPrototypeFilter(testFiles, (_, index) => index % shard.total === shard.index - 1); + } + + let teardown; + let postRun; + let filesWatcher; + let runFiles; + const opts = { + __proto__: null, + root, + signal, + inspectPort, + testNamePatterns, + testSkipPatterns, + hasFiles: files != null, + globPatterns, + only, + forceExit, + cwd, + isolation, + argv, + execArgv, + }; + + if (isolation === 'process') { + if (process.env.NODE_TEST_CONTEXT !== undefined) { + process.emitWarning('node:test run() is being called recursively within a test file. skipping running files.'); + root.postRun(); + return root.reporter; + } + + if (watch) { + filesWatcher = watchFiles(testFiles, opts); + } else { + postRun = () => root.postRun(); + teardown = () => root.harness.teardown(); + } + + runFiles = () => { + root.harness.bootstrapPromise = null; + root.harness.buildPromise = null; + return SafePromiseAllSettledReturnVoid(testFiles, (path) => { + const subtest = runTestFile(path, filesWatcher, opts); + filesWatcher?.runningSubtests.set(path, subtest); + return subtest; + }); + }; + } else if (isolation === 'none') { + if (watch) { + const absoluteTestFiles = ArrayPrototypeMap(testFiles, (file) => (isAbsolute(file) ? file : resolve(cwd, file))); + filesWatcher = watchFiles(absoluteTestFiles, opts); + runFiles = async () => { + root.harness.bootstrapPromise = null; + root.harness.buildPromise = null; + const subtest = runTestFile(kIsolatedProcessName, filesWatcher, opts); + filesWatcher?.runningSubtests.set(kIsolatedProcessName, subtest); + return subtest; + }; + } else { + runFiles = async () => { + const { promise, resolve: finishBootstrap } = PromiseWithResolvers(); + + await root.runInAsyncScope(async () => { + const parentURL = pathToFileURL(cwd + sep).href; + const cascadedLoader = esmLoader.getOrInitializeCascadedLoader(); + let topLevelTestCount = 0; + + root.harness.bootstrapPromise = promise; + + const userImports = getOptionValue('--import'); + for (let i = 0; i < userImports.length; i++) { + await cascadedLoader.import(userImports[i], parentURL, kEmptyObject); + } + + for (let i = 0; i < testFiles.length; ++i) { + const testFile = testFiles[i]; + const fileURL = pathToFileURL(resolve(cwd, testFile)); + const parent = i === 0 ? undefined : parentURL; + let threw = false; + let importError; + + root.entryFile = resolve(testFile); + debug('loading test file:', fileURL.href); + try { + await cascadedLoader.import(fileURL, parent, { __proto__: null }); + } catch (err) { + threw = true; + importError = err; + } + + debug( + 'loaded "%s": top level test count before = %d and after = %d', + testFile, + topLevelTestCount, + root.subtests.length, + ); + if (topLevelTestCount === root.subtests.length) { + // This file had no tests in it. Add the placeholder test. + const subtest = root.createSubtest(Test, testFile); + if (threw) { + subtest.fail(importError); + } + startSubtestAfterBootstrap(subtest); + } + + topLevelTestCount = root.subtests.length; + } + }); + + debug('beginning test execution'); + root.entryFile = null; + finishBootstrap(); + root.processPendingSubtests(); + }; + } + } - PromisePrototypeThen(SafePromiseAll(testFiles, (path) => runTestFile(path, root, inspectPort)), - () => root.postRun()) + const setupPromise = PromiseResolve(setup?.(root.reporter)); + PromisePrototypeThen(PromisePrototypeThen(PromisePrototypeThen(setupPromise, runFiles), postRun), teardown); - return root.reporter + return root.reporter; } -module.exports = { run } +module.exports = { + FileTest, // Exported for tests only + run, +}; diff --git a/lib/internal/test_runner/snapshot.js b/lib/internal/test_runner/snapshot.js new file mode 100644 index 0000000..6004140 --- /dev/null +++ b/lib/internal/test_runner/snapshot.js @@ -0,0 +1,254 @@ +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; +const { + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypeSlice, + ArrayPrototypeSort, + JSONStringify, + ObjectKeys, + SafeMap, + String, + StringPrototypeReplaceAll, +} = primordials; +const { + codes: { + ERR_INVALID_STATE, + }, +} = require('#internal/errors'); +const { emitExperimentalWarning, kEmptyObject } = require('#internal/util'); +let debug = require('#internal/util/debuglog').debuglog('test_runner', (fn) => { + debug = fn; +}); +const { + validateArray, + validateFunction, + validateObject, +} = require('#internal/validators'); +const { strictEqual } = require('assert'); +const { mkdirSync, readFileSync, writeFileSync } = require('fs'); +const { dirname } = require('path'); +const { createContext, runInContext } = require('vm'); +const kExperimentalWarning = 'Snapshot testing'; +const kMissingSnapshotTip = 'Missing snapshots can be generated by rerunning ' + + 'the command with the --test-update-snapshots flag.'; +const defaultSerializers = [ + (value) => { return JSONStringify(value, null, 2); }, +]; + +function defaultResolveSnapshotPath(testPath) { + if (typeof testPath !== 'string') { + return testPath; + } + + return `${testPath}.snapshot`; +} + +let resolveSnapshotPathFn = defaultResolveSnapshotPath; +let serializerFns = defaultSerializers; + +function setResolveSnapshotPath(fn) { + emitExperimentalWarning(kExperimentalWarning); + validateFunction(fn, 'fn'); + resolveSnapshotPathFn = fn; +} + +function setDefaultSnapshotSerializers(serializers) { + emitExperimentalWarning(kExperimentalWarning); + validateFunctionArray(serializers, 'serializers'); + serializerFns = ArrayPrototypeSlice(serializers); +} + +class SnapshotFile { + constructor(snapshotFile) { + this.snapshotFile = snapshotFile; + this.snapshots = { __proto__: null }; + this.nameCounts = new SafeMap(); + this.loaded = false; + } + + getSnapshot(id) { + if (!(id in this.snapshots)) { + const err = new ERR_INVALID_STATE(`Snapshot '${id}' not found in ` + + `'${this.snapshotFile}.' ${kMissingSnapshotTip}`); + err.snapshot = id; + err.filename = this.snapshotFile; + throw err; + } + + return this.snapshots[id]; + } + + setSnapshot(id, value) { + this.snapshots[templateEscape(id)] = value; + } + + nextId(name) { + const count = this.nameCounts.get(name) ?? 1; + this.nameCounts.set(name, count + 1); + return `${name} ${count}`; + } + + readFile() { + if (this.loaded) { + debug('skipping read of snapshot file'); + return; + } + + try { + const source = readFileSync(this.snapshotFile, 'utf8'); + const context = { __proto__: null, exports: { __proto__: null } }; + + createContext(context); + runInContext(source, context); + + if (context.exports === null || typeof context.exports !== 'object') { + throw new ERR_INVALID_STATE( + `Malformed snapshot file '${this.snapshotFile}'.`, + ); + } + + for (const key in context.exports) { + this.snapshots[key] = templateEscape(context.exports[key]); + } + this.loaded = true; + } catch (err) { + let msg = `Cannot read snapshot file '${this.snapshotFile}.'`; + + if (err?.code === 'ENOENT') { + msg += ` ${kMissingSnapshotTip}`; + } + + const error = new ERR_INVALID_STATE(msg); + error.cause = err; + error.filename = this.snapshotFile; + throw error; + } + } + + writeFile() { + try { + const keys = ArrayPrototypeSort(ObjectKeys(this.snapshots)); + const snapshotStrings = ArrayPrototypeMap(keys, (key) => { + return `exports[\`${key}\`] = \`${this.snapshots[key]}\`;\n`; + }); + const output = ArrayPrototypeJoin(snapshotStrings, '\n'); + mkdirSync(dirname(this.snapshotFile), { __proto__: null, recursive: true }); + writeFileSync(this.snapshotFile, output, 'utf8'); + } catch (err) { + const msg = `Cannot write snapshot file '${this.snapshotFile}.'`; + const error = new ERR_INVALID_STATE(msg); + error.cause = err; + error.filename = this.snapshotFile; + throw error; + } + } +} + +class SnapshotManager { + constructor(updateSnapshots) { + // A manager instance will only read or write snapshot files based on the + // updateSnapshots argument. + this.updateSnapshots = updateSnapshots; + this.cache = new SafeMap(); + } + + resolveSnapshotFile(entryFile) { + let snapshotFile = this.cache.get(entryFile); + + if (snapshotFile === undefined) { + const resolved = resolveSnapshotPathFn(entryFile); + + if (typeof resolved !== 'string') { + const err = new ERR_INVALID_STATE('Invalid snapshot filename.'); + err.filename = resolved; + throw err; + } + + snapshotFile = new SnapshotFile(resolved); + snapshotFile.loaded = this.updateSnapshots; + this.cache.set(entryFile, snapshotFile); + } + + return snapshotFile; + } + + serialize(input, serializers = serializerFns) { + try { + let value = input; + + for (let i = 0; i < serializers.length; ++i) { + const fn = serializers[i]; + value = fn(value); + } + + return `\n${templateEscape(value)}\n`; + } catch (err) { + const error = new ERR_INVALID_STATE( + 'The provided serializers did not generate a string.', + ); + error.input = input; + error.cause = err; + throw error; + } + } + + writeSnapshotFiles() { + if (!this.updateSnapshots) { + debug('skipping write of snapshot files'); + return; + } + + this.cache.forEach((snapshotFile) => { + snapshotFile.writeFile(); + }); + } + + createAssert() { + const manager = this; + + return function snapshotAssertion(actual, options = kEmptyObject) { + emitExperimentalWarning(kExperimentalWarning); + validateObject(options, 'options'); + const { + serializers = serializerFns, + } = options; + validateFunctionArray(serializers, 'options.serializers'); + const { filePath, fullName } = this; + const snapshotFile = manager.resolveSnapshotFile(filePath); + const value = manager.serialize(actual, serializers); + const id = snapshotFile.nextId(fullName); + + if (manager.updateSnapshots) { + snapshotFile.setSnapshot(id, value); + } else { + snapshotFile.readFile(); + strictEqual(value, snapshotFile.getSnapshot(id)); + } + }; + } +} + +function validateFunctionArray(fns, name) { + validateArray(fns, name); + for (let i = 0; i < fns.length; ++i) { + validateFunction(fns[i], `${name}[${i}]`); + } +} + +function templateEscape(str) { + let result = String(str); + result = StringPrototypeReplaceAll(result, '\\', '\\\\'); + result = StringPrototypeReplaceAll(result, '`', '\\`'); + result = StringPrototypeReplaceAll(result, '${', '\\${'); + return result; +} + +module.exports = { + SnapshotManager, + defaultResolveSnapshotPath, // Exported for testing only. + defaultSerializers, // Exported for testing only. + setDefaultSnapshotSerializers, + setResolveSnapshotPath, +}; diff --git a/lib/internal/test_runner/tap_checker.js b/lib/internal/test_runner/tap_checker.js deleted file mode 100644 index 6b25155..0000000 --- a/lib/internal/test_runner/tap_checker.js +++ /dev/null @@ -1,156 +0,0 @@ -// https://github.com/nodejs/node/blob/f8ce9117b19702487eb600493d941f7876e00e01/lib/internal/test_runner/tap_checker.js -'use strict' - -const { - ArrayPrototypeFilter, - ArrayPrototypeFind, - NumberParseInt -} = require('#internal/per_context/primordials') -const { - codes: { ERR_TAP_VALIDATION_ERROR } -} = require('#internal/errors') -const { TokenKind } = require('#internal/test_runner/tap_lexer') - -// TODO(@manekinekko): add more validation rules based on the TAP14 spec. -// See https://testanything.org/tap-version-14-specification.html -class TAPValidationStrategy { - validate (ast) { - this.#validateVersion(ast) - this.#validatePlan(ast) - this.#validateTestPoints(ast) - - return true - } - - #validateVersion (ast) { - const entry = ArrayPrototypeFind( - ast, - (node) => node.kind === TokenKind.TAP_VERSION - ) - - if (!entry) { - throw new ERR_TAP_VALIDATION_ERROR('missing TAP version') - } - - const { version } = entry.node - - // TAP14 specification is compatible with observed behavior of existing TAP13 consumers and producers - if (version !== '14' && version !== '13') { - throw new ERR_TAP_VALIDATION_ERROR('TAP version should be 13 or 14') - } - } - - #validatePlan (ast) { - const entry = ArrayPrototypeFind( - ast, - (node) => node.kind === TokenKind.TAP_PLAN - ) - - if (!entry) { - throw new ERR_TAP_VALIDATION_ERROR('missing TAP plan') - } - - const plan = entry.node - - if (!plan.start) { - throw new ERR_TAP_VALIDATION_ERROR('missing plan start') - } - - if (!plan.end) { - throw new ERR_TAP_VALIDATION_ERROR('missing plan end') - } - - const planStart = NumberParseInt(plan.start, 10) - const planEnd = NumberParseInt(plan.end, 10) - - if (planEnd !== 0 && planStart > planEnd) { - throw new ERR_TAP_VALIDATION_ERROR( - `plan start ${planStart} is greater than plan end ${planEnd}` - ) - } - } - - // TODO(@manekinekko): since we are dealing with a flat AST, we need to - // validate test points grouped by their "nesting" level. This is because a set of - // Test points belongs to a TAP document. Each new subtest block creates a new TAP document. - // https://testanything.org/tap-version-14-specification.html#subtests - #validateTestPoints (ast) { - const bailoutEntry = ArrayPrototypeFind( - ast, - (node) => node.kind === TokenKind.TAP_BAIL_OUT - ) - const planEntry = ArrayPrototypeFind( - ast, - (node) => node.kind === TokenKind.TAP_PLAN - ) - const testPointEntries = ArrayPrototypeFilter( - ast, - (node) => node.kind === TokenKind.TAP_TEST_POINT - ) - - const plan = planEntry.node - - const planStart = NumberParseInt(plan.start, 10) - const planEnd = NumberParseInt(plan.end, 10) - - if (planEnd === 0 && testPointEntries.length > 0) { - throw new ERR_TAP_VALIDATION_ERROR( - `found ${testPointEntries.length} Test Point${ - testPointEntries.length > 1 ? 's' : '' - } but plan is ${planStart}..0` - ) - } - - if (planEnd > 0) { - if (testPointEntries.length === 0) { - throw new ERR_TAP_VALIDATION_ERROR('missing Test Points') - } - - if (!bailoutEntry && testPointEntries.length !== planEnd) { - throw new ERR_TAP_VALIDATION_ERROR( - `test Points count ${testPointEntries.length} does not match plan count ${planEnd}` - ) - } - - for (let i = 0; i < testPointEntries.length; i++) { - const test = testPointEntries[i].node - const testId = NumberParseInt(test.id, 10) - - if (testId < planStart || testId > planEnd) { - throw new ERR_TAP_VALIDATION_ERROR( - `test ${testId} is out of plan range ${planStart}..${planEnd}` - ) - } - } - } - } -} - -// TAP14 and TAP13 are compatible with each other -class TAP13ValidationStrategy extends TAPValidationStrategy {} -class TAP14ValidationStrategy extends TAPValidationStrategy {} - -class TapChecker { - static TAP13 = '13' - static TAP14 = '14' - - constructor ({ specs }) { - switch (specs) { - case TapChecker.TAP13: - this.strategy = new TAP13ValidationStrategy() - break - default: - this.strategy = new TAP14ValidationStrategy() - } - } - - check (ast) { - return this.strategy.validate(ast) - } -} - -module.exports = { - TapChecker, - TAP14ValidationStrategy, - TAP13ValidationStrategy -} diff --git a/lib/internal/test_runner/tap_lexer.js b/lib/internal/test_runner/tap_lexer.js deleted file mode 100644 index 022f75f..0000000 --- a/lib/internal/test_runner/tap_lexer.js +++ /dev/null @@ -1,529 +0,0 @@ -// https://github.com/nodejs/node/blob/2483da743cbb48f31c6b3f8cb186d89f31d73611/lib/internal/test_runner/tap_lexer.js -'use strict' - -const { - ArrayPrototypePop, - ArrayPrototypePush, - MathMax, - SafeSet, - StringPrototypeIncludes, - StringPrototypeTrim -} = require('#internal/per_context/primordials') -const { - codes: { ERR_TAP_LEXER_ERROR } -} = require('#internal/errors') - -const kEOL = '' -const kEOF = '' - -const TokenKind = { - EOF: 'EOF', - EOL: 'EOL', - NEWLINE: 'NewLine', - NUMERIC: 'Numeric', - LITERAL: 'Literal', - KEYWORD: 'Keyword', - WHITESPACE: 'Whitespace', - COMMENT: 'Comment', - DASH: 'Dash', - PLUS: 'Plus', - HASH: 'Hash', - ESCAPE: 'Escape', - UNKNOWN: 'Unknown', - - // TAP tokens - TAP: 'TAPKeyword', - TAP_VERSION: 'VersionKeyword', - TAP_PLAN: 'PlanKeyword', - TAP_TEST_POINT: 'TestPointKeyword', - TAP_SUBTEST_POINT: 'SubTestPointKeyword', - TAP_TEST_OK: 'TestOkKeyword', - TAP_TEST_NOTOK: 'TestNotOkKeyword', - TAP_YAML_START: 'YamlStartKeyword', - TAP_YAML_END: 'YamlEndKeyword', - TAP_YAML_BLOCK: 'YamlKeyword', - TAP_PRAGMA: 'PragmaKeyword', - TAP_BAIL_OUT: 'BailOutKeyword' -} - -class Token { - constructor ({ kind, value, stream }) { - const valueLength = ('' + value).length - this.kind = kind - this.value = value - this.location = { - line: stream.line, - column: MathMax(stream.column - valueLength + 1, 1), // 1 based - start: MathMax(stream.pos - valueLength, 0), // zero based - end: stream.pos - (value === '' ? 0 : 1) // zero based - } - - // EOF is a special case - if (value === TokenKind.EOF) { - const eofPosition = stream.input.length + 1 // We consider EOF to be outside the stream - this.location.start = eofPosition - this.location.end = eofPosition - this.location.column = stream.column + 1 // 1 based - } - } -} - -class InputStream { - constructor (input) { - this.input = input - this.pos = 0 - this.column = 0 - this.line = 1 - } - - eof () { - return this.peek() === undefined - } - - peek (offset = 0) { - return this.input[this.pos + offset] - } - - next () { - const char = this.peek() - if (char === undefined) { - return undefined - } - - this.pos++ - this.column++ - if (char === '\n') { - this.line++ - this.column = 0 - } - - return char - } -} - -class TapLexer { - static Keywords = new SafeSet([ - 'TAP', - 'version', - 'ok', - 'not', - '...', - '---', - '..', - 'pragma', - '-', - '+' - - // NOTE: "Skip", "Todo" and "Bail out!" literals are deferred to the parser - ]) - - #isComment = false - #source = null - #line = 1 - #column = 0 - #escapeStack = [] - #lastScannedToken = null - - constructor (source) { - this.#source = new InputStream(source) - this.#lastScannedToken = new Token({ - kind: TokenKind.EOL, - value: kEOL, - stream: this.#source - }) - } - - scan () { - const tokens = [] - let chunk = [] - while (!this.eof()) { - const token = this.#scanToken() - - // Remember the last scanned token (except for whitespace) - if (token.kind !== TokenKind.WHITESPACE) { - this.#lastScannedToken = token - } - - ArrayPrototypePush(chunk, token) - if (token.kind === TokenKind.NEWLINE) { - // Store the current chunk + NEWLINE token - ArrayPrototypePush(tokens, chunk) - chunk = [] - } - } - - if (chunk.length > 0) { - ArrayPrototypePush(chunk, this.#scanEOL()) - ArrayPrototypePush(tokens, chunk) - } - - // send EOF as a separate chunk - ArrayPrototypePush(tokens, [this.#scanEOF()]) - - return tokens - } - - next () { - return this.#source.next() - } - - eof () { - return this.#source.eof() - } - - error (message, token, expected = '') { - this.#source.error(message, token, expected) - } - - #scanToken () { - const char = this.next() - - if (this.#isEOFSymbol(char)) { - return this.#scanEOF() - } else if (this.#isNewLineSymbol(char)) { - return this.#scanNewLine(char) - } else if (this.#isNumericSymbol(char)) { - return this.#scanNumeric(char) - } else if (this.#isDashSymbol(char)) { - return this.#scanDash(char) - } else if (this.#isPlusSymbol(char)) { - return this.#scanPlus(char) - } else if (this.#isHashSymbol(char)) { - return this.#scanHash(char) - } else if (this.#isEscapeSymbol(char)) { - return this.#scanEscapeSymbol(char) - } else if (this.#isWhitespaceSymbol(char)) { - return this.#scanWhitespace(char) - } else if (this.#isLiteralSymbol(char)) { - return this.#scanLiteral(char) - } - - throw new ERR_TAP_LEXER_ERROR( - `Unexpected character: ${char} at line ${this.#line}, column ${ - this.#column - }` - ) - } - - #scanNewLine (char) { - // In case of odd number of ESCAPE symbols, we need to clear the remaining - // escape chars from the stack and start fresh for the next line. - this.#escapeStack = [] - - // We also need to reset the comment flag - this.#isComment = false - - return new Token({ - kind: TokenKind.NEWLINE, - value: char, - stream: this.#source - }) - } - - #scanEOL () { - return new Token({ - kind: TokenKind.EOL, - value: kEOL, - stream: this.#source - }) - } - - #scanEOF () { - this.#isComment = false - - return new Token({ - kind: TokenKind.EOF, - value: kEOF, - stream: this.#source - }) - } - - #scanEscapeSymbol (char) { - // If the escape symbol has been escaped (by previous symbol), - // or if the next symbol is a whitespace symbol, - // then consume it as a literal. - if ( - this.#hasTheCurrentCharacterBeenEscaped() || - this.#source.peek(1) === TokenKind.WHITESPACE - ) { - ArrayPrototypePop(this.#escapeStack) - return new Token({ - kind: TokenKind.LITERAL, - value: char, - stream: this.#source - }) - } - - // Otherwise, consume the escape symbol as an escape symbol that should be ignored by the parser - // we also need to push the escape symbol to the escape stack - // and consume the next character as a literal (done in the next turn) - ArrayPrototypePush(this.#escapeStack, char) - return new Token({ - kind: TokenKind.ESCAPE, - value: char, - stream: this.#source - }) - } - - #scanWhitespace (char) { - return new Token({ - kind: TokenKind.WHITESPACE, - value: char, - stream: this.#source - }) - } - - #scanDash (char) { - // Peek next 3 characters and check if it's a YAML start marker - const marker = char + this.#source.peek() + this.#source.peek(1) - - if (this.#isYamlStartSymbol(marker)) { - this.next() // consume second - - this.next() // consume third - - - return new Token({ - kind: TokenKind.TAP_YAML_START, - value: marker, - stream: this.#source - }) - } - - return new Token({ - kind: TokenKind.DASH, - value: char, - stream: this.#source - }) - } - - #scanPlus (char) { - return new Token({ - kind: TokenKind.PLUS, - value: char, - stream: this.#source - }) - } - - #scanHash (char) { - const lastCharacter = this.#source.peek(-2) - const nextToken = this.#source.peek() - - // If we encounter a hash symbol at the beginning of a line, - // we consider it as a comment - if (!lastCharacter || this.#isNewLineSymbol(lastCharacter)) { - this.#isComment = true - return new Token({ - kind: TokenKind.COMMENT, - value: char, - stream: this.#source - }) - } - - // The only valid case where a hash symbol is considered as a hash token - // is when it's preceded by a whitespace symbol and followed by a non-hash symbol - if ( - this.#isWhitespaceSymbol(lastCharacter) && - !this.#isHashSymbol(nextToken) - ) { - return new Token({ - kind: TokenKind.HASH, - value: char, - stream: this.#source - }) - } - - const charHasBeenEscaped = this.#hasTheCurrentCharacterBeenEscaped() - if (this.#isComment || charHasBeenEscaped) { - if (charHasBeenEscaped) { - ArrayPrototypePop(this.#escapeStack) - } - - return new Token({ - kind: TokenKind.LITERAL, - value: char, - stream: this.#source - }) - } - - // As a fallback, we consume the hash symbol as a literal - return new Token({ - kind: TokenKind.LITERAL, - value: char, - stream: this.#source - }) - } - - #scanLiteral (char) { - let word = char - while (!this.#source.eof()) { - const nextChar = this.#source.peek() - if (this.#isLiteralSymbol(nextChar)) { - word += this.#source.next() - } else { - break - } - } - - word = StringPrototypeTrim(word) - - if (TapLexer.Keywords.has(word)) { - const token = this.#scanTAPKeyword(word) - if (token) { - return token - } - } - - if (this.#isYamlEndSymbol(word)) { - return new Token({ - kind: TokenKind.TAP_YAML_END, - value: word, - stream: this.#source - }) - } - - return new Token({ - kind: TokenKind.LITERAL, - value: word, - stream: this.#source - }) - } - - #scanTAPKeyword (word) { - const isLastScannedTokenEOLorNewLine = - TokenKind.EOL === this.#lastScannedToken.kind || - TokenKind.NEWLINE === this.#lastScannedToken.kind - - if (word === 'TAP' && isLastScannedTokenEOLorNewLine) { - return new Token({ - kind: TokenKind.TAP, - value: word, - stream: this.#source - }) - } - - if (word === 'version' && this.#lastScannedToken.kind === TokenKind.TAP) { - return new Token({ - kind: TokenKind.TAP_VERSION, - value: word, - stream: this.#source - }) - } - - if (word === '..' && this.#lastScannedToken.kind === TokenKind.NUMERIC) { - return new Token({ - kind: TokenKind.TAP_PLAN, - value: word, - stream: this.#source - }) - } - - if (word === 'not' && isLastScannedTokenEOLorNewLine) { - return new Token({ - kind: TokenKind.TAP_TEST_NOTOK, - value: word, - stream: this.#source - }) - } - - if ( - word === 'ok' && - (this.#lastScannedToken.kind === TokenKind.TAP_TEST_NOTOK || - isLastScannedTokenEOLorNewLine) - ) { - return new Token({ - kind: TokenKind.TAP_TEST_OK, - value: word, - stream: this.#source - }) - } - - if (word === 'pragma' && isLastScannedTokenEOLorNewLine) { - return new Token({ - kind: TokenKind.TAP_PRAGMA, - value: word, - stream: this.#source - }) - } - - return null - } - - #scanNumeric (char) { - let number = char - while (!this.#source.eof()) { - const nextChar = this.#source.peek() - if (this.#isNumericSymbol(nextChar)) { - number += nextChar - this.#source.next() - } else { - break - } - } - return new Token({ - kind: TokenKind.NUMERIC, - value: number, - stream: this.#source - }) - } - - #hasTheCurrentCharacterBeenEscaped () { - // Use the escapeStack to keep track of the escape characters - return this.#escapeStack.length > 0 - } - - #isNumericSymbol (char) { - return char >= '0' && char <= '9' - } - - #isLiteralSymbol (char) { - return ( - (char >= 'a' && char <= 'z') || - (char >= 'A' && char <= 'Z') || - this.#isSpecialCharacterSymbol(char) - ) - } - - #isSpecialCharacterSymbol (char) { - // We deliberately do not include "# \ + -"" in this list - // these are used for comments/reasons explanations, pragma and escape characters - // whitespace is not included because it is handled separately - return StringPrototypeIncludes('!"$%&\'()*,./:;<=>?@[]^_`{|}~', char) - } - - #isWhitespaceSymbol (char) { - return char === ' ' || char === '\t' - } - - #isEOFSymbol (char) { - return char === undefined - } - - #isNewLineSymbol (char) { - return char === '\n' || char === '\r' - } - - #isHashSymbol (char) { - return char === '#' - } - - #isDashSymbol (char) { - return char === '-' - } - - #isPlusSymbol (char) { - return char === '+' - } - - #isEscapeSymbol (char) { - return char === '\\' - } - - #isYamlStartSymbol (char) { - return char === '---' - } - - #isYamlEndSymbol (char) { - return char === '...' - } -} - -module.exports = { TapLexer, TokenKind } diff --git a/lib/internal/test_runner/tap_parser.js b/lib/internal/test_runner/tap_parser.js deleted file mode 100644 index 94b06c0..0000000 --- a/lib/internal/test_runner/tap_parser.js +++ /dev/null @@ -1,990 +0,0 @@ -// https://github.com/nodejs/node/blob/4c08c20e575a0954fe3977a20e9f52b4980a2e48/lib/internal/test_runner/tap_parser.js -'use strict' - -const { - ArrayPrototypeFilter, - ArrayPrototypeForEach, - ArrayPrototypeIncludes, - ArrayPrototypeJoin, - ArrayPrototypeMap, - ArrayPrototypePop, - ArrayPrototypePush, - Boolean, - Number, - RegExpPrototypeExec, - String, - StringPrototypeEndsWith, - StringPrototypeReplaceAll, - StringPrototypeSlice, - StringPrototypeSplit, - StringPrototypeTrim -} = require('#internal/per_context/primordials') -const Transform = require('stream').Transform -const { TapLexer, TokenKind } = require('#internal/test_runner/tap_lexer') -const { TapChecker } = require('#internal/test_runner/tap_checker') -const { - codes: { ERR_TAP_VALIDATION_ERROR, ERR_TAP_PARSER_ERROR } -} = require('#internal/errors') -const { kEmptyObject } = require('#internal/util') -/** - * - * TAP14 specifications - * - * See https://testanything.org/tap-version-14-specification.html - * - * Note that the following grammar is intended as a rough "pseudocode" guidance. - * It is not strict EBNF: - * - * TAPDocument := Version Plan Body | Version Body Plan - * Version := "TAP version 14\n" - * Plan := "1.." (Number) (" # " Reason)? "\n" - * Body := (TestPoint | BailOut | Pragma | Comment | Anything | Empty | Subtest)* - * TestPoint := ("not ")? "ok" (" " Number)? ((" -")? (" " Description) )? (" " Directive)? "\n" (YAMLBlock)? - * Directive := " # " ("todo" | "skip") (" " Reason)? - * YAMLBlock := " ---\n" (YAMLLine)* " ...\n" - * YAMLLine := " " (YAML)* "\n" - * BailOut := "Bail out!" (" " Reason)? "\n" - * Reason := [^\n]+ - * Pragma := "pragma " [+-] PragmaKey "\n" - * PragmaKey := ([a-zA-Z0-9_-])+ - * Subtest := ("# Subtest" (": " SubtestName)?)? "\n" SubtestDocument TestPoint - * Comment := ^ (" ")* "#" [^\n]* "\n" - * Empty := [\s\t]* "\n" - * Anything := [^\n]+ "\n" - * - */ - -/** - * An LL(1) parser for TAP14/TAP13. - */ -class TapParser extends Transform { - #checker = null - #lexer = null - #currentToken = null - - #input = '' - #currentChunkAsString = '' - #lastLine = '' - - #tokens = [[]] - #flatAST = [] - #bufferedComments = [] - #bufferedTestPoints = [] - #lastTestPointDetails = {} - #yamlBlockBuffer = [] - - #currentTokenIndex = 0 - #currentTokenChunk = 0 - #subTestNestingLevel = 0 - #yamlCurrentIndentationLevel = 0 - #kSubtestBlockIndentationFactor = 4 - - #isYAMLBlock = false - #isSyncParsingEnabled = false - - constructor ({ specs = TapChecker.TAP13 } = kEmptyObject) { - super({ __proto__: null, readableObjectMode: true }) - - this.#checker = new TapChecker({ specs }) - } - - // ----------------------------------------------------------------------// - // ----------------------------- Public API -----------------------------// - // ----------------------------------------------------------------------// - - parse (chunkAsString = '', callback = null) { - this.#isSyncParsingEnabled = false - this.#currentTokenChunk = 0 - this.#currentTokenIndex = 0 - // Note: we are overwriting the input on each stream call - // This is fine because we don't want to parse previous chunks - this.#input = chunkAsString - this.#lexer = new TapLexer(chunkAsString) - - try { - this.#tokens = this.#scanTokens() - this.#parseTokens(callback) - } catch (error) { - callback(null, error) - } - } - - parseSync (input = '', callback = null) { - if (typeof input !== 'string' || input === '') { - return [] - } - - this.#isSyncParsingEnabled = true - this.#input = input - this.#lexer = new TapLexer(input) - this.#tokens = this.#scanTokens() - - this.#parseTokens(callback) - - if (this.#isYAMLBlock) { - // Looks like we have a non-ending YAML block - this.#error('Expected end of YAML block') - } - - // Manually flush the remaining buffered comments and test points - this._flush() - - return this.#flatAST - } - - // Check if the TAP content is semantically valid - // Note: Validating the TAP content requires the whole AST to be available. - check () { - if (this.#isSyncParsingEnabled) { - return this.#checker.check(this.#flatAST) - } - - // TODO(@manekinekko): when running in async mode, it doesn't make sense to - // validate the current chunk. Validation needs to whole AST to be available. - throw new ERR_TAP_VALIDATION_ERROR( - 'TAP validation is not supported for async parsing' - ) - } - // ----------------------------------------------------------------------// - // --------------------------- Transform API ----------------------------// - // ----------------------------------------------------------------------// - - processChunk (chunk) { - const str = this.#lastLine + chunk.toString('utf8') - const lines = StringPrototypeSplit(str, '\n') - this.#lastLine = ArrayPrototypePop(lines) - - let chunkAsString = ArrayPrototypeJoin(lines, '\n') - // Special case where chunk is emitted by a child process - chunkAsString = StringPrototypeReplaceAll( - chunkAsString, - '[out] ', - '' - ) - chunkAsString = StringPrototypeReplaceAll( - chunkAsString, - '[err] ', - '' - ) - if (StringPrototypeEndsWith(chunkAsString, '\n')) { - chunkAsString = StringPrototypeSlice(chunkAsString, 0, -1) - } - if (StringPrototypeEndsWith(chunkAsString, 'EOF')) { - chunkAsString = StringPrototypeSlice(chunkAsString, 0, -3) - } - - return chunkAsString - } - - _transform (chunk, _encoding, next) { - const chunkAsString = this.processChunk(chunk) - - if (!chunkAsString) { - // Ignore empty chunks - next() - return - } - - this.parse(chunkAsString, (node, error) => { - if (error) { - next(error) - return - } - - if (node.kind === TokenKind.EOF) { - // Emit when the current chunk is fully processed and consumed - next() - } - }) - } - - // Flush the remaining buffered comments and test points - // This will be called automatically when the stream is closed - // We also call this method manually when we reach the end of the sync parsing - _flush (next = null) { - if (!this.#lastLine) { - this.#__flushPendingTestPointsAndComments() - next?.() - return - } - // Parse the remaining line - this.parse(this.#lastLine, (node, error) => { - this.#lastLine = '' - - if (error) { - next?.(error) - return - } - - if (node.kind === TokenKind.EOF) { - this.#__flushPendingTestPointsAndComments() - next?.() - } - }) - } - - #__flushPendingTestPointsAndComments () { - ArrayPrototypeForEach(this.#bufferedTestPoints, (node) => { - this.#emit(node) - }) - ArrayPrototypeForEach(this.#bufferedComments, (node) => { - this.#emit(node) - }) - - // Clean up - this.#bufferedTestPoints = [] - this.#bufferedComments = [] - } - - // ----------------------------------------------------------------------// - // ----------------------------- Private API ----------------------------// - // ----------------------------------------------------------------------// - - #scanTokens () { - return this.#lexer.scan() - } - - #parseTokens (callback = null) { - for (let index = 0; index < this.#tokens.length; index++) { - const chunk = this.#tokens[index] - this.#parseChunk(chunk) - } - - callback?.({ kind: TokenKind.EOF }) // eslint-disable-line n/no-callback-literal - } - - #parseChunk (chunk) { - this.#subTestNestingLevel = this.#getCurrentIndentationLevel(chunk) - // We compute the current index of the token in the chunk - // based on the indentation level (number of spaces). - // We also need to take into account if we are in a YAML block or not. - // If we are in a YAML block, we compute the current index of the token - // based on the indentation level of the YAML block (start block). - - if (this.#isYAMLBlock) { - this.#currentTokenIndex = - this.#yamlCurrentIndentationLevel * - this.#kSubtestBlockIndentationFactor - } else { - this.#currentTokenIndex = - this.#subTestNestingLevel * this.#kSubtestBlockIndentationFactor - this.#yamlCurrentIndentationLevel = this.#subTestNestingLevel - } - - let node - - // Parse current chunk - try { - node = this.#TAPDocument(chunk) - } catch { - node = { - kind: TokenKind.UNKNOWN, - node: { - value: this.#currentChunkAsString - } - } - } - - // Emit the parsed node to both the stream and the AST - this.#emitOrBufferCurrentNode(node) - - // Move pointers to the next chunk and reset the current token index - this.#currentTokenChunk++ - this.#currentTokenIndex = 0 - } - - #error (message) { - const token = this.#currentToken || { value: '', kind: '' } - // Escape NewLine characters - if (token.value === '\n') { - token.value = '\\n' - } - - throw new ERR_TAP_PARSER_ERROR( - message, - `, received "${token.value}" (${token.kind})`, - token, - this.#input - ) - } - - #peek (shouldSkipBlankTokens = true) { - if (shouldSkipBlankTokens) { - this.#skip(TokenKind.WHITESPACE) - } - - return this.#tokens[this.#currentTokenChunk][this.#currentTokenIndex] - } - - #next (shouldSkipBlankTokens = true) { - if (shouldSkipBlankTokens) { - this.#skip(TokenKind.WHITESPACE) - } - - if (this.#tokens[this.#currentTokenChunk]) { - this.#currentToken = - this.#tokens[this.#currentTokenChunk][this.#currentTokenIndex++] - } else { - this.#currentToken = null - } - - return this.#currentToken - } - - // Skip the provided tokens in the current chunk - #skip (...tokensToSkip) { - let token = this.#tokens[this.#currentTokenChunk][this.#currentTokenIndex] - while (token && ArrayPrototypeIncludes(tokensToSkip, token.kind)) { - // pre-increment to skip current tokens but make sure we don't advance index on the last iteration - token = this.#tokens[this.#currentTokenChunk][++this.#currentTokenIndex] - } - } - - #readNextLiterals () { - const literals = [] - let nextToken = this.#peek(false) - - // Read all literal, numeric, whitespace and escape tokens until we hit a different token - // or reach end of current chunk - while ( - nextToken && - ArrayPrototypeIncludes( - [ - TokenKind.LITERAL, - TokenKind.NUMERIC, - TokenKind.DASH, - TokenKind.PLUS, - TokenKind.WHITESPACE, - TokenKind.ESCAPE - ], - nextToken.kind - ) - ) { - const word = this.#next(false).value - - // Don't output escaped characters - if (nextToken.kind !== TokenKind.ESCAPE) { - ArrayPrototypePush(literals, word) - } - - nextToken = this.#peek(false) - } - - return ArrayPrototypeJoin(literals, '') - } - - #countLeadingSpacesInCurrentChunk (chunk) { - // Count the number of whitespace tokens in the chunk, starting from the first token - let whitespaceCount = 0 - while (chunk?.[whitespaceCount]?.kind === TokenKind.WHITESPACE) { - whitespaceCount++ - } - return whitespaceCount - } - - #addDiagnosticsToLastTestPoint (currentNode) { - const { length, [length - 1]: lastTestPoint } = this.#bufferedTestPoints - - // Diagnostic nodes are only added to Test points of the same nesting level - if (lastTestPoint && lastTestPoint.nesting === currentNode.nesting) { - lastTestPoint.node.time = this.#lastTestPointDetails.duration - - // TODO(@manekinekko): figure out where to put the other diagnostic properties - // See https://github.com/nodejs/node/pull/44952 - lastTestPoint.node.diagnostics = lastTestPoint.node.diagnostics || [] - - ArrayPrototypeForEach(currentNode.node.diagnostics, (diagnostic) => { - // Avoid adding empty diagnostics - if (diagnostic) { - ArrayPrototypePush(lastTestPoint.node.diagnostics, diagnostic) - } - }) - - this.#bufferedTestPoints = [] - } - - return lastTestPoint - } - - #flushBufferedTestPointNode (shouldClearBuffer = true) { - if (this.#bufferedTestPoints.length > 0) { - this.#emit(this.#bufferedTestPoints[0]) - - if (shouldClearBuffer) { - this.#bufferedTestPoints = [] - } - } - } - - #addCommentsToCurrentNode (currentNode) { - if (this.#bufferedComments.length > 0) { - currentNode.comments = ArrayPrototypeMap( - this.#bufferedComments, - (c) => c.node.comment - ) - this.#bufferedComments = [] - } - - return currentNode - } - - #flushBufferedComments (shouldClearBuffer = true) { - if (this.#bufferedComments.length > 0) { - ArrayPrototypeForEach(this.#bufferedComments, (node) => { - this.#emit(node) - }) - - if (shouldClearBuffer) { - this.#bufferedComments = [] - } - } - } - - #getCurrentIndentationLevel (chunk) { - const whitespaceCount = this.#countLeadingSpacesInCurrentChunk(chunk) - return (whitespaceCount / this.#kSubtestBlockIndentationFactor) | 0 - } - - #emit (node) { - if (node.kind !== TokenKind.EOF) { - ArrayPrototypePush(this.#flatAST, node) - this.push({ - __proto__: null, - ...node - }) - } - } - - #emitOrBufferCurrentNode (currentNode) { - currentNode = { - ...currentNode, - nesting: this.#subTestNestingLevel, - lexeme: this.#currentChunkAsString - } - - switch (currentNode.kind) { - // Emit these nodes - case TokenKind.UNKNOWN: - if (!currentNode.node.value) { - // Ignore unrecognized and empty nodes - break - } - // falls through - - case TokenKind.TAP_PLAN: - case TokenKind.TAP_PRAGMA: - case TokenKind.TAP_VERSION: - case TokenKind.TAP_BAIL_OUT: - case TokenKind.TAP_SUBTEST_POINT: - // Check if we have a buffered test point, and if so, emit it - this.#flushBufferedTestPointNode() - - // If we have buffered comments, add them to the current node - currentNode = this.#addCommentsToCurrentNode(currentNode) - - // Emit the current node - this.#emit(currentNode) - break - - // By default, we buffer the next test point node in case we have a diagnostic - // to add to it in the next iteration - // Note: in case we hit and EOF, we flush the comments buffer (see _flush()) - case TokenKind.TAP_TEST_POINT: - // In case of an already buffered test point, we flush it and buffer the current one - // Because diagnostic nodes are only added to the last processed test point - this.#flushBufferedTestPointNode() - - // Buffer this node (and also add any pending comments to it) - ArrayPrototypePush( - this.#bufferedTestPoints, - this.#addCommentsToCurrentNode(currentNode) - ) - break - - // Keep buffering comments until we hit a non-comment node, then add them to the that node - // Note: in case we hit and EOF, we flush the comments buffer (see _flush()) - case TokenKind.COMMENT: - ArrayPrototypePush(this.#bufferedComments, currentNode) - break - - // Diagnostic nodes are added to Test points of the same nesting level - case TokenKind.TAP_YAML_END: - // Emit either the last updated test point (w/ diagnostics) or the current diagnostics node alone - this.#emit( - this.#addDiagnosticsToLastTestPoint(currentNode) || currentNode - ) - break - - // In case we hit an EOF, we emit it to indicate the end of the stream - case TokenKind.EOF: - this.#emit(currentNode) - break - } - } - - #serializeChunk (chunk) { - return ArrayPrototypeJoin( - ArrayPrototypeMap( - // Exclude NewLine and EOF tokens - ArrayPrototypeFilter( - chunk, - (token) => - token.kind !== TokenKind.NEWLINE && token.kind !== TokenKind.EOF - ), - (token) => token.value - ), - '' - ) - } - - // --------------------------------------------------------------------------// - // ------------------------------ Parser rules ------------------------------// - // --------------------------------------------------------------------------// - - // TAPDocument := Version Plan Body | Version Body Plan - #TAPDocument (tokenChunks) { - this.#currentChunkAsString = this.#serializeChunk(tokenChunks) - const firstToken = this.#peek(false) - - if (firstToken) { - const { kind } = firstToken - - switch (kind) { - case TokenKind.TAP: - return this.#Version() - case TokenKind.NUMERIC: - return this.#Plan() - case TokenKind.TAP_TEST_OK: - case TokenKind.TAP_TEST_NOTOK: - return this.#TestPoint() - case TokenKind.COMMENT: - case TokenKind.HASH: - return this.#Comment() - case TokenKind.TAP_PRAGMA: - return this.#Pragma() - case TokenKind.WHITESPACE: - return this.#YAMLBlock() - case TokenKind.LITERAL: - // Check for "Bail out!" literal (case insensitive) - if ( - RegExpPrototypeExec(/^Bail\s+out!/i, this.#currentChunkAsString) - ) { - return this.#Bailout() - } else if (this.#isYAMLBlock) { - return this.#YAMLBlock() - } - - // Read token because error needs the last token details - this.#next(false) - this.#error('Expected a valid token') - - break - case TokenKind.EOF: - return firstToken - - case TokenKind.NEWLINE: - // Consume and ignore NewLine token - return this.#next(false) - default: - // Read token because error needs the last token details - this.#next(false) - this.#error('Expected a valid token') - } - } - - const node = { - kind: TokenKind.UNKNOWN, - node: { - value: this.#currentChunkAsString - } - } - - // We make sure the emitted node has the same shape - // both in sync and async parsing (for the stream interface) - return node - } - - // ----------------Version---------------- - // Version := "TAP version Number\n" - #Version () { - const tapToken = this.#peek() - - if (tapToken.kind === TokenKind.TAP) { - this.#next() // Consume the TAP token - } else { - this.#error('Expected "TAP" keyword') - } - - const versionToken = this.#peek() - if (versionToken?.kind === TokenKind.TAP_VERSION) { - this.#next() // Consume the version token - } else { - this.#error('Expected "version" keyword') - } - - const numberToken = this.#peek() - if (numberToken?.kind === TokenKind.NUMERIC) { - const version = this.#next().value - const node = { kind: TokenKind.TAP_VERSION, node: { version } } - return node - } - this.#error('Expected a version number') - } - - // ----------------Plan---------------- - // Plan := "1.." (Number) (" # " Reason)? "\n" - #Plan () { - // Even if specs mention plan starts at 1, we need to make sure we read the plan start value - // in case of a missing or invalid plan start value - const planStart = this.#next() - - if (planStart.kind !== TokenKind.NUMERIC) { - this.#error('Expected a plan start count') - } - - const planToken = this.#next() - if (planToken?.kind !== TokenKind.TAP_PLAN) { - this.#error('Expected ".." symbol') - } - - const planEnd = this.#next() - if (planEnd?.kind !== TokenKind.NUMERIC) { - this.#error('Expected a plan end count') - } - - const plan = { - start: planStart.value, - end: planEnd.value - } - - // Read optional reason - const hashToken = this.#peek() - if (hashToken) { - if (hashToken.kind === TokenKind.HASH) { - this.#next() // skip hash - plan.reason = StringPrototypeTrim(this.#readNextLiterals()) - } else if (hashToken.kind === TokenKind.LITERAL) { - this.#error('Expected "#" symbol before a reason') - } - } - - const node = { - kind: TokenKind.TAP_PLAN, - node: plan - } - - return node - } - - // ----------------TestPoint---------------- - // TestPoint := ("not ")? "ok" (" " Number)? ((" -")? (" " Description) )? (" " Directive)? "\n" (YAMLBlock)? - // Directive := " # " ("todo" | "skip") (" " Reason)? - // YAMLBlock := " ---\n" (YAMLLine)* " ...\n" - // YAMLLine := " " (YAML)* "\n" - - // Test Status: ok/not ok (required) - // Test number (recommended) - // Description (recommended, prefixed by " - ") - // Directive (only when necessary) - #TestPoint () { - const notToken = this.#peek() - let isTestFailed = false - - if (notToken.kind === TokenKind.TAP_TEST_NOTOK) { - this.#next() // skip "not" token - isTestFailed = true - } - - const okToken = this.#next() - if (okToken.kind !== TokenKind.TAP_TEST_OK) { - this.#error('Expected "ok" or "not ok" keyword') - } - - // Read optional test number - let numberToken = this.#peek() - if (numberToken && numberToken.kind === TokenKind.NUMERIC) { - numberToken = this.#next().value - } else { - numberToken = '' // Set an empty ID to indicate that the test hasn't provider an ID - } - - const test = { - // Output both failed and passed properties to make it easier for the checker to detect the test status - status: { - fail: isTestFailed, - pass: !isTestFailed, - todo: false, - skip: false - }, - id: numberToken, - description: '', - reason: '', - time: 0, - diagnostics: [] - } - - // Read optional description prefix " - " - const descriptionDashToken = this.#peek() - if (descriptionDashToken && descriptionDashToken.kind === TokenKind.DASH) { - this.#next() // skip dash - } - - // Read optional description - if (this.#peek()) { - const description = StringPrototypeTrim(this.#readNextLiterals()) - if (description) { - test.description = description - } - } - - // Read optional directive and reason - const hashToken = this.#peek() - if (hashToken && hashToken.kind === TokenKind.HASH) { - this.#next() // skip hash - } - - let todoOrSkipToken = this.#peek() - if (todoOrSkipToken && todoOrSkipToken.kind === TokenKind.LITERAL) { - if (RegExpPrototypeExec(/todo/i, todoOrSkipToken.value)) { - todoOrSkipToken = 'todo' - this.#next() // skip token - } else if (RegExpPrototypeExec(/skip/i, todoOrSkipToken.value)) { - todoOrSkipToken = 'skip' - this.#next() // skip token - } - } - - const reason = StringPrototypeTrim(this.#readNextLiterals()) - if (todoOrSkipToken) { - if (reason) { - test.reason = reason - } - - test.status.todo = todoOrSkipToken === 'todo' - test.status.skip = todoOrSkipToken === 'skip' - } - - const node = { - kind: TokenKind.TAP_TEST_POINT, - node: test - } - - return node - } - - // ----------------Bailout---------------- - // BailOut := "Bail out!" (" " Reason)? "\n" - #Bailout () { - this.#next() // skip "Bail" - this.#next() // skip "out!" - - // Read optional reason - const hashToken = this.#peek() - if (hashToken && hashToken.kind === TokenKind.HASH) { - this.#next() // skip hash - } - - const reason = StringPrototypeTrim(this.#readNextLiterals()) - - const node = { - kind: TokenKind.TAP_BAIL_OUT, - node: { bailout: true, reason } - } - - return node - } - - // ----------------Comment---------------- - // Comment := ^ (" ")* "#" [^\n]* "\n" - #Comment () { - const commentToken = this.#next() - if ( - commentToken.kind !== TokenKind.COMMENT && - commentToken.kind !== TokenKind.HASH - ) { - this.#error('Expected "#" symbol') - } - - const commentContent = this.#peek() - if (commentContent) { - if (RegExpPrototypeExec(/^Subtest:/i, commentContent.value) !== null) { - this.#next() // skip subtest keyword - const name = StringPrototypeTrim(this.#readNextLiterals()) - const node = { - kind: TokenKind.TAP_SUBTEST_POINT, - node: { - name - } - } - - return node - } - - const comment = StringPrototypeTrim(this.#readNextLiterals()) - const node = { - kind: TokenKind.COMMENT, - node: { comment } - } - - return node - } - - // If there is no comment content, then we ignore the current node - } - - // ----------------YAMLBlock---------------- - // YAMLBlock := " ---\n" (YAMLLine)* " ...\n" - #YAMLBlock () { - const space1 = this.#peek(false) - if (space1 && space1.kind === TokenKind.WHITESPACE) { - this.#next(false) // skip 1st space - } - - const space2 = this.#peek(false) - if (space2 && space2.kind === TokenKind.WHITESPACE) { - this.#next(false) // skip 2nd space - } - - const yamlBlockSymbol = this.#peek(false) - - if (yamlBlockSymbol.kind === TokenKind.WHITESPACE) { - if (this.#isYAMLBlock === false) { - this.#next(false) // skip 3rd space - this.#error('Expected valid YAML indentation (2 spaces)') - } - } - - if (yamlBlockSymbol.kind === TokenKind.TAP_YAML_START) { - if (this.#isYAMLBlock) { - // Looks like we have another YAML start block, but we didn't close the previous one - this.#error('Unexpected YAML start marker') - } - - this.#isYAMLBlock = true - this.#yamlCurrentIndentationLevel = this.#subTestNestingLevel - this.#lastTestPointDetails = {} - - // Consume the YAML start marker - this.#next(false) // skip "---" - - // No need to pass this token to the stream interface - return - } else if (yamlBlockSymbol.kind === TokenKind.TAP_YAML_END) { - this.#next(false) // skip "..." - - if (!this.#isYAMLBlock) { - // Looks like we have an YAML end block, but we didn't encounter any YAML start marker - this.#error('Unexpected YAML end marker') - } - - this.#isYAMLBlock = false - - const diagnostics = this.#yamlBlockBuffer - this.#yamlBlockBuffer = [] // Free the buffer for the next YAML block - - const node = { - kind: TokenKind.TAP_YAML_END, - node: { - diagnostics - } - } - - return node - } - - if (this.#isYAMLBlock) { - this.#YAMLLine() - } else { - return { - kind: TokenKind.UNKNOWN, - node: { - value: yamlBlockSymbol.value - } - } - } - } - - // ----------------YAMLLine---------------- - // YAMLLine := " " (YAML)* "\n" - #YAMLLine () { - const yamlLiteral = this.#readNextLiterals() - const { 0: key, 1: value } = StringPrototypeSplit(yamlLiteral, ':', 2) - - // Note that this.#lastTestPointDetails has been cleared when we encounter a YAML start marker - - switch (key) { - case 'duration_ms': - this.#lastTestPointDetails.duration = Number(value) - break - // Below are diagnostic properties introduced in https://github.com/nodejs/node/pull/44952 - case 'expected': - this.#lastTestPointDetails.expected = Boolean(value) - break - case 'actual': - this.#lastTestPointDetails.actual = Boolean(value) - break - case 'operator': - this.#lastTestPointDetails.operator = String(value) - break - } - - ArrayPrototypePush(this.#yamlBlockBuffer, yamlLiteral) - } - - // ----------------PRAGMA---------------- - // Pragma := "pragma " [+-] PragmaKey "\n" - // PragmaKey := ([a-zA-Z0-9_-])+ - // TODO(@manekinekko): pragmas are parsed but not used yet! TapChecker() should take care of that. - #Pragma () { - const pragmaToken = this.#next() - if (pragmaToken.kind !== TokenKind.TAP_PRAGMA) { - this.#error('Expected "pragma" keyword') - } - - const pragmas = {} - - let nextToken = this.#peek() - while ( - nextToken && - ArrayPrototypeIncludes( - [TokenKind.NEWLINE, TokenKind.EOF, TokenKind.EOL], - nextToken.kind - ) === false - ) { - let isEnabled = true - const pragmaKeySign = this.#next() - if (pragmaKeySign.kind === TokenKind.PLUS) { - isEnabled = true - } else if (pragmaKeySign.kind === TokenKind.DASH) { - isEnabled = false - } else { - this.#error('Expected "+" or "-" before pragma keys') - } - - const pragmaKeyToken = this.#peek() - if (pragmaKeyToken.kind !== TokenKind.LITERAL) { - this.#error('Expected pragma key') - } - - let pragmaKey = this.#next().value - - // In some cases, pragma key can be followed by a comma separator, - // so we need to remove it - pragmaKey = StringPrototypeReplaceAll(pragmaKey, ',', '') - - pragmas[pragmaKey] = isEnabled - nextToken = this.#peek() - } - - const node = { - kind: TokenKind.TAP_PRAGMA, - node: { - pragmas - } - } - - return node - } -} - -module.exports = { TapParser } diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 784a9c2..6e1883d 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -1,653 +1,1130 @@ -// https://github.com/nodejs/node/blob/9eb363a3e00dbba572756c7ed314273f17ea8e2e/lib/internal/test_runner/test.js - -'use strict' +const { primordials, internalBinding } = require('#lib/bootstrap'); +'use strict'; const { - ArrayPrototypeMap, ArrayPrototypePush, + ArrayPrototypePushApply, ArrayPrototypeReduce, ArrayPrototypeShift, ArrayPrototypeSlice, ArrayPrototypeSome, + ArrayPrototypeSplice, ArrayPrototypeUnshift, + ArrayPrototypeUnshiftApply, FunctionPrototype, MathMax, Number, + NumberPrototypeToFixed, + ObjectDefineProperty, ObjectSeal, + Promise, PromisePrototypeThen, PromiseResolve, + PromiseWithResolvers, ReflectApply, RegExpPrototypeExec, SafeMap, - SafeSet, SafePromiseAll, + SafePromiseAllReturnVoid, + SafePromisePrototypeFinally, SafePromiseRace, - Symbol -} = require('#internal/per_context/primordials') -const { AsyncResource } = require('async_hooks') -const { once } = require('events') -const { AbortController } = require('#internal/abort_controller') + SafeSet, + StringPrototypeStartsWith, + StringPrototypeTrim, + Symbol, + SymbolDispose, +} = primordials; +const { getCallerLocation } = internalBinding('util'); +const { exitCodes: { kGenericUserError } } = internalBinding('errors'); +const { addAbortListener } = require('#internal/events/abort_listener'); +const { queueMicrotask } = require('#internal/process/task_queues'); +const { AsyncResource } = require('async_hooks'); +const { AbortController } = require('#internal/abort_controller'); const { + AbortError, codes: { ERR_INVALID_ARG_TYPE, - ERR_TEST_FAILURE + ERR_TEST_FAILURE, }, - AbortError -} = require('#internal/errors') -const { getOptionValue } = require('#internal/options') -const { MockTracker } = require('#internal/test_runner/mock') -const { TestsStream } = require('#internal/test_runner/tests_stream') +} = require('#internal/errors'); +const { MockTracker } = require('#internal/test_runner/mock/mock'); +const { TestsStream } = require('#internal/test_runner/tests_stream'); const { - convertStringToRegExp, createDeferredCallback, - isTestFailureError -} = require('#internal/test_runner/utils') + countCompletedTest, + isTestFailureError, + reporterScope, +} = require('#internal/test_runner/utils'); const { - createDeferredPromise, kEmptyObject, - once: runOnce -} = require('#internal/util') -const { isPromise } = require('#internal/util/types') + once: runOnce, +} = require('#internal/util'); +const { isPromise } = require('#internal/util/types'); const { validateAbortSignal, validateNumber, validateOneOf, - validateUint32 -} = require('#internal/validators') -const { setTimeout } = require('#timers/promises') -const { TIMEOUT_MAX } = require('#internal/timers') -const { cpus, availableParallelism } = require('os') -const { bigint: hrtime } = process.hrtime -const kCallbackAndPromisePresent = 'callbackAndPromisePresent' -const kCancelledByParent = 'cancelledByParent' -const kParentAlreadyFinished = 'parentAlreadyFinished' -const kSubtestsFailed = 'subtestsFailed' -const kTestCodeFailure = 'testCodeFailure' -const kTestTimeoutFailure = 'testTimeoutFailure' -const kHookFailure = 'hookFailed' -const kDefaultTimeout = null -const noop = FunctionPrototype -const isTestRunner = getOptionValue('--test') -const testOnlyFlag = !isTestRunner && getOptionValue('--test-only') -const testNamePatternFlag = isTestRunner - ? null - : getOptionValue('--test-name-pattern') -const testNamePatterns = testNamePatternFlag?.length > 0 - ? ArrayPrototypeMap( - testNamePatternFlag, - (re) => convertStringToRegExp(re, '--test-name-pattern') - ) - : null -const kShouldAbort = Symbol('kShouldAbort') -const kFilename = process.argv?.[1] -const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']) + validateUint32, +} = require('#internal/validators'); +const { setTimeout } = require('timers'); +const { TIMEOUT_MAX } = require('#internal/timers'); +const { fileURLToPath } = require('#internal/url'); +const { availableParallelism } = require('os'); +const { innerOk } = require('#internal/assert/utils'); +const { bigint: hrtime } = process.hrtime; +const kCallbackAndPromisePresent = 'callbackAndPromisePresent'; +const kCancelledByParent = 'cancelledByParent'; +const kAborted = 'testAborted'; +const kParentAlreadyFinished = 'parentAlreadyFinished'; +const kSubtestsFailed = 'subtestsFailed'; +const kTestCodeFailure = 'testCodeFailure'; +const kTestTimeoutFailure = 'testTimeoutFailure'; +const kHookFailure = 'hookFailed'; +const kDefaultTimeout = null; +const noop = FunctionPrototype; +const kShouldAbort = Symbol('kShouldAbort'); +const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']); const kUnwrapErrors = new SafeSet() .add(kTestCodeFailure).add(kHookFailure) - .add('uncaughtException').add('unhandledRejection') + .add('uncaughtException').add('unhandledRejection'); +let kResistStopPropagation; +let assertObj; +let findSourceMap; +let noopTestStream; + +const kRunOnceOptions = { __proto__: null, preserveReturnValue: true }; + +function lazyFindSourceMap(file) { + if (findSourceMap === undefined) { + ({ findSourceMap } = require('#internal/source_map/source_map_cache')); + } + + return findSourceMap(file); +} + +function lazyAssertObject(harness) { + if (assertObj === undefined) { + assertObj = new SafeMap(); + const assert = require('assert'); + const methodsToCopy = [ + 'deepEqual', + 'deepStrictEqual', + 'doesNotMatch', + 'doesNotReject', + 'doesNotThrow', + 'equal', + 'fail', + 'ifError', + 'match', + 'notDeepEqual', + 'notDeepStrictEqual', + 'notEqual', + 'notStrictEqual', + 'rejects', + 'strictEqual', + 'throws', + ]; + for (let i = 0; i < methodsToCopy.length; i++) { + assertObj.set(methodsToCopy[i], assert[methodsToCopy[i]]); + } + + const { getOptionValue } = require('#internal/options'); + if (getOptionValue('--experimental-test-snapshots')) { + const { SnapshotManager } = require('#internal/test_runner/snapshot'); + harness.snapshotManager = new SnapshotManager(harness.config.updateSnapshots); + assertObj.set('snapshot', harness.snapshotManager.createAssert()); + } + } + return assertObj; +} + +function stopTest(timeout, signal) { + const deferred = PromiseWithResolvers(); + const abortListener = addAbortListener(signal, deferred.resolve); + let timer; + let disposeFunction; -function stopTest (timeout, signal) { if (timeout === kDefaultTimeout) { - return once(signal, 'abort') - } - return PromisePrototypeThen(setTimeout(timeout, null, { ref: false, signal }), () => { - throw new ERR_TEST_FAILURE( - `test timed out after ${timeout}ms`, - kTestTimeoutFailure - ) - }) + disposeFunction = abortListener[SymbolDispose]; + } else { + timer = setTimeout(() => deferred.resolve(), timeout); + timer.unref(); + + ObjectDefineProperty(deferred, 'promise', { + __proto__: null, + configurable: true, + writable: true, + value: PromisePrototypeThen(deferred.promise, () => { + throw new ERR_TEST_FAILURE( + `test timed out after ${timeout}ms`, + kTestTimeoutFailure, + ); + }), + }); + + disposeFunction = () => { + abortListener[SymbolDispose](); + timer[SymbolDispose](); + }; + } + + ObjectDefineProperty(deferred.promise, SymbolDispose, { + __proto__: null, + configurable: true, + writable: true, + value: disposeFunction, + }); + return deferred.promise; +} + +function testMatchesPattern(test, patterns) { + const matchesByNameOrParent = ArrayPrototypeSome(patterns, (re) => + RegExpPrototypeExec(re, test.name) !== null, + ) || (test.parent && testMatchesPattern(test.parent, patterns)); + if (matchesByNameOrParent) return true; + + const testNameWithAncestors = StringPrototypeTrim(test.getTestNameWithAncestors()); + + return ArrayPrototypeSome(patterns, (re) => + RegExpPrototypeExec(re, testNameWithAncestors) !== null, + ); +} + +class TestPlan { + constructor(count) { + validateUint32(count, 'count'); + this.expected = count; + this.actual = 0; + } + + check() { + if (this.actual !== this.expected) { + throw new ERR_TEST_FAILURE( + `plan expected ${this.expected} assertions but received ${this.actual}`, + kTestCodeFailure, + ); + } + } } class TestContext { - #test + #assert; + #test; - constructor (test) { - this.#test = test + constructor(test) { + this.#test = test; } - get signal () { - return this.#test.signal + get signal() { + return this.#test.signal; } - get name () { - return this.#test.name + get name() { + return this.#test.name; } - diagnostic (message) { - this.#test.diagnostic(message) + get filePath() { + return this.#test.entryFile; } - get mock () { - this.#test.mock = this.#test.mock ?? new MockTracker() - return this.#test.mock + get fullName() { + return getFullName(this.#test); } - runOnly (value) { - this.#test.runOnlySubtests = !!value + get error() { + return this.#test.error; } - skip (message) { - this.#test.skip(message) + get passed() { + return this.#test.passed; } - todo (message) { - this.#test.todo(message) + diagnostic(message) { + this.#test.diagnostic(message); } - test (name, options, fn) { - // eslint-disable-next-line no-use-before-define - const subtest = this.#test.createSubtest(Test, name, options, fn) + plan(count) { + if (this.#test.plan !== null) { + throw new ERR_TEST_FAILURE( + 'cannot set plan more than once', + kTestCodeFailure, + ); + } + + this.#test.plan = new TestPlan(count); + } + + get assert() { + if (this.#assert === undefined) { + const { plan } = this.#test; + const map = lazyAssertObject(this.#test.root.harness); + const assert = { __proto__: null }; + + this.#assert = assert; + map.forEach((method, name) => { + assert[name] = (...args) => { + if (plan !== null) { + plan.actual++; + } + return ReflectApply(method, this, args); + }; + }); + + // This is a hack. It allows the innerOk function to collect the stacktrace from the correct starting point. + function ok(...args) { + if (plan !== null) { + plan.actual++; + } + innerOk(ok, args.length, ...args); + } + + assert.ok = ok; + } + return this.#assert; + } + + get mock() { + this.#test.mock ??= new MockTracker(); + return this.#test.mock; + } + + runOnly(value) { + this.#test.runOnlySubtests = !!value; + } + + skip(message) { + this.#test.skip(message); + } + + todo(message) { + this.#test.todo(message); + } - return subtest.start() + test(name, options, fn) { + const overrides = { + __proto__: null, + loc: getCallerLocation(), + }; + + const { plan } = this.#test; + if (plan !== null) { + plan.actual++; + } + + const subtest = this.#test.createSubtest( + // eslint-disable-next-line no-use-before-define + Test, name, options, fn, overrides, + ); + + return subtest.start(); + } + + before(fn, options) { + this.#test.createHook('before', fn, { + __proto__: null, + ...options, + parent: this.#test, + hookType: 'before', + loc: getCallerLocation(), + }); } - after (fn, options) { - this.#test.createHook('after', fn, options) + after(fn, options) { + this.#test.createHook('after', fn, { + __proto__: null, + ...options, + parent: this.#test, + hookType: 'after', + loc: getCallerLocation(), + }); } - beforeEach (fn, options) { - this.#test.createHook('beforeEach', fn, options) + beforeEach(fn, options) { + this.#test.createHook('beforeEach', fn, { + __proto__: null, + ...options, + parent: this.#test, + hookType: 'beforeEach', + loc: getCallerLocation(), + }); } - afterEach (fn, options) { - this.#test.createHook('afterEach', fn, options) + afterEach(fn, options) { + this.#test.createHook('afterEach', fn, { + __proto__: null, + ...options, + parent: this.#test, + hookType: 'afterEach', + loc: getCallerLocation(), + }); + } +} + +class SuiteContext { + #suite; + + constructor(suite) { + this.#suite = suite; + } + + get signal() { + return this.#suite.signal; + } + + get name() { + return this.#suite.name; + } + + get filePath() { + return this.#suite.entryFile; + } + + get fullName() { + return getFullName(this.#suite); } } class Test extends AsyncResource { - #abortController - #outerSignal - #reportedSubtest + abortController; + outerSignal; + #reportedSubtest; - constructor (options) { - super('Test') + constructor(options) { + super('Test'); - let { fn, name, parent, skip } = options - const { concurrency, only, timeout, todo, signal } = options + let { fn, name, parent } = options; + const { concurrency, entryFile, loc, only, timeout, todo, skip, signal, plan } = options; if (typeof fn !== 'function') { - fn = noop + fn = noop; } if (typeof name !== 'string' || name === '') { - name = fn.name || '' + name = fn.name || ''; } if (!(parent instanceof Test)) { - parent = null + parent = null; } + this.name = name; + this.parent = parent; + this.testNumber = 0; + this.outputSubtestCount = 0; + this.diagnostics = []; + this.filtered = false; + this.filteredByName = false; + this.hasOnlyTests = false; + if (parent === null) { - this.concurrency = 1 - this.nesting = 0 - this.only = testOnlyFlag - this.reporter = new TestsStream() - this.runOnlySubtests = this.only - this.testNumber = 0 - this.timeout = kDefaultTimeout + this.root = this; + this.harness = options.harness; + this.config = this.harness.config; + this.concurrency = 1; + this.nesting = 0; + this.only = this.config.only; + this.reporter = new TestsStream(); + this.runOnlySubtests = this.only; + this.childNumber = 0; + this.timeout = kDefaultTimeout; + this.entryFile = entryFile; } else { - const nesting = parent.parent === null - ? parent.nesting - : parent.nesting + 1 + const nesting = parent.parent === null ? parent.nesting : + parent.nesting + 1; + const { config, isFilteringByName, isFilteringByOnly } = parent.root.harness; + + this.root = parent.root; + this.harness = null; + this.config = config; + this.concurrency = parent.concurrency; + this.nesting = nesting; + this.only = only; + this.reporter = parent.reporter; + this.runOnlySubtests = false; + this.childNumber = parent.subtests.length + 1; + this.timeout = parent.timeout; + this.entryFile = parent.entryFile; + + if (isFilteringByName) { + this.filteredByName = this.willBeFilteredByName(); + if (!this.filteredByName) { + for (let t = this.parent; t !== null && t.filteredByName; t = t.parent) { + t.filteredByName = false; + } + } + } - this.concurrency = parent.concurrency - this.nesting = nesting - this.only = only ?? !parent.runOnlySubtests - this.reporter = parent.reporter - this.runOnlySubtests = !this.only - this.testNumber = parent.subtests.length + 1 - this.timeout = parent.timeout + if (isFilteringByOnly) { + if (this.only) { + // If filtering impacts the tests within a suite, then the suite only + // runs those tests. If filtering does not impact the tests within a + // suite, then all tests are run. + this.parent.runOnlySubtests = true; + + if (this.parent === this.root || this.parent.startTime === null) { + for (let t = this.parent; t !== null && !t.hasOnlyTests; t = t.parent) { + t.hasOnlyTests = true; + } + } + } else if (this.only === false) { + fn = noop; + } + } else if (only || this.parent.runOnlySubtests) { + const warning = + "'only' and 'runOnly' require the --test-only command-line option."; + this.diagnostic(warning); + } } switch (typeof concurrency) { case 'number': - validateUint32(concurrency, 'options.concurrency', 1) - this.concurrency = concurrency - break + validateUint32(concurrency, 'options.concurrency', true); + this.concurrency = concurrency; + break; case 'boolean': if (concurrency) { - this.concurrency = parent === null - ? MathMax((typeof availableParallelism === 'undefined' ? cpus().length : availableParallelism()) - 1, 1) - : Infinity + this.concurrency = parent === null ? + MathMax(availableParallelism() - 1, 1) : Infinity; } else { - this.concurrency = 1 + this.concurrency = 1; } - break + break; default: - if (concurrency != null) throw new ERR_INVALID_ARG_TYPE('options.concurrency', 'a number or boolean', concurrency) + if (concurrency != null) + throw new ERR_INVALID_ARG_TYPE('options.concurrency', ['boolean', 'number'], concurrency); } if (timeout != null && timeout !== Infinity) { - validateNumber(timeout, 'options.timeout', 0, TIMEOUT_MAX) - this.timeout = timeout + validateNumber(timeout, 'options.timeout', 0, TIMEOUT_MAX); + this.timeout = timeout; } - if (testNamePatterns !== null) { - // eslint-disable-next-line no-use-before-define - const match = this instanceof TestHook || ArrayPrototypeSome( - testNamePatterns, - (re) => RegExpPrototypeExec(re, name) !== null - ) - - if (!match) { - skip = 'test name does not match pattern' - } + if (skip) { + fn = noop; } - if (testOnlyFlag && !this.only) { - skip = '\'only\' option not set' + this.abortController = new AbortController(); + this.outerSignal = signal; + this.signal = this.abortController.signal; + + validateAbortSignal(signal, 'options.signal'); + if (signal) { + kResistStopPropagation ??= require('#internal/event_target').kResistStopPropagation; } - if (skip) { - fn = noop - } - - this.#abortController = new AbortController() - this.#outerSignal = signal - this.signal = this.#abortController.signal - - validateAbortSignal(signal, 'options.signal') - this.#outerSignal?.addEventListener('abort', this.#abortHandler) - - this.fn = fn - this.mock = null - this.name = name - this.parent = parent - this.cancelled = false - this.skipped = !!skip - this.isTodo = !!todo - this.startTime = null - this.endTime = null - this.passed = false - this.error = null - this.diagnostics = [] - this.message = typeof skip === 'string' - ? skip - : typeof todo === 'string' ? todo : null - this.activeSubtests = 0 - this.pendingSubtests = [] - this.readySubtests = new SafeMap() - this.subtests = [] + this.outerSignal?.addEventListener( + 'abort', + this.#abortHandler, + { __proto__: null, [kResistStopPropagation]: true }, + ); + + this.fn = fn; + this.mock = null; + this.plan = null; + this.expectedAssertions = plan; + this.cancelled = false; + this.skipped = skip !== undefined && skip !== false; + this.isTodo = todo !== undefined && todo !== false; + this.startTime = null; + this.endTime = null; + this.passed = false; + this.error = null; + this.message = typeof skip === 'string' ? skip : + typeof todo === 'string' ? todo : null; + this.activeSubtests = 0; + this.pendingSubtests = []; + this.readySubtests = new SafeMap(); + this.subtests = []; + this.waitingOn = 0; + this.finished = false; this.hooks = { + __proto__: null, before: [], after: [], beforeEach: [], - afterEach: [] + afterEach: [], + ownAfterEachCount: 0, + }; + + if (loc === undefined) { + this.loc = undefined; + } else { + this.loc = { + __proto__: null, + line: loc[0], + column: loc[1], + file: loc[2], + }; + + if (this.config.sourceMaps === true) { + const map = lazyFindSourceMap(this.loc.file); + const entry = map?.findEntry(this.loc.line - 1, this.loc.column - 1); + + if (entry?.originalSource !== undefined) { + this.loc.line = entry.originalLine + 1; + this.loc.column = entry.originalColumn + 1; + this.loc.file = entry.originalSource; + } + } + + /* NOTE(RedYetiDev): The following line has been modified for node-core-test */ + if (this.loc.file && StringPrototypeStartsWith(this.loc.file, 'file://')) { + this.loc.file = fileURLToPath(this.loc.file); + } + } + } + + applyFilters() { + if (this.error) { + // Never filter out errors. + return; + } + + if (this.filteredByName) { + this.filtered = true; + return; + } + + if (this.root.harness.isFilteringByOnly && !this.only && !this.hasOnlyTests) { + if (this.parent.runOnlySubtests || + this.parent.hasOnlyTests || + this.only === false) { + this.filtered = true; + } + } + } + + willBeFilteredByName() { + const { testNamePatterns, testSkipPatterns } = this.config; + + if (testNamePatterns && !testMatchesPattern(this, testNamePatterns)) { + return true; } - this.waitingOn = 0 - this.finished = false + if (testSkipPatterns && testMatchesPattern(this, testSkipPatterns)) { + return true; + } + return false; + } + + /** + * Returns a name of the test prefixed by name of all its ancestors in ascending order, separated by a space + * Ex."grandparent parent test" + * + * It's needed to match a single test with non-unique name by pattern + */ + getTestNameWithAncestors() { + if (!this.parent) return ''; + + return `${this.parent.getTestNameWithAncestors()} ${this.name}`; } - hasConcurrency () { - return this.concurrency > this.activeSubtests + hasConcurrency() { + return this.concurrency > this.activeSubtests; } - addPendingSubtest (deferred) { - ArrayPrototypePush(this.pendingSubtests, deferred) + addPendingSubtest(deferred) { + ArrayPrototypePush(this.pendingSubtests, deferred); } - async processPendingSubtests () { + async processPendingSubtests() { while (this.pendingSubtests.length > 0 && this.hasConcurrency()) { - const deferred = ArrayPrototypeShift(this.pendingSubtests) - await deferred.test.run() - deferred.resolve() + const deferred = ArrayPrototypeShift(this.pendingSubtests); + const test = deferred.test; + test.reporter.dequeue(test.nesting, test.loc, test.name); + await test.run(); + deferred.resolve(); } } - addReadySubtest (subtest) { - this.readySubtests.set(subtest.testNumber, subtest) + addReadySubtest(subtest) { + this.readySubtests.set(subtest.childNumber, subtest); } - processReadySubtestRange (canSend) { - const start = this.waitingOn - const end = start + this.readySubtests.size + processReadySubtestRange(canSend) { + const start = this.waitingOn; + const end = start + this.readySubtests.size; for (let i = start; i < end; i++) { - const subtest = this.readySubtests.get(i) + const subtest = this.readySubtests.get(i); // Check if the specified subtest is in the map. If it is not, return // early to avoid trying to process any more tests since they would be // out of order. if (subtest === undefined) { - return + return; } // Call isClearToSend() in the loop so that it is: // - Only called if there are results to report in the correct order. // - Guaranteed to only be called a maximum of once per call to // processReadySubtestRange(). - canSend = canSend || this.isClearToSend() + canSend ||= this.isClearToSend(); if (!canSend) { - return - } - - if (i === 1 && this.parent !== null) { - this.reportStarted() + return; } // Report the subtest's results and remove it from the ready map. - subtest.finalize() - this.readySubtests.delete(i) + subtest.finalize(); + this.readySubtests.delete(i); } } - createSubtest (Factory, name, options, fn, overrides) { + createSubtest(Factory, name, options, fn, overrides) { if (typeof name === 'function') { - fn = name + fn = name; } else if (name !== null && typeof name === 'object') { - fn = options - options = name + fn = options; + options = name; } else if (typeof options === 'function') { - fn = options + fn = options; } if (options === null || typeof options !== 'object') { - options = kEmptyObject + options = kEmptyObject; } - let parent = this + let parent = this; // If this test has already ended, attach this test to the root test so // that the error can be properly reported. - const preventAddingSubtests = this.finished || this.buildPhaseFinished + const preventAddingSubtests = this.finished || this.buildPhaseFinished; if (preventAddingSubtests) { while (parent.parent !== null) { - parent = parent.parent + parent = parent.parent; } } - const test = new Factory({ __proto__: null, fn, name, parent, ...options, ...overrides }) + const test = new Factory({ __proto__: null, fn, name, parent, ...options, ...overrides }); if (parent.waitingOn === 0) { - parent.waitingOn = test.testNumber + parent.waitingOn = test.childNumber; } if (preventAddingSubtests) { - test.startTime = test.startTime || hrtime() test.fail( new ERR_TEST_FAILURE( 'test could not be started because its parent finished', - kParentAlreadyFinished - ) - ) + kParentAlreadyFinished, + ), + ); } - ArrayPrototypePush(parent.subtests, test) - return test + ArrayPrototypePush(parent.subtests, test); + return test; } #abortHandler = () => { - this.cancel(this.#outerSignal?.reason || new AbortError('The test was aborted')) - } - - cancel (error) { - if (this.endTime !== null) { - return + const error = this.outerSignal?.reason || new AbortError('The test was aborted'); + error.failureType = kAborted; + this.#cancel(error); + }; + + #cancel(error) { + if (this.endTime !== null || this.error !== null) { + return; } this.fail(error || new ERR_TEST_FAILURE( 'test did not finish before its parent and was cancelled', - kCancelledByParent - ) - ) - this.startTime = this.startTime || this.endTime // If a test was canceled before it was started, e.g inside a hook - this.cancelled = true - this.#abortController.abort() + kCancelledByParent, + ), + ); + this.cancelled = true; + this.abortController.abort(); } - createHook (name, fn, options) { - validateOneOf(name, 'hook name', kHookNames) + computeInheritedHooks() { + if (this.parent.hooks.beforeEach.length > 0) { + ArrayPrototypeUnshiftApply( + this.hooks.beforeEach, + ArrayPrototypeSlice(this.parent.hooks.beforeEach), + ); + } + + if (this.parent.hooks.afterEach.length > 0) { + ArrayPrototypePushApply( + this.hooks.afterEach, ArrayPrototypeSlice(this.parent.hooks.afterEach), + ); + } + } + + createHook(name, fn, options) { + validateOneOf(name, 'hook name', kHookNames); // eslint-disable-next-line no-use-before-define - const hook = new TestHook(fn, options) - ArrayPrototypePush(this.hooks[name], hook) - return hook + const hook = new TestHook(fn, options); + if (name === 'before' || name === 'after') { + hook.run = runOnce(hook.run, kRunOnceOptions); + } + if (name === 'before' && this.startTime !== null) { + // Test has already started, run the hook immediately + PromisePrototypeThen(hook.run(this.getRunArgs()), () => { + if (hook.error != null) { + this.fail(hook.error); + } + }); + } + if (name === 'afterEach') { + // afterEach hooks for the current test should run in the order that they + // are created. However, the current test's afterEach hooks should run + // prior to any ancestor afterEach hooks. + ArrayPrototypeSplice(this.hooks[name], this.hooks.ownAfterEachCount, 0, hook); + this.hooks.ownAfterEachCount++; + } else { + ArrayPrototypePush(this.hooks[name], hook); + } } - fail (err) { + fail(err) { if (this.error !== null) { - return + return; } - this.endTime = hrtime() - this.passed = false - this.error = err + this.passed = false; + this.error = err; } - pass () { - if (this.endTime !== null) { - return + pass() { + if (this.error !== null) { + return; } - this.endTime = hrtime() - this.passed = true + this.passed = true; } - skip (message) { - this.skipped = true - this.message = message + skip(message) { + this.skipped = true; + this.message = message; } - todo (message) { - this.isTodo = true - this.message = message + todo(message) { + this.isTodo = true; + this.message = message; } - diagnostic (message) { - ArrayPrototypePush(this.diagnostics, message) + diagnostic(message) { + ArrayPrototypePush(this.diagnostics, message); } - start () { + start() { + this.applyFilters(); + + if (this.filtered) { + noopTestStream ??= new TestsStream(); + this.reporter = noopTestStream; + this.run = this.filteredRun; + } else { + this.testNumber = ++this.parent.outputSubtestCount; + } + // If there is enough available concurrency to run the test now, then do // it. Otherwise, return a Promise to the caller and mark the test as // pending for later execution. - if (!this.parent.hasConcurrency()) { - const deferred = createDeferredPromise() + this.reporter.enqueue(this.nesting, this.loc, this.name); + if (this.root.harness.buildPromise || !this.parent.hasConcurrency()) { + const deferred = PromiseWithResolvers(); - deferred.test = this - this.parent.addPendingSubtest(deferred) - return deferred.promise + deferred.test = this; + this.parent.addPendingSubtest(deferred); + return deferred.promise; } - return this.run() + this.reporter.dequeue(this.nesting, this.loc, this.name); + return this.run(); } - [kShouldAbort] () { - if (this.signal.aborted) { - return true - } - if (this.#outerSignal?.aborted) { - this.cancel(this.#outerSignal.reason || new AbortError('The test was aborted')) - return true + [kShouldAbort]() { + if (this.signal.aborted || this.outerSignal?.aborted) { + this.#abortHandler(); + return true; } } - getRunArgs () { - const ctx = new TestContext(this) - return { ctx, args: [ctx] } + getRunArgs() { + const ctx = new TestContext(this); + return { __proto__: null, ctx, args: [ctx] }; } - async runHook (hook, args) { - validateOneOf(hook, 'hook name', kHookNames) + async runHook(hook, args) { + validateOneOf(hook, 'hook name', kHookNames); try { await ArrayPrototypeReduce(this.hooks[hook], async (prev, hook) => { - await prev - await hook.run(args) + await prev; + await hook.run(args); if (hook.error) { - throw hook.error + throw hook.error; } - }, PromiseResolve()) + }, PromiseResolve()); } catch (err) { - const error = new ERR_TEST_FAILURE(`failed running ${hook} hook`, kHookFailure) - error.cause = isTestFailureError(err) ? err.cause : err - throw error + const error = new ERR_TEST_FAILURE(`failed running ${hook} hook`, kHookFailure); + error.cause = isTestFailureError(err) ? err.cause : err; + throw error; } } - async run () { + async filteredRun() { + this.pass(); + this.subtests = []; + this.report = noop; + queueMicrotask(() => this.postRun()); + } + + async run() { if (this.parent !== null) { - this.parent.activeSubtests++ + this.parent.activeSubtests++; + this.computeInheritedHooks(); } - this.startTime = hrtime() + this.startTime ??= hrtime(); if (this[kShouldAbort]()) { - this.postRun() - return + this.postRun(); + return; } - const { args, ctx } = this.getRunArgs() - const after = runOnce(async () => { + const hookArgs = this.getRunArgs(); + const { args, ctx } = hookArgs; + + if (this.plan === null && this.expectedAssertions) { + ctx.plan(this.expectedAssertions); + } + + const after = async () => { if (this.hooks.after.length > 0) { - await this.runHook('after', { args, ctx }) + await this.runHook('after', hookArgs); } - }) + }; + + const afterEach = runOnce(async () => { - if (this.parent?.hooks.afterEach.length > 0) { - await this.parent.runHook('afterEach', { args, ctx }) + if (this.parent?.hooks.afterEach.length > 0 && !this.skipped) { + await this.parent.runHook('afterEach', hookArgs); } - }) + }, kRunOnceOptions); + + let stopPromise; + try { - if (this.parent?.hooks.beforeEach.length > 0) { - await this.parent.runHook('beforeEach', { args, ctx }) + if (this.parent?.hooks.before.length > 0) { + // This hook usually runs immediately, we need to wait for it to finish + await this.parent.runHook('before', this.parent.getRunArgs()); + } + if (this.parent?.hooks.beforeEach.length > 0 && !this.skipped) { + await this.parent.runHook('beforeEach', hookArgs); } - const stopPromise = stopTest(this.timeout, this.signal) - const runArgs = ArrayPrototypeSlice(args) - ArrayPrototypeUnshift(runArgs, this.fn, ctx) + stopPromise = stopTest(this.timeout, this.signal); + const runArgs = ArrayPrototypeSlice(args); + ArrayPrototypeUnshift(runArgs, this.fn, ctx); if (this.fn.length === runArgs.length - 1) { // This test is using legacy Node.js error first callbacks. - const { promise, cb } = createDeferredCallback() + const { promise, cb } = createDeferredCallback(); - ArrayPrototypePush(runArgs, cb) - const ret = ReflectApply(this.runInAsyncScope, this, runArgs) + ArrayPrototypePush(runArgs, cb); + const ret = ReflectApply(this.runInAsyncScope, this, runArgs); if (isPromise(ret)) { this.fail(new ERR_TEST_FAILURE( 'passed a callback but also returned a Promise', - kCallbackAndPromisePresent - )) - await SafePromiseRace([ret, stopPromise]) + kCallbackAndPromisePresent, + )); + await SafePromiseRace([ret, stopPromise]); } else { - await SafePromiseRace([PromiseResolve(promise), stopPromise]) + await SafePromiseRace([PromiseResolve(promise), stopPromise]); } } else { // This test is synchronous or using Promises. - const promise = ReflectApply(this.runInAsyncScope, this, runArgs) - await SafePromiseRace([PromiseResolve(promise), stopPromise]) - } - - if (this[kShouldAbort]()) { - this.postRun() - return + const promise = ReflectApply(this.runInAsyncScope, this, runArgs); + await SafePromiseRace([PromiseResolve(promise), stopPromise]); } - await after() - await afterEach() - this.pass() + this[kShouldAbort](); + this.plan?.check(); + this.pass(); + await afterEach(); + await after(); } catch (err) { - try { await after() } catch { /* Ignore error. */ } - try { await afterEach() } catch { /* test is already failing, let's the error */ } if (isTestFailureError(err)) { if (err.failureType === kTestTimeoutFailure) { - this.cancel(err) + this.#cancel(err); } else { - this.fail(err) + this.fail(err); } } else { - this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)) + this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)); + } + try { await afterEach(); } catch { /* test is already failing, let's ignore the error */ } + try { await after(); } catch { /* Ignore error. */ } + } finally { + stopPromise?.[SymbolDispose](); + + // Do not abort hooks and the root test as hooks instance are shared between tests suite so aborting them will + // cause them to not run for further tests. + if (this.parent !== null) { + this.abortController.abort(); } } - // Clean up the test. Then, try to report the results and execute any - // tests that were pending due to available concurrency. - this.postRun() - } - - postRun (pendingSubtestsError) { - const counters = { __proto__: null, failed: 0, passed: 0, cancelled: 0, skipped: 0, todo: 0, totalFailed: 0 } + if (this.parent !== null || typeof this.hookType === 'string') { + // Clean up the test. Then, try to report the results and execute any + // tests that were pending due to available concurrency. + // + // The root test is skipped here because it is a special case. Its + // postRun() method is called when the process is getting ready to exit. + // This helps catch any asynchronous activity that occurs after the tests + // have finished executing. + this.postRun(); + } else if (this.config.forceExit) { + // This is the root test, and all known tests and hooks have finished + // executing. If the user wants to force exit the process regardless of + // any remaining ref'ed handles, then do that now. It is theoretically + // possible that a ref'ed handle could asynchronously create more tests, + // but the user opted into this behavior. + const promises = []; + + for (let i = 0; i < reporterScope.reporters.length; i++) { + const { destination } = reporterScope.reporters[i]; + + ArrayPrototypePush(promises, new Promise((resolve) => { + destination.on('unpipe', () => { + if (!destination.closed && typeof destination.close === 'function') { + destination.close(resolve); + } else { + resolve(); + } + }); + })); + } - // If the test was failed before it even started, then the end time will - // be earlier than the start time. Correct that here. - if (this.endTime < this.startTime) { - this.endTime = hrtime() + this.harness.teardown(); + await SafePromiseAllReturnVoid(promises); + process.exit(); } - if (this.startTime == null) this.startTime = this.endTime + + } + + postRun(pendingSubtestsError) { + // If the test was cancelled before it started, then the start and end + // times need to be corrected. + this.endTime ??= hrtime(); + this.startTime ??= this.endTime; // The test has run, so recursively cancel any outstanding subtests and // mark this test as failed if any subtests failed. - this.pendingSubtests = [] + this.pendingSubtests = []; + let failed = 0; for (let i = 0; i < this.subtests.length; i++) { - const subtest = this.subtests[i] + const subtest = this.subtests[i]; if (!subtest.finished) { - subtest.cancel(pendingSubtestsError) - subtest.postRun(pendingSubtestsError) - } - - // Check SKIP and TODO tests first, as those should not be counted as - // failures. - if (subtest.skipped) { - counters.skipped++ - } else if (subtest.isTodo) { - counters.todo++ - } else if (subtest.cancelled) { - counters.cancelled++ - } else if (!subtest.passed) { - counters.failed++ - } else { - counters.passed++ + subtest.#cancel(pendingSubtestsError); + subtest.postRun(pendingSubtestsError); } - - if (!subtest.passed) { - counters.totalFailed++ + if (!subtest.passed && !subtest.isTodo) { + failed++; } } - if ((this.passed || this.parent === null) && counters.totalFailed > 0) { - const subtestString = `subtest${counters.totalFailed > 1 ? 's' : ''}` - const msg = `${counters.totalFailed} ${subtestString} failed` + if ((this.passed || this.parent === null) && failed > 0) { + const subtestString = `subtest${failed > 1 ? 's' : ''}`; + const msg = `${failed} ${subtestString} failed`; - this.fail(new ERR_TEST_FAILURE(msg, kSubtestsFailed)) + this.fail(new ERR_TEST_FAILURE(msg, kSubtestsFailed)); } - this.#outerSignal?.removeEventListener('abort', this.#abortHandler) - this.mock?.reset() + this.outerSignal?.removeEventListener('abort', this.#abortHandler); + this.mock?.reset(); if (this.parent !== null) { - this.parent.activeSubtests-- - this.parent.addReadySubtest(this) - this.parent.processReadySubtestRange(false) - this.parent.processPendingSubtests() + if (!this.filtered) { + const report = this.getReportDetails(); + report.details.passed = this.passed; + this.testNumber ||= ++this.parent.outputSubtestCount; + this.reporter.complete(this.nesting, this.loc, this.testNumber, this.name, report.details, report.directive); + this.parent.activeSubtests--; + } + + this.parent.addReadySubtest(this); + this.parent.processReadySubtestRange(false); + this.parent.processPendingSubtests(); } else if (!this.reported) { - this.reported = true - this.reporter.plan(this.nesting, kFilename, this.subtests.length) + const { + diagnostics, + harness, + loc, + nesting, + reporter, + } = this; + + this.reported = true; + reporter.plan(nesting, loc, harness.counters.topLevel); + + // Call this harness.coverage() before collecting diagnostics, since failure to collect coverage is a diagnostic. + const coverage = harness.coverage(); + harness.snapshotManager?.writeSnapshotFiles(); + for (let i = 0; i < diagnostics.length; i++) { + reporter.diagnostic(nesting, loc, diagnostics[i]); + } - for (let i = 0; i < this.diagnostics.length; i++) { - this.reporter.diagnostic(this.nesting, kFilename, this.diagnostics[i]) + const duration = this.duration(); + reporter.diagnostic(nesting, loc, `tests ${harness.counters.tests}`); + reporter.diagnostic(nesting, loc, `suites ${harness.counters.suites}`); + reporter.diagnostic(nesting, loc, `pass ${harness.counters.passed}`); + reporter.diagnostic(nesting, loc, `fail ${harness.counters.failed}`); + reporter.diagnostic(nesting, loc, `cancelled ${harness.counters.cancelled}`); + reporter.diagnostic(nesting, loc, `skipped ${harness.counters.skipped}`); + reporter.diagnostic(nesting, loc, `todo ${harness.counters.todo}`); + reporter.diagnostic(nesting, loc, `duration_ms ${duration}`); + + if (coverage) { + const coverages = [ + { __proto__: null, actual: coverage.totals.coveredLinePercent, + threshold: this.config.lineCoverage, name: 'line' }, + + { __proto__: null, actual: coverage.totals.coveredBranchPercent, + threshold: this.config.branchCoverage, name: 'branch' }, + + { __proto__: null, actual: coverage.totals.coveredFunctionPercent, + threshold: this.config.functionCoverage, name: 'function' }, + ]; + + for (let i = 0; i < coverages.length; i++) { + const { threshold, actual, name } = coverages[i]; + if (actual < threshold) { + harness.success = false; + process.exitCode = kGenericUserError; + reporter.diagnostic(nesting, loc, `Error: ${NumberPrototypeToFixed(actual, 2)}% ${name} coverage does not meet threshold of ${threshold}%.`); + } + } + + reporter.coverage(nesting, loc, coverage); } - this.reporter.diagnostic(this.nesting, kFilename, `tests ${this.subtests.length}`) - this.reporter.diagnostic(this.nesting, kFilename, `pass ${counters.passed}`) - this.reporter.diagnostic(this.nesting, kFilename, `fail ${counters.failed}`) - this.reporter.diagnostic(this.nesting, kFilename, `cancelled ${counters.cancelled}`) - this.reporter.diagnostic(this.nesting, kFilename, `skipped ${counters.skipped}`) - this.reporter.diagnostic(this.nesting, kFilename, `todo ${counters.todo}`) - this.reporter.diagnostic(this.nesting, kFilename, `duration_ms ${this.#duration()}`) - this.reporter.push(null) + reporter.summary( + nesting, loc?.file, harness.success, harness.counters, duration, + ); + + if (harness.watching) { + this.reported = false; + harness.resetCounters(); + assertObj = undefined; + } else { + reporter.end(); + } } } - isClearToSend () { + isClearToSend() { return this.parent === null || ( - this.parent.waitingOn === this.testNumber && this.parent.isClearToSend() - ) + this.parent.waitingOn === this.childNumber && this.parent.isClearToSend() + ); } - finalize () { + finalize() { // By the time this function is called, the following can be relied on: // - The current test has completed or been cancelled. // - All of this test's subtests have completed or been cancelled. @@ -656,162 +1133,234 @@ class Test extends AsyncResource { // Report any subtests that have not been reported yet. Since all of the // subtests have finished, it's safe to pass true to // processReadySubtestRange(), which will finalize all remaining subtests. - this.processReadySubtestRange(true) + this.processReadySubtestRange(true); // Output this test's results and update the parent's waiting counter. - this.report() - this.parent.waitingOn++ - this.finished = true + this.report(); + this.parent.waitingOn++; + this.finished = true; + + if (this.parent === this.root && + this.root.waitingOn > this.root.subtests.length) { + // At this point all of the tests have finished running. However, there + // might be ref'ed handles keeping the event loop alive. This gives the + // global after() hook a chance to clean them up. The user may also + // want to force the test runner to exit despite ref'ed handles. + this.root.run(); + } } - #duration () { + duration() { // Duration is recorded in BigInt nanoseconds. Convert to milliseconds. - return Number(this.endTime - this.startTime) / 1_000_000 + return Number(this.endTime - this.startTime) / 1_000_000; } - report () { - if (this.subtests.length > 0) { - this.reporter.plan(this.subtests[0].nesting, kFilename, this.subtests.length) - } else { - this.reportStarted() - } - let directive - const details = { __proto__: null, duration_ms: this.#duration() } + getReportDetails() { + let directive; + + const details = { __proto__: null, duration_ms: this.duration() }; if (this.skipped) { - directive = this.reporter.getSkip(this.message) + directive = this.reporter.getSkip(this.message); } else if (this.isTodo) { - directive = this.reporter.getTodo(this.message) + directive = this.reporter.getTodo(this.message); } + if (this.reportedType) { + details.type = this.reportedType; + } + if (!this.passed) { + details.error = this.error; + } + return { __proto__: null, details, directive }; + } + + report() { + countCompletedTest(this); + if (this.outputSubtestCount > 0) { + this.reporter.plan(this.subtests[0].nesting, this.loc, this.outputSubtestCount); + } else { + this.reportStarted(); + } + const report = this.getReportDetails(); + if (this.passed) { - this.reporter.ok(this.nesting, kFilename, this.testNumber, this.name, details, directive) + this.reporter.ok(this.nesting, this.loc, this.testNumber, this.name, report.details, report.directive); } else { - details.error = this.error - this.reporter.fail(this.nesting, kFilename, this.testNumber, this.name, details, directive) + this.reporter.fail(this.nesting, this.loc, this.testNumber, this.name, report.details, report.directive); } for (let i = 0; i < this.diagnostics.length; i++) { - this.reporter.diagnostic(this.nesting, kFilename, this.diagnostics[i]) + this.reporter.diagnostic(this.nesting, this.loc, this.diagnostics[i]); } } - reportStarted () { + reportStarted() { if (this.#reportedSubtest || this.parent === null) { - return + return; } - this.#reportedSubtest = true - this.parent.reportStarted() - this.reporter.start(this.nesting, kFilename, this.name) + this.#reportedSubtest = true; + this.parent.reportStarted(); + this.reporter.start(this.nesting, this.loc, this.name); } } class TestHook extends Test { - #args - constructor (fn, options) { - if (options === null || typeof options !== 'object') { - options = kEmptyObject - } - const { timeout, signal } = options - super({ __proto__: null, fn, timeout, signal }) + #args; + constructor(fn, options) { + const { hookType, loc, parent, timeout, signal } = options; + super({ + __proto__: null, + fn, + loc, + timeout, + signal, + harness: parent.root.harness, + }); + this.parentTest = parent; + this.hookType = hookType; } + run(args) { + if (this.error && !this.outerSignal?.aborted) { + this.passed = false; + this.error = null; + this.abortController.abort(); + this.abortController = new AbortController(); + this.signal = this.abortController.signal; + } - run (args) { - this.#args = args - return super.run() + this.#args = args; + return super.run(); } - - getRunArgs () { - return this.#args + getRunArgs() { + return this.#args; } - - postRun () { + willBeFilteredByName() { + return false; } -} + postRun() { + const { error, loc, parentTest: parent } = this; -class ItTest extends Test { - constructor (opt) { super(opt) } // eslint-disable-line no-useless-constructor - getRunArgs () { - return { ctx: { signal: this.signal, name: this.name }, args: [] } + // Report failures in the root test's after() hook. + if (error && parent === parent.root && this.hookType === 'after') { + if (isTestFailureError(error)) { + error.failureType = kHookFailure; + } + + this.endTime ??= hrtime(); + parent.reporter.fail(0, loc, parent.subtests.length + 1, loc.file, { + __proto__: null, + duration_ms: this.duration(), + error, + }, undefined); + } } } class Suite extends Test { - constructor (options) { - super(options) + reportedType = 'suite'; + constructor(options) { + super(options); + + if (this.config.testNamePatterns !== null && + this.config.testSkipPatterns !== null && + !options.skip) { + this.fn = options.fn || this.fn; + this.skipped = false; + } try { - const { ctx, args } = this.getRunArgs() - this.buildSuite = PromisePrototypeThen( - PromiseResolve(this.runInAsyncScope(this.fn, ctx, args)), - undefined, - (err) => { - this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)) - }) + const { ctx, args } = this.getRunArgs(); + const runArgs = [this.fn, ctx]; + ArrayPrototypePushApply(runArgs, args); + this.buildSuite = SafePromisePrototypeFinally( + PromisePrototypeThen( + PromiseResolve(ReflectApply(this.runInAsyncScope, this, runArgs)), + undefined, + (err) => { + this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)); + }), + () => this.postBuild(), + ); } catch (err) { - this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)) + this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)); + this.postBuild(); } - this.fn = () => {} - this.buildPhaseFinished = true + this.fn = noop; } - getRunArgs () { - return { ctx: { signal: this.signal, name: this.name }, args: [] } + postBuild() { + this.buildPhaseFinished = true; } - async run () { - const hookArgs = this.getRunArgs() - const afterEach = runOnce(async () => { - if (this.parent?.hooks.afterEach.length > 0) { - await this.parent.runHook('afterEach', hookArgs) - } - }) + getRunArgs() { + const ctx = new SuiteContext(this); + return { __proto__: null, ctx, args: [ctx] }; + } + + async run() { + this.computeInheritedHooks(); + const hookArgs = this.getRunArgs(); + let stopPromise; + const after = runOnce(() => this.runHook('after', hookArgs), kRunOnceOptions); try { - this.parent.activeSubtests++ - await this.buildSuite - this.startTime = hrtime() + this.parent.activeSubtests++; + await this.buildSuite; + this.startTime = hrtime(); if (this[kShouldAbort]()) { - this.subtests = [] - this.postRun() - return + this.subtests = []; + this.postRun(); + return; } - if (this.parent?.hooks.beforeEach.length > 0) { - await this.parent.runHook('beforeEach', hookArgs) + if (this.parent.hooks.before.length > 0) { + await this.parent.runHook('before', this.parent.getRunArgs()); } - await this.runHook('before', hookArgs) + await this.runHook('before', hookArgs); - const stopPromise = stopTest(this.timeout, this.signal) - const subtests = this.skipped || this.error ? [] : this.subtests - const promise = SafePromiseAll(subtests, (subtests) => subtests.start()) + stopPromise = stopTest(this.timeout, this.signal); + const subtests = this.skipped || this.error ? [] : this.subtests; + const promise = SafePromiseAll(subtests, (subtests) => subtests.start()); - await SafePromiseRace([promise, stopPromise]) - await this.runHook('after', hookArgs) - await afterEach() + await SafePromiseRace([promise, stopPromise]); + await after(); - this.pass() + this.pass(); } catch (err) { - try { await afterEach() } catch { /* test is already failing, let's the error */ } + try { await after(); } catch { /* suite is already failing */ } if (isTestFailureError(err)) { - this.fail(err) + this.fail(err); } else { - this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)) + this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)); } + } finally { + stopPromise?.[SymbolDispose](); } - this.postRun() + this.postRun(); } } +function getFullName(test) { + let fullName = test.name; + + for (let t = test.parent; t !== t.root; t = t.parent) { + fullName = `${t.name} > ${fullName}`; + } + + return fullName; +} + module.exports = { - ItTest, kCancelledByParent, kSubtestsFailed, kTestCodeFailure, + kTestTimeoutFailure, + kAborted, kUnwrapErrors, Suite, - Test -} + Test, +}; diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js index b8b5b09..af2faef 100644 --- a/lib/internal/test_runner/tests_stream.js +++ b/lib/internal/test_runner/tests_stream.js @@ -1,75 +1,178 @@ -// https://github.com/nodejs/node/blob/9eb363a3e00dbba572756c7ed314273f17ea8e2e/lib/internal/test_runner/tests_stream.js -'use strict' +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; const { ArrayPrototypePush, - ArrayPrototypeShift -} = require('#internal/per_context/primordials') -const { Readable } = require('readable-stream') + ArrayPrototypeShift, + NumberMAX_SAFE_INTEGER, + Symbol, +} = primordials; +const Readable = require('#internal/streams/readable'); -class TestsStream extends Readable { - #buffer - #canPush +/* NOTE(RedYetiDev): The following line has been modified for node-core-test */ +const { spec } = require('#node:test/reporters'); - constructor () { - super({ objectMode: true }) - this.#buffer = [] - this.#canPush = true +const kEmitMessage = Symbol('kEmitMessage'); +class TestsStream extends Readable { + #buffer; + #canPush; + + constructor() { + super({ + __proto__: null, + objectMode: true, + highWaterMark: NumberMAX_SAFE_INTEGER, + }); + this.#buffer = []; + this.#canPush = true; } - _read () { - this.#canPush = true + _read() { + this.#canPush = true; while (this.#buffer.length > 0) { - const obj = ArrayPrototypeShift(this.#buffer) + const obj = ArrayPrototypeShift(this.#buffer); if (!this.#tryPush(obj)) { - return + return; } } } - fail (nesting, file, testNumber, name, details, directive) { - this.#emit('test:fail', { __proto__: null, name, nesting, file, testNumber, details, ...directive }) + fail(nesting, loc, testNumber, name, details, directive) { + this[kEmitMessage]('test:fail', { + __proto__: null, + name, + nesting, + testNumber, + details, + ...loc, + ...directive, + }); + } + + ok(nesting, loc, testNumber, name, details, directive) { + this[kEmitMessage]('test:pass', { + __proto__: null, + name, + nesting, + testNumber, + details, + ...loc, + ...directive, + }); + } + + complete(nesting, loc, testNumber, name, details, directive) { + this[kEmitMessage]('test:complete', { + __proto__: null, + name, + nesting, + testNumber, + details, + ...loc, + ...directive, + }); + } + + plan(nesting, loc, count) { + this[kEmitMessage]('test:plan', { + __proto__: null, + nesting, + count, + ...loc, + }); } - ok (nesting, file, testNumber, name, details, directive) { - this.#emit('test:pass', { __proto__: null, name, nesting, file, testNumber, details, ...directive }) + getSkip(reason = undefined) { + return { __proto__: null, skip: reason ?? true }; } - plan (nesting, file, count) { - this.#emit('test:plan', { __proto__: null, nesting, file, count }) + getTodo(reason = undefined) { + return { __proto__: null, todo: reason ?? true }; } - getSkip (reason = undefined) { - return { __proto__: null, skip: reason ?? true } + enqueue(nesting, loc, name) { + this[kEmitMessage]('test:enqueue', { + __proto__: null, + nesting, + name, + ...loc, + }); } - getTodo (reason = undefined) { - return { __proto__: null, todo: reason ?? true } + dequeue(nesting, loc, name) { + this[kEmitMessage]('test:dequeue', { + __proto__: null, + nesting, + name, + ...loc, + }); } - start (nesting, file, name) { - this.#emit('test:start', { __proto__: null, nesting, file, name }) + start(nesting, loc, name) { + this[kEmitMessage]('test:start', { + __proto__: null, + nesting, + name, + ...loc, + }); } - diagnostic (nesting, file, message) { - this.#emit('test:diagnostic', { __proto__: null, nesting, file, message }) + diagnostic(nesting, loc, message) { + this[kEmitMessage]('test:diagnostic', { + __proto__: null, + nesting, + message, + ...loc, + }); } - #emit (type, data) { - this.emit(type, data) - this.#tryPush({ type, data }) + coverage(nesting, loc, summary) { + this[kEmitMessage]('test:coverage', { + __proto__: null, + nesting, + summary, + ...loc, + }); } - #tryPush (message) { + summary(nesting, file, success, counts, duration_ms) { + this[kEmitMessage]('test:summary', { + __proto__: null, + success, + counts, + duration_ms, + file, + }); + } + + end() { + this.#tryPush(null); + } + + [kEmitMessage](type, data) { + this.emit(type, data); + // Disabling as this going to the user-land + // eslint-disable-next-line node-core/set-proto-to-null-in-object + this.#tryPush({ type, data }); + } + + #tryPush(message) { if (this.#canPush) { - this.#canPush = this.push(message) + this.#canPush = this.push(message); } else { - ArrayPrototypePush(this.#buffer, message) + ArrayPrototypePush(this.#buffer, message); } - return this.#canPush + return this.#canPush; + } + + /* NOTE(RedYetiDev): The following lines have been modified for node-core-test */ + compose(stream, ...args) { + if (stream === spec) stream = spec(); + return super.compose(stream, ...args); } } -module.exports = { TestsStream } +module.exports = { TestsStream, kEmitMessage }; diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 7fe4ca2..39092a1 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -1,45 +1,72 @@ -// https://github.com/nodejs/node/blob/4788c0d2a02a2e7c7088c6f39d9f13a4209ba863/lib/internal/test_runner/utils.js -'use strict' +const { primordials, internalBinding } = require('#lib/bootstrap'); + +'use strict'; const { + ArrayPrototypeFlatMap, + ArrayPrototypeForEach, + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypePop, ArrayPrototypePush, + ArrayPrototypeReduce, + ArrayPrototypeSome, + MathFloor, + MathMax, + MathMin, + NumberParseInt, + NumberPrototypeToFixed, ObjectGetOwnPropertyDescriptor, - SafePromiseAllReturnArrayLike, + PromiseWithResolvers, RegExp, RegExpPrototypeExec, - SafeMap -} = require('#internal/per_context/primordials') -const { basename } = require('path') -const { createWriteStream } = require('fs') -const { createDeferredPromise } = require('#internal/util') -const { getOptionValue } = require('#internal/options') + SafeMap, + SafePromiseAllReturnArrayLike, + StringPrototypePadEnd, + StringPrototypePadStart, + StringPrototypeRepeat, + StringPrototypeSlice, + StringPrototypeSplit, +} = primordials; + +const { AsyncResource } = require('async_hooks'); +const { relative, sep } = require('path'); +const { createWriteStream } = require('fs'); +const { pathToFileURL } = require('#internal/url'); +const { getOptionValue } = require('#internal/options'); +const { green, yellow, red, white, shouldColorize } = require('#internal/util/colors'); const { codes: { ERR_INVALID_ARG_VALUE, - ERR_TEST_FAILURE + ERR_TEST_FAILURE, }, - kIsNodeError -} = require('#internal/errors') -const { compose } = require('readable-stream') - -const kMultipleCallbackInvocations = 'multipleCallbackInvocations' -const kRegExpPattern = /^\/(.*)\/([a-z]*)$/ -const kSupportedFileExtensions = /\.[cm]?js$/ -const kTestFilePattern = /((^test(-.+)?)|(.+[.\-_]test))\.[cm]?js$/ - -function doesPathMatchFilter (p) { - return RegExpPrototypeExec(kTestFilePattern, basename(p)) !== null + kIsNodeError, +} = require('#internal/errors'); +const { compose } = require('stream'); +const { validateInteger } = require('#internal/validators'); + +const coverageColors = { + __proto__: null, + high: green, + medium: yellow, + low: red, +}; + +const kMultipleCallbackInvocations = 'multipleCallbackInvocations'; +const kRegExpPattern = /^\/(.*)\/([a-z]*)$/; + +const kPatterns = ['test', 'test/**/*', 'test-*', '*[._-]test']; +const kFileExtensions = ['js', 'mjs', 'cjs']; +if (getOptionValue('--experimental-strip-types')) { + ArrayPrototypePush(kFileExtensions, 'ts', 'mts', 'cts'); } +const kDefaultPattern = `**/{${ArrayPrototypeJoin(kPatterns, ',')}}.{${ArrayPrototypeJoin(kFileExtensions, ',')}}`; -function isSupportedFileType (p) { - return RegExpPrototypeExec(kSupportedFileExtensions, p) !== null -} - -function createDeferredCallback () { - let calledCount = 0 - const { promise, resolve, reject } = createDeferredPromise() +function createDeferredCallback() { + let calledCount = 0; + const { promise, resolve, reject } = PromiseWithResolvers(); const cb = (err) => { - calledCount++ + calledCount++; // If the callback is called a second time, let the user know, but // don't let them know more than once. @@ -47,125 +74,536 @@ function createDeferredCallback () { if (calledCount === 2) { throw new ERR_TEST_FAILURE( 'callback invoked multiple times', - kMultipleCallbackInvocations - ) + kMultipleCallbackInvocations, + ); } - return + return; } if (err) { - return reject(err) + return reject(err); } - resolve() - } + resolve(); + }; - return { promise, cb } + return { __proto__: null, promise, cb }; } -function isTestFailureError (err) { - return err?.code === 'ERR_TEST_FAILURE' && kIsNodeError in err +function isTestFailureError(err) { + return err?.code === 'ERR_TEST_FAILURE' && kIsNodeError in err; } -function convertStringToRegExp (str, name) { - const match = RegExpPrototypeExec(kRegExpPattern, str) - const pattern = match?.[1] ?? str - const flags = match?.[2] || '' +function convertStringToRegExp(str, name) { + const match = RegExpPrototypeExec(kRegExpPattern, str); + const pattern = match?.[1] ?? str; + const flags = match?.[2] || ''; try { - return new RegExp(pattern, flags) + return new RegExp(pattern, flags); } catch (err) { - const msg = err?.message + const msg = err?.message; throw new ERR_INVALID_ARG_VALUE( name, str, - `is an invalid regular expression.${msg ? ` ${msg}` : ''}` - ) + `is an invalid regular expression.${msg ? ` ${msg}` : ''}`, + ); } } const kBuiltinDestinations = new SafeMap([ ['stdout', process.stdout], - ['stderr', process.stderr] -]) + ['stderr', process.stderr], +]); const kBuiltinReporters = new SafeMap([ ['spec', '#internal/test_runner/reporter/spec'], ['dot', '#internal/test_runner/reporter/dot'], - ['tap', '#internal/test_runner/reporter/tap'] -]) + ['tap', '#internal/test_runner/reporter/tap'], + ['junit', '#internal/test_runner/reporter/junit'], + ['lcov', '#internal/test_runner/reporter/lcov'], +]); -const kDefaultReporter = 'tap' -const kDefaultDestination = 'stdout' +const kDefaultReporter = 'spec'; +const kDefaultDestination = 'stdout'; -function tryBuiltinReporter (name) { - const builtinPath = kBuiltinReporters.get(name) +function tryBuiltinReporter(name) { + const builtinPath = kBuiltinReporters.get(name); if (builtinPath === undefined) { - return + return; } - return require(builtinPath) + return require(builtinPath); } -async function getReportersMap (reporters, destinations) { +function shouldColorizeTestFiles(destinations) { + // This function assumes only built-in destinations (stdout/stderr) supports coloring + return ArrayPrototypeSome(destinations, (_, index) => { + const destination = kBuiltinDestinations.get(destinations[index]); + return destination && shouldColorize(destination); + }); +} + +async function getReportersMap(reporters, destinations) { return SafePromiseAllReturnArrayLike(reporters, async (name, i) => { - const destination = kBuiltinDestinations.get(destinations[i]) ?? createWriteStream(destinations[i]) + const destination = kBuiltinDestinations.get(destinations[i]) ?? + createWriteStream(destinations[i], { __proto__: null, flush: true }); // Load the test reporter passed to --test-reporter - let reporter = tryBuiltinReporter(name) + let reporter = tryBuiltinReporter(name); if (reporter === undefined) { - reporter = await import(name) + let parentURL; + + try { + parentURL = pathToFileURL(process.cwd() + '/').href; + } catch { + parentURL = 'file:///'; + } + + const cascadedLoader = require('#internal/modules/esm/loader').getOrInitializeCascadedLoader(); + reporter = await cascadedLoader.import(name, parentURL, { __proto__: null }); } if (reporter?.default) { - reporter = reporter.default + reporter = reporter.default; } if (reporter?.prototype && ObjectGetOwnPropertyDescriptor(reporter.prototype, 'constructor')) { - reporter = new reporter() // eslint-disable-line new-cap + reporter = new reporter(); } if (!reporter) { - throw new ERR_INVALID_ARG_VALUE('Reporter', name, 'is not a valid reporter') + throw new ERR_INVALID_ARG_VALUE('Reporter', name, 'is not a valid reporter'); + } + + return { __proto__: null, reporter, destination }; + }); +} + +const reporterScope = new AsyncResource('TestReporterScope'); +let globalTestOptions; + +function parseCommandLine() { + if (globalTestOptions) { + return globalTestOptions; + } + + const isTestRunner = getOptionValue('--test'); + const coverage = getOptionValue('--experimental-test-coverage'); + const forceExit = getOptionValue('--test-force-exit'); + const sourceMaps = getOptionValue('--enable-source-maps'); + const updateSnapshots = getOptionValue('--test-update-snapshots'); + const watch = getOptionValue('--watch'); + const isChildProcess = process.env.NODE_TEST_CONTEXT === 'child'; + const isChildProcessV8 = process.env.NODE_TEST_CONTEXT === 'child-v8'; + let concurrency; + let coverageExcludeGlobs; + let coverageIncludeGlobs; + let lineCoverage; + let branchCoverage; + let functionCoverage; + let destinations; + let isolation; + let only = getOptionValue('--test-only'); + let reporters; + let shard; + let testNamePatterns = mapPatternFlagToRegExArray('--test-name-pattern'); + let testSkipPatterns = mapPatternFlagToRegExArray('--test-skip-pattern'); + let timeout; + + if (isChildProcessV8) { + kBuiltinReporters.set('v8-serializer', '#internal/test_runner/reporter/v8-serializer'); + reporters = ['v8-serializer']; + destinations = [kDefaultDestination]; + } else if (isChildProcess) { + reporters = ['tap']; + destinations = [kDefaultDestination]; + } else { + destinations = getOptionValue('--test-reporter-destination'); + reporters = getOptionValue('--test-reporter'); + if (reporters.length === 0 && destinations.length === 0) { + ArrayPrototypePush(reporters, kDefaultReporter); + } + + if (reporters.length === 1 && destinations.length === 0) { + ArrayPrototypePush(destinations, kDefaultDestination); + } + + if (destinations.length !== reporters.length) { + throw new ERR_INVALID_ARG_VALUE( + '--test-reporter', + reporters, + 'must match the number of specified \'--test-reporter-destination\'', + ); + } + } + + if (isTestRunner) { + isolation = getOptionValue('--experimental-test-isolation'); + timeout = getOptionValue('--test-timeout') || Infinity; + + if (isolation === 'none') { + concurrency = 1; + } else { + concurrency = getOptionValue('--test-concurrency') || true; + only = false; + testNamePatterns = null; + testSkipPatterns = null; + } + + const shardOption = getOptionValue('--test-shard'); + if (shardOption) { + if (!RegExpPrototypeExec(/^\d+\/\d+$/, shardOption)) { + throw new ERR_INVALID_ARG_VALUE( + '--test-shard', + shardOption, + 'must be in the form of /', + ); + } + + const indexAndTotal = StringPrototypeSplit(shardOption, '/'); + shard = { + __proto__: null, + index: NumberParseInt(indexAndTotal[0], 10), + total: NumberParseInt(indexAndTotal[1], 10), + }; + } + } else { + timeout = Infinity; + concurrency = 1; + const testNamePatternFlag = getOptionValue('--test-name-pattern'); + only = getOptionValue('--test-only'); + testNamePatterns = testNamePatternFlag?.length > 0 ? + ArrayPrototypeMap( + testNamePatternFlag, + (re) => convertStringToRegExp(re, '--test-name-pattern'), + ) : null; + const testSkipPatternFlag = getOptionValue('--test-skip-pattern'); + testSkipPatterns = testSkipPatternFlag?.length > 0 ? + ArrayPrototypeMap(testSkipPatternFlag, (re) => convertStringToRegExp(re, '--test-skip-pattern')) : null; + } + + if (coverage) { + coverageExcludeGlobs = getOptionValue('--test-coverage-exclude'); + coverageIncludeGlobs = getOptionValue('--test-coverage-include'); + + branchCoverage = getOptionValue('--test-coverage-branches'); + lineCoverage = getOptionValue('--test-coverage-lines'); + functionCoverage = getOptionValue('--test-coverage-functions'); + + validateInteger(branchCoverage, '--test-coverage-branches', 0, 100); + validateInteger(lineCoverage, '--test-coverage-lines', 0, 100); + validateInteger(functionCoverage, '--test-coverage-functions', 0, 100); + } + + const setup = reporterScope.bind(async (rootReporter) => { + const reportersMap = await getReportersMap(reporters, destinations); + + for (let i = 0; i < reportersMap.length; i++) { + const { reporter, destination } = reportersMap[i]; + compose(rootReporter, reporter).pipe(destination); + } + + reporterScope.reporters = reportersMap; + }); + + globalTestOptions = { + __proto__: null, + isTestRunner, + concurrency, + coverage, + coverageExcludeGlobs, + coverageIncludeGlobs, + destinations, + forceExit, + isolation, + branchCoverage, + functionCoverage, + lineCoverage, + only, + reporters, + setup, + shard, + sourceMaps, + testNamePatterns, + testSkipPatterns, + timeout, + updateSnapshots, + watch, + }; + + return globalTestOptions; +} + +function mapPatternFlagToRegExArray(flagName) { + const patterns = getOptionValue(flagName); + + if (patterns?.length > 0) { + return ArrayPrototypeMap(patterns, (re) => convertStringToRegExp(re, flagName)); + } + + return null; +} + +function countCompletedTest(test, harness = test.root.harness) { + if (test.nesting === 0) { + harness.counters.topLevel++; + } + if (test.reportedType === 'suite') { + harness.counters.suites++; + return; + } + // Check SKIP and TODO tests first, as those should not be counted as + // failures. + if (test.skipped) { + harness.counters.skipped++; + } else if (test.isTodo) { + harness.counters.todo++; + } else if (test.cancelled) { + harness.counters.cancelled++; + harness.success = false; + } else if (!test.passed) { + harness.counters.failed++; + harness.success = false; + } else { + harness.counters.passed++; + } + harness.counters.tests++; +} + + +const memo = new SafeMap(); +function addTableLine(prefix, width) { + const key = `${prefix}-${width}`; + let value = memo.get(key); + if (value === undefined) { + value = `${prefix}${StringPrototypeRepeat('-', width)}\n`; + memo.set(key, value); + } + + return value; +} + +const kHorizontalEllipsis = '\u2026'; +function truncateStart(string, width) { + return string.length > width ? `${kHorizontalEllipsis}${StringPrototypeSlice(string, string.length - width + 1)}` : string; +} + +function truncateEnd(string, width) { + return string.length > width ? `${StringPrototypeSlice(string, 0, width - 1)}${kHorizontalEllipsis}` : string; +} + +function formatLinesToRanges(values) { + return ArrayPrototypeMap(ArrayPrototypeReduce(values, (prev, current, index, array) => { + if ((index > 0) && ((current - array[index - 1]) === 1)) { + prev[prev.length - 1][1] = current; + } else { + prev.push([current]); } + return prev; + }, []), (range) => ArrayPrototypeJoin(range, '-')); +} + +function getUncoveredLines(lines) { + return ArrayPrototypeFlatMap(lines, (line) => (line.count === 0 ? line.line : [])); +} + +function formatUncoveredLines(lines, table) { + if (table) return ArrayPrototypeJoin(formatLinesToRanges(lines), ' '); + return ArrayPrototypeJoin(lines, ', '); +} + +const kColumns = ['line %', 'branch %', 'funcs %']; +const kColumnsKeys = ['coveredLinePercent', 'coveredBranchPercent', 'coveredFunctionPercent']; +const kSeparator = ' | '; + +function buildFileTree(summary) { + const tree = { __proto__: null }; + let treeDepth = 1; + let longestFile = 0; + + ArrayPrototypeForEach(summary.files, (file) => { + let longestPart = 0; + const parts = StringPrototypeSplit(relative(summary.workingDirectory, file.path), sep); + let current = tree; + + ArrayPrototypeForEach(parts, (part, index) => { + current[part] ||= { __proto__: null }; + current = current[part]; + // If this is the last part, add the file to the tree + if (index === parts.length - 1) { + current.file = file; + } + // Keep track of the longest part for padding + longestPart = MathMax(longestPart, part.length); + }); - return { __proto__: null, reporter, destination } - }) + treeDepth = MathMax(treeDepth, parts.length); + longestFile = MathMax(longestPart, longestFile); + }); + + return { __proto__: null, tree, treeDepth, longestFile }; } -async function setupTestReporters (testsStream) { - const destinations = getOptionValue('--test-reporter-destination') ?? [] - const reporters = getOptionValue('--test-reporter') ?? [] +function getCoverageReport(pad, summary, symbol, color, table) { + const prefix = `${pad}${symbol}`; + let report = `${color}${prefix}start of coverage report\n`; + + let filePadLength; + let columnPadLengths = []; + let uncoveredLinesPadLength; + let tableWidth; + + // Create a tree of file paths + const { tree, treeDepth, longestFile } = buildFileTree(summary); + if (table) { + // Calculate expected column sizes based on the tree + filePadLength = table && longestFile; + filePadLength += (treeDepth - 1); + if (color) { + filePadLength += 2; + } + filePadLength = MathMax(filePadLength, 'file'.length); + if (filePadLength > (process.stdout.columns / 2)) { + filePadLength = MathFloor(process.stdout.columns / 2); + } + const fileWidth = filePadLength + 2; + + columnPadLengths = ArrayPrototypeMap(kColumns, (column) => (table ? MathMax(column.length, 6) : 0)); + const columnsWidth = ArrayPrototypeReduce(columnPadLengths, (acc, columnPadLength) => acc + columnPadLength + 3, 0); + + uncoveredLinesPadLength = table && ArrayPrototypeReduce(summary.files, (acc, file) => + MathMax(acc, formatUncoveredLines(getUncoveredLines(file.lines), table).length), 0); + uncoveredLinesPadLength = MathMax(uncoveredLinesPadLength, 'uncovered lines'.length); + const uncoveredLinesWidth = uncoveredLinesPadLength + 2; + + tableWidth = fileWidth + columnsWidth + uncoveredLinesWidth; + + const availableWidth = (process.stdout.columns || Infinity) - prefix.length; + const columnsExtras = tableWidth - availableWidth; + if (table && columnsExtras > 0) { + filePadLength = MathMin(availableWidth * 0.5, filePadLength); + uncoveredLinesPadLength = MathMax(availableWidth - columnsWidth - (filePadLength + 2) - 2, 1); + tableWidth = availableWidth; + } else { + uncoveredLinesPadLength = Infinity; + } + } + + function getCell(string, width, pad, truncate, coverage) { + if (!table) return string; - if (reporters.length === 0 && destinations.length === 0) { - ArrayPrototypePush(reporters, kDefaultReporter) + let result = string; + if (pad) result = pad(result, width); + if (truncate) result = truncate(result, width); + if (color && coverage !== undefined) { + if (coverage > 90) return `${coverageColors.high}${result}${color}`; + if (coverage > 50) return `${coverageColors.medium}${result}${color}`; + return `${coverageColors.low}${result}${color}`; + } + return result; } - if (reporters.length === 1 && destinations.length === 0) { - ArrayPrototypePush(destinations, kDefaultDestination) + function writeReportLine({ file, depth = 0, coveragesColumns, fileCoverage, uncoveredLines }) { + const fileColumn = `${prefix}${StringPrototypeRepeat(' ', depth)}${getCell(file, filePadLength - depth, StringPrototypePadEnd, truncateStart, fileCoverage)}`; + const coverageColumns = ArrayPrototypeJoin(ArrayPrototypeMap(coveragesColumns, (coverage, j) => { + const coverageText = typeof coverage === 'number' ? NumberPrototypeToFixed(coverage, 2) : coverage; + return getCell(coverageText, columnPadLengths[j], StringPrototypePadStart, false, coverage); + }), kSeparator); + + const uncoveredLinesColumn = getCell(uncoveredLines, uncoveredLinesPadLength, false, truncateEnd); + + return `${fileColumn}${kSeparator}${coverageColumns}${kSeparator}${uncoveredLinesColumn}\n`; } - if (destinations.length !== reporters.length) { - throw new ERR_INVALID_ARG_VALUE('--test-reporter', reporters, - 'must match the number of specified \'--test-reporter-destination\'') + function printCoverageBodyTree(tree, depth = 0) { + for (const key in tree) { + if (tree[key].file) { + const file = tree[key].file; + const fileName = ArrayPrototypePop(StringPrototypeSplit(file.path, sep)); + + let fileCoverage = 0; + const coverages = ArrayPrototypeMap(kColumnsKeys, (columnKey) => { + const percent = file[columnKey]; + fileCoverage += percent; + return percent; + }); + fileCoverage /= kColumnsKeys.length; + + const uncoveredLines = formatUncoveredLines(getUncoveredLines(file.lines), table); + + report += writeReportLine({ + __proto__: null, + file: fileName, + depth: depth, + coveragesColumns: coverages, + fileCoverage: fileCoverage, + uncoveredLines: uncoveredLines, + }); + } else { + report += writeReportLine({ + __proto__: null, + file: key, + depth: depth, + coveragesColumns: ArrayPrototypeMap(columnPadLengths, () => ''), + fileCoverage: undefined, + uncoveredLines: '', + }); + printCoverageBodyTree(tree[key], depth + 1); + } + } } - const reportersMap = await getReportersMap(reporters, destinations) - for (let i = 0; i < reportersMap.length; i++) { - const { reporter, destination } = reportersMap[i] - compose(testsStream, reporter).pipe(destination) + // -------------------------- Coverage Report -------------------------- + if (table) report += addTableLine(prefix, tableWidth); + + // Print the header + report += writeReportLine({ + __proto__: null, + file: 'file', + coveragesColumns: kColumns, + fileCoverage: undefined, + uncoveredLines: 'uncovered lines', + }); + + if (table) report += addTableLine(prefix, tableWidth); + + // Print the body + printCoverageBodyTree(tree); + + if (table) report += addTableLine(prefix, tableWidth); + + // Print the footer + const allFilesCoverages = ArrayPrototypeMap(kColumnsKeys, (columnKey) => summary.totals[columnKey]); + report += writeReportLine({ + __proto__: null, + file: 'all files', + coveragesColumns: allFilesCoverages, + fileCoverage: undefined, + uncoveredLines: '', + }); + + if (table) report += addTableLine(prefix, tableWidth); + + report += `${prefix}end of coverage report\n`; + if (color) { + report += white; } + return report; } module.exports = { convertStringToRegExp, + countCompletedTest, createDeferredCallback, - doesPathMatchFilter, - isSupportedFileType, isTestFailureError, - setupTestReporters -} + kDefaultPattern, + parseCommandLine, + reporterScope, + shouldColorizeTestFiles, + getCoverageReport, +}; diff --git a/lib/internal/test_runner/yaml_to_js.js b/lib/internal/test_runner/yaml_to_js.js deleted file mode 100644 index 399b915..0000000 --- a/lib/internal/test_runner/yaml_to_js.js +++ /dev/null @@ -1,121 +0,0 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/lib/internal/test_runner/yaml_parser.js -'use strict' -const { - codes: { - ERR_TEST_FAILURE - } -} = require('#internal/errors') -const AssertionError = require('assert').AssertionError -const { - ArrayPrototypeJoin, - ArrayPrototypePush, - Error, - Number, - NumberIsNaN, - RegExpPrototypeExec, - StringPrototypeEndsWith, - StringPrototypeRepeat, - StringPrototypeSlice, - StringPrototypeStartsWith, - StringPrototypeSubstring -} = require('#internal/per_context/primordials') - -const kYamlKeyRegex = /^(\s+)?(\w+):(\s)+([>|][-+])?(.*)$/ -const kStackDelimiter = ' at ' - -function reConstructError (parsedYaml) { - if (!('error' in parsedYaml)) { - return parsedYaml - } - const isAssertionError = parsedYaml.code === 'ERR_ASSERTION' || - 'actual' in parsedYaml || 'expected' in parsedYaml || 'operator' in parsedYaml - const isTestFailure = parsedYaml.code === 'ERR_TEST_FAILURE' || 'failureType' in parsedYaml - const stack = parsedYaml.stack ? kStackDelimiter + ArrayPrototypeJoin(parsedYaml.stack, `\n${kStackDelimiter}`) : '' - let error, cause - - if (isAssertionError) { - cause = new AssertionError({ - message: parsedYaml.error, - actual: parsedYaml.actual, - expected: parsedYaml.expected, - operator: parsedYaml.operator - }) - } else { - // eslint-disable-next-line no-restricted-syntax - cause = new Error(parsedYaml.error) - cause.code = parsedYaml.code - } - cause.stack = stack - - if (isTestFailure) { - error = new ERR_TEST_FAILURE(cause, parsedYaml.failureType) - error.stack = stack - } - - parsedYaml.error = error ?? cause - delete parsedYaml.stack - delete parsedYaml.code - delete parsedYaml.failureType - delete parsedYaml.actual - delete parsedYaml.expected - delete parsedYaml.operator - - return parsedYaml -} - -function getYamlValue (value) { - if (StringPrototypeStartsWith(value, "'") && StringPrototypeEndsWith(value, "'")) { - return StringPrototypeSlice(value, 1, -1) - } - if (value === 'true') { - return true - } - if (value === 'false') { - return false - } - if (value !== '') { - const valueAsNumber = Number(value) - return NumberIsNaN(valueAsNumber) ? value : valueAsNumber - } - return value -} - -// This parses the YAML generated by the built-in TAP reporter, -// which is a subset of the full YAML spec. There are some -// YAML features that won't be parsed here. This function should not be exposed publicly. -function YAMLToJs (lines) { - if (lines == null) { - return undefined - } - const result = { __proto__: null } - let isInYamlBlock = false - for (let i = 0; i < lines.length; i++) { - const line = lines[i] - if (isInYamlBlock && !StringPrototypeStartsWith(line, StringPrototypeRepeat(' ', isInYamlBlock.indent))) { - result[isInYamlBlock.key] = isInYamlBlock.key === 'stack' - ? result[isInYamlBlock.key] - : ArrayPrototypeJoin(result[isInYamlBlock.key], '\n') - isInYamlBlock = false - } - if (isInYamlBlock) { - const blockLine = StringPrototypeSubstring(line, isInYamlBlock.indent) - ArrayPrototypePush(result[isInYamlBlock.key], blockLine) - continue - } - const match = RegExpPrototypeExec(kYamlKeyRegex, line) - if (match !== null) { - const { 1: leadingSpaces, 2: key, 4: block, 5: value } = match - if (block) { - isInYamlBlock = { key, indent: (leadingSpaces?.length ?? 0) + 2 } - result[key] = [] - } else { - result[key] = getYamlValue(value) - } - } - } - return reConstructError(result) -} - -module.exports = { - YAMLToJs -} diff --git a/lib/internal/timers.js b/lib/internal/timers.js index 482fdb4..d269e51 100644 --- a/lib/internal/timers.js +++ b/lib/internal/timers.js @@ -1,7 +1 @@ -'use strict' - -const TIMEOUT_MAX = 2 ** 31 - 1 - -module.exports = { - TIMEOUT_MAX -} +module.exports = { TIMEOUT_MAX: 2 ** 31 - 1 } \ No newline at end of file diff --git a/lib/internal/url.js b/lib/internal/url.js new file mode 100644 index 0000000..646d858 --- /dev/null +++ b/lib/internal/url.js @@ -0,0 +1,16 @@ +const url = require('node:url'); + +url.URL.parse = (a, b) => { + console.log(a, b); + return new URL(a, b); +} + +module.exports = { + ...url, + URLParse: url.parse, + toPathIfFileURL: (self) => { + if (!module.exports.isURL(self)) return self; + return url.fileURLToPath(self); + }, + isURL: (self) => Boolean(self?.href && self.protocol && self.auth === undefined && self.path === undefined) +} \ No newline at end of file diff --git a/lib/internal/util.js b/lib/internal/util.js index 9cbb706..5173db3 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -1,47 +1,49 @@ -// https://github.com/nodejs/node/blob/3759935ee29d8042d917d3ceaa768521c14413ff/lib/internal/util.js -'use strict' +const util = require('node:util'); +const vm = require('node:vm'); -const { - ObjectCreate, - ObjectFreeze, - ReflectApply -} = require('#internal/per_context/primordials') -const { - types: { isNativeError } -} = require('util') +const getStructuredStack = vm.runInNewContext(`(function() { + try { Error.stackTraceLimit = Infinity; } catch {} + return function structuredStack() { + Error.prepareStackTrace = (_, stack) => stack; + const e = new Error(); + return e.stack; + }; + })()`, { overrideStackTrace: new WeakMap() }, { filename: 'structured-stack' }); -function createDeferredPromise () { - let _resolve - let _reject - const promise = new Promise((resolve, reject) => { - _resolve = resolve - _reject = reject - }) +const experimentalWarnings = new Set(); - return { promise, resolve: _resolve, reject: _reject } +function once(callback, { preserveReturnValue = false } = {}) { + let called = false; + let returnValue; + return function (...args) { + if (called) return returnValue; + called = true; + const result = Reflect.apply(callback, this, args); + returnValue = preserveReturnValue ? result : undefined; + return result; + }; } -function isError (e) { - // An error could be an instance of Error while not being a native error - // or could be from a different realm and not be instance of Error but still - // be a native error. - return isNativeError(e) || e instanceof Error -} +module.exports = { + ...util, -function once (callback) { - let called = false - return function (...args) { - if (called) return - called = true - return ReflectApply(callback, this, args) - } -} + kEmptyObject: Object.freeze({}), + isWindows: process.platform === 'win32', + isMacOS: process.platform === 'darwin', + getStructuredStack, + once, -const kEmptyObject = ObjectFreeze(ObjectCreate(null)) + emitExperimentalWarning(feature, prefix = '') { + if (experimentalWarnings.has(feature)) return; + experimentalWarnings.add(feature); + process.emitWarning( + `${prefix}${feature} is an experimental feature and might change at any time`, + 'ExperimentalWarning' + ); + }, -module.exports = { - createDeferredPromise, - isError, - kEmptyObject, - once -} + setupCoverageHooks() { + // TODO + return ''; + } +} \ No newline at end of file diff --git a/lib/internal/util/colors.js b/lib/internal/util/colors.js index d44f2c1..00e7227 100644 --- a/lib/internal/util/colors.js +++ b/lib/internal/util/colors.js @@ -1,26 +1,38 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/lib/internal/util/colors.js -'use strict' +'use strict'; + +const tty = require('node:tty'); + +const colorCodes = { + blue: '\u001b[34m', + green: '\u001b[32m', + white: '\u001b[39m', + yellow: '\u001b[33m', + red: '\u001b[31m', + gray: '\u001b[90m', + clear: '\u001bc', + reset: '\u001b[0m', +}; module.exports = { - blue: '', - green: '', - white: '', - red: '', - gray: '', - clear: '', - hasColors: false, - refresh () { - if (process.stderr.isTTY) { - const hasColors = process.stderr.hasColors() - module.exports.blue = hasColors ? '\u001b[34m' : '' - module.exports.green = hasColors ? '\u001b[32m' : '' - module.exports.white = hasColors ? '\u001b[39m' : '' - module.exports.red = hasColors ? '\u001b[31m' : '' - module.exports.gray = hasColors ? '\u001b[90m' : '' - module.exports.clear = hasColors ? '\u001bc' : '' - module.exports.hasColors = hasColors + shouldColorize(stream) { + // FORCE_COLOR env variable forces color depth check + if (process.env.FORCE_COLOR !== undefined) { + return tty.WriteStream.prototype.getColorDepth() > 2; + } + // Check if the stream is a TTY and supports color depth + return stream?.isTTY && (stream.getColorDepth?.() > 2 || true); + }, + + refresh() { + const hasColors = this.shouldColorize(process.stderr); + + // Assign color escape codes or empty strings based on color support + for (const [key, code] of Object.entries(colorCodes)) { + this[key] = hasColors ? code : ''; } - } -} -module.exports.refresh() + this.hasColors = hasColors; + }, +}; + +module.exports.refresh(); diff --git a/lib/internal/util/debuglog.js b/lib/internal/util/debuglog.js new file mode 100644 index 0000000..6b3ba95 --- /dev/null +++ b/lib/internal/util/debuglog.js @@ -0,0 +1,3 @@ +module.exports = { + debuglog: () => () => {} +}; \ No newline at end of file diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 59ed900..dfb6998 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1,5 +1,2 @@ -'use strict' - -const { format, inspect } = require('util') - -module.exports = { format, inspect } +const { inspect } = require('node:util'); +module.exports = { inspect }; \ No newline at end of file diff --git a/lib/internal/util/inspector.js b/lib/internal/util/inspector.js index b80129f..838f6e1 100644 --- a/lib/internal/util/inspector.js +++ b/lib/internal/util/inspector.js @@ -1,48 +1,36 @@ -// https://github.com/nodejs/node/blob/a165193c5c8e4bcfbd12b2c3f6e55a81a251c258/lib/internal/util/inspector.js -const { - ArrayPrototypeSome, - RegExpPrototypeExec -} = require('#internal/per_context/primordials') +const { validatePort } = require("#internal/validators"); -const { validatePort } = require('#internal/validators') - -const kMinPort = 1024 -const kMaxPort = 65535 -const kInspectArgRegex = /--inspect(?:-brk|-port)?|--debug-port/ -const kInspectMsgRegex = /Debugger listening on ws:\/\/\[?(.+?)\]?:(\d+)\/|Debugger attached|Waiting for the debugger to disconnect\.\.\./ - -let _isUsingInspector -function isUsingInspector () { - // Node.js 14.x does not support Logical_nullish_assignment operator - _isUsingInspector = _isUsingInspector ?? - (ArrayPrototypeSome(process.execArgv, (arg) => RegExpPrototypeExec(kInspectArgRegex, arg) !== null) || - RegExpPrototypeExec(kInspectArgRegex, process.env.NODE_OPTIONS) !== null) - return _isUsingInspector -} - -let debugPortOffset = 1 -function getInspectPort (inspectPort) { - if (!isUsingInspector()) { - return null - } +let debugPortOffset = 1; +function getInspectPort(inspectPort) { if (typeof inspectPort === 'function') { - inspectPort = inspectPort() + inspectPort = inspectPort(); } else if (inspectPort == null) { - inspectPort = process.debugPort + debugPortOffset - if (inspectPort > kMaxPort) { inspectPort = inspectPort - kMaxPort + kMinPort - 1 } - debugPortOffset++ + inspectPort = process.debugPort + debugPortOffset; + if (inspectPort > kMaxPort) + inspectPort = inspectPort - kMaxPort + kMinPort - 1; + debugPortOffset++; } - validatePort(inspectPort) + validatePort(inspectPort); - return inspectPort + return inspectPort; } -function isInspectorMessage (string) { - return isUsingInspector() && RegExpPrototypeExec(kInspectMsgRegex, string) !== null +const _isUsingInspector = new Map(); +const kInspectArgRegex = /--inspect(?:-brk|-port)?|--debug-port/; +function isUsingInspector(execArgv = process.execArgv) { + if (!_isUsingInspector.has(execArgv)) { + _isUsingInspector.set(execArgv, execArgv.some((arg) => kInspectArgRegex.exec(arg) !== null) || kInspectArgRegex.exec(process.env.NODE_OPTIONS) !== null); + } + return _isUsingInspector.get(execArgv); } -module.exports = { - isUsingInspector, - getInspectPort, - isInspectorMessage +const kInspectMsgRegex = /Debugger listening on ws:\/\/\[?(.+?)\]?:(\d+)\/|For help, see: https:\/\/nodejs\.org\/en\/docs\/inspector|Debugger attached|Waiting for the debugger to disconnect\.\.\./; +function isInspectorMessage(string) { + return isUsingInspector() && kInspectMsgRegex.exec(string) !== null; } + +module.exports = { + getInspectPort, + isUsingInspector, + isInspectorMessage, +} \ No newline at end of file diff --git a/lib/internal/util/types.js b/lib/internal/util/types.js index d7f791d..6bd19f4 100644 --- a/lib/internal/util/types.js +++ b/lib/internal/util/types.js @@ -1,3 +1 @@ -const { types } = require('util') - -module.exports = types +module.exports = require('node:util/types'); \ No newline at end of file diff --git a/lib/internal/validators.js b/lib/internal/validators.js index 8149840..1c2e63d 100644 --- a/lib/internal/validators.js +++ b/lib/internal/validators.js @@ -1,127 +1,85 @@ -// https://github.com/nodejs/node/blob/60da0a1b364efdd84870269d23b39faa12fb46d8/lib/internal/validators.js const { - ArrayIsArray, - ArrayPrototypeIncludes, - ArrayPrototypeJoin, - ArrayPrototypeMap, - NumberIsInteger, - NumberMAX_SAFE_INTEGER, // eslint-disable-line camelcase - NumberMIN_SAFE_INTEGER, // eslint-disable-line camelcase - ObjectPrototypeHasOwnProperty -} = require('#internal/per_context/primordials') -const { - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, - ERR_OUT_OF_RANGE -} = require('#internal/errors').codes + codes: { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_OUT_OF_RANGE, ERR_SOCKET_BAD_PORT } +} = require('#internal/errors'); + +const validateType = (value, name, type) => { + if (typeof value !== type) throw new ERR_INVALID_ARG_TYPE(name, type, value); +}; + +const validateArray = (value, name) => { + if (!Array.isArray(value)) throw new ERR_INVALID_ARG_TYPE(name, 'Array', value); +}; + +const validateStringArray = (value, name) => { + validateArray(value, name); + value.forEach((v, i) => validateType(v, `${name}[${i}]`, 'string')); +}; -function isUint32 (value) { - return value === (value >>> 0) -} +const validateObject = (value, name) => { + if (value === null || Array.isArray(value) || typeof value !== 'object') { + throw new ERR_INVALID_ARG_TYPE(name, 'Object', value); + } +}; -function validateNumber (value, name, min = undefined, max) { - if (typeof value !== 'number') { - throw new ERR_INVALID_ARG_TYPE(name, 'a number', value) +const validateOneOf = (value, name, validValues) => { + if (!validValues.includes(value)) { + const allowed = validValues.map(v => (typeof v === 'string' ? `'${v}'` : String(v))).join(', '); + throw new ERR_INVALID_ARG_VALUE(name, value, `must be one of: ${allowed}`); } +}; + +const validateInteger = (value, name, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) => { + validateType(value, name, 'number'); + if (!Number.isInteger(value)) throw new ERR_OUT_OF_RANGE(name, 'an integer', value); + if (value < min || value > max) throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value); +}; - if ((min != null && value < min) || (max != null && value > max) || - ((min != null || max != null) && Number.isNaN(value))) { +const validateUint32 = (value, name, positive = false) => { + validateInteger(value, name, positive ? 1 : 0, 2 ** 32 - 1); +}; + +const validateNumber = (value, name, min, max) => { + validateType(value, name, 'number'); + if ((min !== undefined && value < min) || (max !== undefined && value > max) || Number.isNaN(value)) { throw new ERR_OUT_OF_RANGE( name, - `${min != null ? `>= ${min}` : ''}${min != null && max != null ? ' && ' : ''}${max != null ? `<= ${max}` : ''}`, + `${min !== undefined ? `>= ${min}` : ''}${min !== undefined && max !== undefined ? ' && ' : ''}${max !== undefined ? `<= ${max}` : ''}`, value - ) + ); + } +}; + +const validatePort = (port, name = 'Port', allowZero = true) => { + if ((typeof port !== 'number' && typeof port !== 'string') || + (typeof port === 'string' && port.trim().length === 0) || + +port !== (+port >>> 0) || + port > 0xFFFF || + (port === 0 && !allowZero)) { + throw new ERR_SOCKET_BAD_PORT(name, port, allowZero); } -} + return port | 0; +}; const validateAbortSignal = (signal, name) => { if (signal !== undefined && (signal === null || typeof signal !== 'object' || !('aborted' in signal))) { - throw new ERR_INVALID_ARG_TYPE(name, 'an AbortSignal', signal) - } -} - -const validateUint32 = (value, name, positive) => { - if (typeof value !== 'number') { - throw new ERR_INVALID_ARG_TYPE(name, 'a number', value) - } - if (!Number.isInteger(value)) { - throw new ERR_OUT_OF_RANGE(name, 'an integer', value) - } - const min = positive ? 1 : 0 - // 2 ** 32 === 4294967296 - const max = 4_294_967_295 - if (value < min || value > max) { - throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value) - } -} - -const validateOneOf = (value, name, oneOf) => { - if (!ArrayPrototypeIncludes(oneOf, value)) { - const allowed = ArrayPrototypeJoin( - ArrayPrototypeMap(oneOf, (v) => - (typeof v === 'string' ? `'${v}'` : String(v))), - ', ') - const reason = 'must be one of: ' + allowed - throw new ERR_INVALID_ARG_VALUE(name, value, reason) - } -} - -const validateArray = (value, name, minLength = 0) => { - if (!ArrayIsArray(value)) { - throw new ERR_INVALID_ARG_TYPE(name, 'Array', value) - } - if (value.length < minLength) { - const reason = `must be longer than ${minLength}` - throw new ERR_INVALID_ARG_VALUE(name, value, reason) - } -} - -function getOwnPropertyValueOrDefault (options, key, defaultValue) { - return options == null || !ObjectPrototypeHasOwnProperty(options, key) - ? defaultValue - : options[key] -} - -const validateObject = (value, name, options = null) => { - const allowArray = getOwnPropertyValueOrDefault(options, 'allowArray', false) - const allowFunction = getOwnPropertyValueOrDefault(options, 'allowFunction', false) - const nullable = getOwnPropertyValueOrDefault(options, 'nullable', false) - if ((!nullable && value === null) || - (!allowArray && ArrayIsArray(value)) || - (typeof value !== 'object' && ( - !allowFunction || typeof value !== 'function' - ))) { - throw new ERR_INVALID_ARG_TYPE(name, 'Object', value) - } -} - -const validateFunction = (value, name) => { - if (typeof value !== 'function') { throw new ERR_INVALID_ARG_TYPE(name, 'Function', value) } -} - -function validateBoolean (value, name) { - if (typeof value !== 'boolean') { throw new ERR_INVALID_ARG_TYPE(name, 'boolean', value) } -} - -const validateInteger = - (value, name, min = NumberMIN_SAFE_INTEGER, max = NumberMAX_SAFE_INTEGER) => { - if (typeof value !== 'number') { throw new ERR_INVALID_ARG_TYPE(name, 'number', value) } - if (!NumberIsInteger(value)) { throw new ERR_OUT_OF_RANGE(name, 'an integer', value) } - if (value < min || value > max) { throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value) } + throw new ERR_INVALID_ARG_TYPE(name, 'AbortSignal', signal); } +}; module.exports = { - isUint32, - validateAbortSignal, validateArray, - validateBoolean, - validateFunction, - validateInteger, - validateNumber, + validateBoolean: (value, name) => validateType(value, name, 'boolean'), + validateFunction: (value, name) => validateType(value, name, 'function'), + validateString: (value, name) => validateType(value, name, 'string'), validateObject, validateOneOf, - validateUint32 -} + validateInteger, + validateUint32, + validateNumber, + validateStringArray, + validateAbortSignal, + validatePort, +}; diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js new file mode 100644 index 0000000..bfac455 --- /dev/null +++ b/lib/internal/watch_mode/files_watcher.js @@ -0,0 +1,3 @@ +module.exports = { + FilesWatcher: null // --watch is not supported +} \ No newline at end of file diff --git a/lib/reporters.js b/lib/reporters.js new file mode 100644 index 0000000..3d43479 --- /dev/null +++ b/lib/reporters.js @@ -0,0 +1,47 @@ +'use strict'; + +let dot; +let junit; +let spec; +let tap; +let lcov; + +Object.defineProperties(module.exports, { + dot: { + configurable: true, + enumerable: true, + get() { + return dot ??= require('#internal/test_runner/reporter/dot'); + }, + }, + junit: { + configurable: true, + enumerable: true, + get() { + return junit ??= require('#internal/test_runner/reporter/junit'); + }, + }, + spec: { + configurable: true, + enumerable: true, + value: function (...args) { + spec ??= require('#internal/test_runner/reporter/spec'); + return new spec(...args); + }, + }, + tap: { + configurable: true, + enumerable: true, + get() { + return tap ??= require('#internal/test_runner/reporter/tap'); + }, + }, + lcov: { + configurable: true, + enumerable: true, + value: function (...args) { + lcov ??= require('#internal/test_runner/reporter/lcov'); + return new lcov(...args); + }, + }, +}); diff --git a/lib/test.d.ts b/lib/test.d.ts deleted file mode 100644 index 29e794b..0000000 --- a/lib/test.d.ts +++ /dev/null @@ -1,119 +0,0 @@ -interface TestOptions { - /** - * The number of tests that can be run at the same time. If unspecified, subtests inherit this value from their parent. - * Default: 1. - */ - concurrency?: boolean | number; - - /** - * If truthy, the test is skipped. If a string is provided, that string is displayed in the test results as the reason for skipping the test. - * Default: false. - */ - skip?: boolean | string; - - /** - * If truthy, the test marked as TODO. If a string is provided, that string is displayed in the test results as the reason why the test is TODO. - * Default: false. - */ - todo?: boolean | string; - - /** - * A number of milliseconds the test will fail after. If unspecified, subtests inherit this value from their parent. - * Default: Infinity - */ - timeout?: number; - - /** - * Allows aborting an in-progress test - */ - signal?: AbortSignal; -} - -type TestFn = (t: TestContext) => any | Promise; - -export default test; - -export function test(name: string, options: TestOptions, fn: TestFn): void; -export function test(name: string, fn: TestFn): void; -export function test(options: TestOptions, fn: TestFn): void; -export function test(fn: TestFn): void; - -type SuiteFn = (t: SuiteContext) => void; - -export function describe(name: string, options: TestOptions, fn: SuiteFn): void; -export function describe(name: string, fn: SuiteFn): void; -export function describe(options: TestOptions, fn: SuiteFn): void; -export function describe(fn: SuiteFn): void; - -type ItFn = (t: ItContext) => any | Promise; - -export function it(name: string, options: TestOptions, fn: ItFn): void; -export function it(name: string, fn: ItFn): void; -export function it(options: TestOptions, fn: ItFn): void; -export function it(fn: ItFn): void; - -/** - * An instance of TestContext is passed to each test function in order to interact with the test runner. - * However, the TestContext constructor is not exposed as part of the API. - */ -declare class TestContext { - /** - * This function is used to create subtests under the current test. This function behaves in the same fashion as the top level test() function. - */ - public test(name: string, options: TestOptions, fn: TestFn): Promise; - public test(name: string, fn: TestFn): Promise; - public test(fn: TestFn): Promise; - - /** - * This function is used to write TAP diagnostics to the output. - * Any diagnostic information is included at the end of the test's results. This function does not return a value. - * - * @param message Message to be displayed as a TAP diagnostic. - */ - public diagnostic(message: string): void; - - /** - * This function causes the test's output to indicate the test as skipped. - * If message is provided, it is included in the TAP output. - * Calling skip() does not terminate execution of the test function. This function does not return a value. - * - * @param message Optional skip message to be displayed in TAP output. - */ - public skip(message?: string): void; - - /** - * This function adds a TODO directive to the test's output. - * If message is provided, it is included in the TAP output. - * Calling todo() does not terminate execution of the test function. This function does not return a value. - * - * @param message Optional TODO message to be displayed in TAP output. - */ - public todo(message?: string): void; - - /** - * Can be used to abort test subtasks when the test has been aborted. - */ - public signal: AbortSignal; -} - -/** - * An instance of SuiteContext is passed to each suite function in order to interact with the test runner. - * However, the SuiteContext constructor is not exposed as part of the API. - */ -declare class SuiteContext { - /** - * Can be used to abort test subtasks when the test has been aborted. - */ - public signal: AbortSignal; -} - -/** - * An instance of ItContext is passed to each suite function in order to interact with the test runner. - * However, the ItContext constructor is not exposed as part of the API. - */ -declare class ItContext { - /** - * Can be used to abort test subtasks when the test has been aborted. - */ - public signal: AbortSignal; -} diff --git a/lib/test.js b/lib/test.js index 7e92edb..b372c2e 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,34 +1,68 @@ -// https://github.com/nodejs/node/blob/7c6682957b3c5f86d0616cebc0ad09cc2a1fd50d/lib/test.js -'use strict' -const { ObjectAssign, ObjectDefineProperty } = require('#internal/per_context/primordials') -const { test, describe, it, before, after, beforeEach, afterEach } = require('#internal/test_runner/harness') -const { run } = require('#internal/test_runner/runner') +const { primordials, internalBinding } = require('#lib/bootstrap'); -module.exports = test +'use strict'; + +const { + ObjectAssign, + ObjectDefineProperty, +} = primordials; + +const { test, suite, before, after, beforeEach, afterEach } = require('#internal/test_runner/harness'); +const { run } = require('#internal/test_runner/runner'); +const { getOptionValue } = require('#internal/options'); + +module.exports = test; ObjectAssign(module.exports, { after, afterEach, before, beforeEach, - describe, - it, + describe: suite, + it: test, run, - test -}) + suite, + test, +}); -let lazyMock +let lazyMock; ObjectDefineProperty(module.exports, 'mock', { __proto__: null, configurable: true, enumerable: true, - get () { + get() { if (lazyMock === undefined) { - const { MockTracker } = require('#internal/test_runner/mock') + const { MockTracker } = require('#internal/test_runner/mock/mock'); - lazyMock = new MockTracker() + lazyMock = new MockTracker(); } - return lazyMock - } -}) + return lazyMock; + }, +}); + +if (getOptionValue('--experimental-test-snapshots')) { + let lazySnapshot; + + ObjectDefineProperty(module.exports, 'snapshot', { + __proto__: null, + configurable: true, + enumerable: true, + get() { + if (lazySnapshot === undefined) { + const { + setDefaultSnapshotSerializers, + setResolveSnapshotPath, + } = require('#internal/test_runner/snapshot'); + + lazySnapshot = { + __proto__: null, + setDefaultSnapshotSerializers, + setResolveSnapshotPath, + }; + } + + return lazySnapshot; + }, + }); +} \ No newline at end of file diff --git a/lib/timers/promises.js b/lib/timers/promises.js deleted file mode 100644 index 16a5396..0000000 --- a/lib/timers/promises.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict' -try { - module.exports = require('node:timers/promises') -} catch { - const { promisify } = require('node:util') - module.exports = { - setImmediate: promisify(setImmediate), - setInterval: promisify(setInterval), - setTimeout: promisify(setTimeout) - } -} diff --git a/package-lock.json b/package-lock.json index 5e40efc..8a27ec8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,4117 +1,52 @@ { "name": "test", - "version": "3.3.0", - "lockfileVersion": 2, + "version": "4.0.0", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "test", - "version": "3.3.0", + "version": "4.0.0", "license": "MIT", "dependencies": { - "minimist": "^1.2.6", - "readable-stream": "^4.3.0", - "string.prototype.replaceall": "^1.0.6" - }, - "bin": { - "node--test": "bin/node--test.js", - "node--test-name-pattern": "bin/node--test-name-pattern.js", - "node--test-only": "bin/node--test-only.js", - "test": "bin/node-core-test.js" - }, - "devDependencies": { - "standard": "^17.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", - "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.3.1", - "globals": "^13.9.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/array-includes": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", - "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", - "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", - "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", - "es-shim-unscopables": "^1.0.0" + "frozen-fruit": "^0.0.2", + "minimatch": "^9.0.5" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/builtins": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-4.1.0.tgz", - "integrity": "sha512-1bPRZQtmKaO6h7qV1YHXNtr6nCK28k0Zo95KM4dXfILcZZwoHJBN1m3lfLv9LPkcOZlrSr+J1bzMaZFO98Yq0w==", - "dev": true, - "dependencies": { - "semver": "^7.0.0" - } - }, - "node_modules/builtins/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.5.tgz", - "integrity": "sha512-Aa2G2+Rd3b6kxEUKTF4TaW67czBLyAv3z7VOhYRU50YBx+bbsYZ9xQP4lMNazePuFlybXI0V4MruPos7qUo5fA==", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.13.0.tgz", - "integrity": "sha512-D+Xei61eInqauAyTJ6C0q6x9mx7kTUC1KZ0m0LSEexR0V+e94K12LmWX076ZIsldwfQ2RONdaJe0re0TRGQbRQ==", - "dev": true, - "dependencies": { - "@eslint/eslintrc": "^1.2.1", - "@humanwhocodes/config-array": "^0.9.2", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-standard": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz", - "integrity": "sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "peerDependencies": { - "eslint": "^8.0.1", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0", - "eslint-plugin-promise": "^6.0.0" - } - }, - "node_modules/eslint-config-standard-jsx": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-11.0.0.tgz", - "integrity": "sha512-+1EV/R0JxEK1L0NGolAr8Iktm3Rgotx3BKwgaX+eAuSX8D952LULKtjgZD3F+e6SvibONnhLwoTi9DPxN5LvvQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "peerDependencies": { - "eslint": "^8.8.0", - "eslint-plugin-react": "^7.28.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "resolve": "^1.20.0" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", - "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "find-up": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-es": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", - "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", - "dev": true, - "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "node_modules/eslint-plugin-es/node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" + "balanced-match": "^1.0.0" } }, - "node_modules/eslint-plugin-import": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", - "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", - "has": "^1.0.3", - "is-core-module": "^2.8.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "node_modules/eslint-plugin-n": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.1.0.tgz", - "integrity": "sha512-Tgx4Z58QXv2Ha7Qzp0u4wavnZNZ3AOievZMxrAxi7nvDbzD5B/JqOD80LHYcGHFZc2HD9jDmM/+KWMPov46a4A==", - "dev": true, - "dependencies": { - "builtins": "^4.0.0", - "eslint-plugin-es": "^4.1.0", - "eslint-utils": "^3.0.0", - "ignore": "^5.1.1", - "is-core-module": "^2.3.0", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-promise": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz", - "integrity": "sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.29.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz", - "integrity": "sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flatmap": "^1.2.5", - "doctrine": "^2.1.0", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.5", - "object.fromentries": "^2.0.5", - "object.hasown": "^1.1.0", - "object.values": "^1.1.5", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.3", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } + "node_modules/frozen-fruit": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/frozen-fruit/-/frozen-fruit-0.0.2.tgz", + "integrity": "sha512-eSW1xmo5dTbbOqSx9uv+QmKB3d1/m6ZPZzbavGVktE3J2X2h1Q6kqyVqpr1vKwi9aR+/zuq2d0pl6tTumxZsHQ==" }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", - "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", - "dev": true, - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dependencies": { - "eslint-visitor-keys": "^2.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", - "dev": true, - "dependencies": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "url": "https://github.com/sponsors/isaacs" } - }, - "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", - "dev": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", - "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.2.tgz", - "integrity": "sha512-HDAyJ4MNQBboGpUnHAVUNJs6X0lh058s6FuixsFGP7MgJYpD6Vasd6nzSG5iIfXu1zAYlHJ/zsOKNlrenTUBnw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.4", - "object.assign": "^4.1.2" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/load-json-file": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", - "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.15", - "parse-json": "^4.0.0", - "pify": "^4.0.1", - "strip-bom": "^3.0.0", - "type-fest": "^0.3.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/load-json-file/node_modules/type-fest": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", - "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", - "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", - "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz", - "integrity": "sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-conf": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz", - "integrity": "sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==", - "dev": true, - "dependencies": { - "find-up": "^3.0.0", - "load-json-file": "^5.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-conf/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-conf/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-conf/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-conf/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-conf/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, - "node_modules/readable-stream": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", - "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/standard": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/standard/-/standard-17.0.0.tgz", - "integrity": "sha512-GlCM9nzbLUkr+TYR5I2WQoIah4wHA2lMauqbyPLV/oI5gJxqhHzhjl9EG2N0lr/nRqI3KCbCvm/W3smxvLaChA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "eslint": "^8.13.0", - "eslint-config-standard": "17.0.0", - "eslint-config-standard-jsx": "^11.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-n": "^15.1.0", - "eslint-plugin-promise": "^6.0.0", - "eslint-plugin-react": "^7.28.0", - "standard-engine": "^15.0.0" - }, - "bin": { - "standard": "bin/cmd.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/standard-engine": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-15.0.0.tgz", - "integrity": "sha512-4xwUhJNo1g/L2cleysUqUv7/btn7GEbYJvmgKrQ2vd/8pkTmN8cpqAZg+BT8Z1hNeEH787iWUdOpL8fmApLtxA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "get-stdin": "^8.0.0", - "minimist": "^1.2.6", - "pkg-conf": "^3.1.0", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", - "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.1", - "side-channel": "^1.0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.replaceall": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.6.tgz", - "integrity": "sha512-OA8VDhE7ssNFlyoDXUHxw6V5cjgPrtosyJKqJX5i1P5tV9eUynsbhx1yz0g+Ye4fjFwAxhKLxt8GSRx2Aqc+Sw==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.2", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "node_modules/tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dependencies": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "node_modules/xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - }, - "dependencies": { - "@eslint/eslintrc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", - "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.3.1", - "globals": "^13.9.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - } - }, - "@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - } - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "array-includes": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", - "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" - } - }, - "array.prototype.flat": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", - "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", - "es-shim-unscopables": "^1.0.0" - } - }, - "array.prototype.flatmap": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", - "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", - "es-shim-unscopables": "^1.0.0" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "builtins": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-4.1.0.tgz", - "integrity": "sha512-1bPRZQtmKaO6h7qV1YHXNtr6nCK28k0Zo95KM4dXfILcZZwoHJBN1m3lfLv9LPkcOZlrSr+J1bzMaZFO98Yq0w==", - "dev": true, - "requires": { - "semver": "^7.0.0" - }, - "dependencies": { - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.5.tgz", - "integrity": "sha512-Aa2G2+Rd3b6kxEUKTF4TaW67czBLyAv3z7VOhYRU50YBx+bbsYZ9xQP4lMNazePuFlybXI0V4MruPos7qUo5fA==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - } - }, - "es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.13.0.tgz", - "integrity": "sha512-D+Xei61eInqauAyTJ6C0q6x9mx7kTUC1KZ0m0LSEexR0V+e94K12LmWX076ZIsldwfQ2RONdaJe0re0TRGQbRQ==", - "dev": true, - "requires": { - "@eslint/eslintrc": "^1.2.1", - "@humanwhocodes/config-array": "^0.9.2", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - } - }, - "eslint-config-standard": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz", - "integrity": "sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==", - "dev": true, - "requires": {} - }, - "eslint-config-standard-jsx": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-11.0.0.tgz", - "integrity": "sha512-+1EV/R0JxEK1L0NGolAr8Iktm3Rgotx3BKwgaX+eAuSX8D952LULKtjgZD3F+e6SvibONnhLwoTi9DPxN5LvvQ==", - "dev": true, - "requires": {} - }, - "eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", - "dev": true, - "requires": { - "debug": "^3.2.7", - "resolve": "^1.20.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "eslint-module-utils": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", - "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", - "dev": true, - "requires": { - "debug": "^3.2.7", - "find-up": "^2.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "eslint-plugin-es": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", - "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", - "dev": true, - "requires": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "dependencies": { - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "eslint-plugin-import": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", - "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", - "dev": true, - "requires": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", - "has": "^1.0.3", - "is-core-module": "^2.8.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-plugin-n": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.1.0.tgz", - "integrity": "sha512-Tgx4Z58QXv2Ha7Qzp0u4wavnZNZ3AOievZMxrAxi7nvDbzD5B/JqOD80LHYcGHFZc2HD9jDmM/+KWMPov46a4A==", - "dev": true, - "requires": { - "builtins": "^4.0.0", - "eslint-plugin-es": "^4.1.0", - "eslint-utils": "^3.0.0", - "ignore": "^5.1.1", - "is-core-module": "^2.3.0", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.3.0" - } - }, - "eslint-plugin-promise": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz", - "integrity": "sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw==", - "dev": true, - "requires": {} - }, - "eslint-plugin-react": { - "version": "7.29.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz", - "integrity": "sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==", - "dev": true, - "requires": { - "array-includes": "^3.1.4", - "array.prototype.flatmap": "^1.2.5", - "doctrine": "^2.1.0", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.5", - "object.fromentries": "^2.0.5", - "object.hasown": "^1.1.0", - "object.values": "^1.1.5", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.3", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.6" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "resolve": { - "version": "2.0.0-next.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", - "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - } - } - }, - "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - } - } - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true - }, - "espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", - "dev": true, - "requires": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^3.3.0" - } - }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true - }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", - "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "requires": { - "get-intrinsic": "^1.1.1" - } - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "requires": { - "has-bigints": "^1.0.1" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" - }, - "is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" - }, - "is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "jsx-ast-utils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.2.tgz", - "integrity": "sha512-HDAyJ4MNQBboGpUnHAVUNJs6X0lh058s6FuixsFGP7MgJYpD6Vasd6nzSG5iIfXu1zAYlHJ/zsOKNlrenTUBnw==", - "dev": true, - "requires": { - "array-includes": "^3.1.4", - "object.assign": "^4.1.2" - } - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "load-json-file": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", - "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "parse-json": "^4.0.0", - "pify": "^4.0.1", - "strip-bom": "^3.0.0", - "type-fest": "^0.3.0" - }, - "dependencies": { - "type-fest": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", - "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", - "dev": true - } - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - }, - "object.entries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", - "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "object.fromentries": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", - "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "object.hasown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz", - "integrity": "sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "pkg-conf": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz", - "integrity": "sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==", - "dev": true, - "requires": { - "find-up": "^3.0.0", - "load-json-file": "^5.2.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - } - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" - }, - "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, - "readable-stream": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", - "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", - "requires": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10" - } - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, - "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, - "requires": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "standard": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/standard/-/standard-17.0.0.tgz", - "integrity": "sha512-GlCM9nzbLUkr+TYR5I2WQoIah4wHA2lMauqbyPLV/oI5gJxqhHzhjl9EG2N0lr/nRqI3KCbCvm/W3smxvLaChA==", - "dev": true, - "requires": { - "eslint": "^8.13.0", - "eslint-config-standard": "17.0.0", - "eslint-config-standard-jsx": "^11.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-n": "^15.1.0", - "eslint-plugin-promise": "^6.0.0", - "eslint-plugin-react": "^7.28.0", - "standard-engine": "^15.0.0" - } - }, - "standard-engine": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-15.0.0.tgz", - "integrity": "sha512-4xwUhJNo1g/L2cleysUqUv7/btn7GEbYJvmgKrQ2vd/8pkTmN8cpqAZg+BT8Z1hNeEH787iWUdOpL8fmApLtxA==", - "dev": true, - "requires": { - "get-stdin": "^8.0.0", - "minimist": "^1.2.6", - "pkg-conf": "^3.1.0", - "xdg-basedir": "^4.0.0" - } - }, - "string.prototype.matchall": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", - "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.1", - "side-channel": "^1.0.4" - } - }, - "string.prototype.replaceall": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.6.tgz", - "integrity": "sha512-OA8VDhE7ssNFlyoDXUHxw6V5cjgPrtosyJKqJX5i1P5tV9eUynsbhx1yz0g+Ye4fjFwAxhKLxt8GSRx2Aqc+Sw==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.2", - "is-regex": "^1.1.4" - } - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", - "dev": true, - "requires": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, - "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true } } } diff --git a/package.json b/package.json index ce6f046..ee00997 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,20 @@ { "name": "test", - "version": "3.3.0", - "description": "Node.js 18's node:test, as an npm package", + "version": "4.0.0", + "description": "Node.js 23's node:test, as an npm package", "license": "MIT", "repository": "nodejs/node-core-test", - "main": "./lib/test.js", - "types": "./lib/test.d.ts", - "bin": { - "node--test": "./bin/node--test.js", - "node--test-only": "./bin/node--test-only.js", - "node--test-name-pattern": "./bin/node--test-name-pattern.js", - "test": "./bin/node-core-test.js" + "engines": { + "node": ">=18" }, "imports": { "#internal/*": "./lib/internal/*.js", - "#timers/*": "./lib/timers/*.js", - "#node:test": "./lib/test.js" - }, - "scripts": { - "test": "npm run test:lint && npm run test:unit", - "test:lint": "standard", - "test:lint:fix": "standard --fix", - "test:unit": "node test/parallel.mjs && node test/message.js && node test/node-core-test.js" - }, - "devDependencies": { - "standard": "^17.0.0" + "#lib/*": "./lib/*.js", + "#node:test": "./lib/test.js", + "#node:test/reporters": "./lib/reporters.js" }, "dependencies": { - "minimist": "^1.2.6", - "readable-stream": "^4.3.0", - "string.prototype.replaceall": "^1.0.6" + "frozen-fruit": "^0.0.2", + "minimatch": "9.0.5" } } diff --git a/test/common/assertSnapshot.js b/test/common/assertSnapshot.js new file mode 100644 index 0000000..5416f53 --- /dev/null +++ b/test/common/assertSnapshot.js @@ -0,0 +1,104 @@ +'use strict'; +const common = require('.'); +const path = require('node:path'); +const test = require('node:test'); +const fs = require('node:fs/promises'); +const assert = require('node:assert/strict'); + +const stackFramesRegexp = /(?<=\n)(\s+)((.+?)\s+\()?(?:\(?(.+?):(\d+)(?::(\d+))?)\)?(\s+\{)?(\[\d+m)?(\n|$)/g; +const windowNewlineRegexp = /\r/g; + +function replaceNodeVersion(str) { + return str.replaceAll(process.version, '*'); +} + +function replaceStackTrace(str, replacement = '$1*$7$8\n') { + return str.replace(stackFramesRegexp, replacement); +} + +function replaceWindowsLineEndings(str) { + return str.replace(windowNewlineRegexp, ''); +} + +function replaceWindowsPaths(str) { + return common.isWindows ? str.replaceAll(path.win32.sep, path.posix.sep) : str; +} + +function replaceFullPaths(str) { + return str.replaceAll('\\\'', "'").replaceAll(path.resolve(__dirname, '../..'), ''); +} + +function transform(...args) { + return (str) => args.reduce((acc, fn) => fn(acc), str); +} + +function getSnapshotPath(filename) { + const { name, dir } = path.parse(filename); + return path.resolve(dir, `${name}.snapshot`); +} + +async function assertSnapshot(actual, filename = process.argv[1]) { + const snapshot = getSnapshotPath(filename); + if (process.env.NODE_REGENERATE_SNAPSHOTS) { + await fs.writeFile(snapshot, actual); + } else { + let expected; + try { + expected = await fs.readFile(snapshot, 'utf8'); + } catch (e) { + if (e.code === 'ENOENT') { + console.log( + 'Snapshot file does not exist. You can create a new one by running the test with NODE_REGENERATE_SNAPSHOTS=1', + ); + } + throw e; + } + assert.strictEqual(actual, replaceWindowsLineEndings(expected)); + } +} + +/** + * Spawn a process and assert its output against a snapshot. + * if you want to automatically update the snapshot, run tests with NODE_REGENERATE_SNAPSHOTS=1 + * transform is a function that takes the output and returns a string that will be compared against the snapshot + * this is useful for normalizing output such as stack traces + * there are some predefined transforms in this file such as replaceStackTrace and replaceWindowsLineEndings + * both of which can be used as an example for writing your own + * compose multiple transforms by passing them as arguments to the transform function: + * assertSnapshot.transform(assertSnapshot.replaceStackTrace, assertSnapshot.replaceWindowsLineEndings) + * @param {string} filename + * @param {function(string): string} [transform] + * @param {object} [options] - control how the child process is spawned + * @param {boolean} [options.tty] - whether to spawn the process in a pseudo-tty + * @returns {Promise} + */ +async function spawnAndAssert(filename, transform = (x) => x, { tty = false, ...options } = {}) { + if (tty && common.isWindows) { + test({ skip: 'Skipping pseudo-tty tests, as pseudo terminals are not available on Windows.' }); + return; + } + let flags = common.parseTestFlags(filename); + if (options.flags) { + flags = [...options.flags, ...flags]; + } + + const executable = tty ? (process.env.PYTHON || 'python3') : process.execPath; + const args = + tty ? + [path.join(__dirname, 'pseudo-tty.py'), process.execPath, filename, ...flags] : + [filename, ...flags]; + const { stdout, stderr } = await common.spawnPromisified(executable, args, options); + await assertSnapshot(transform(`${stdout}${stderr}`), filename); +} + +module.exports = { + assertSnapshot, + getSnapshotPath, + replaceFullPaths, + replaceNodeVersion, + replaceStackTrace, + replaceWindowsLineEndings, + replaceWindowsPaths, + spawnAndAssert, + transform, +}; diff --git a/test/common/fixtures.js b/test/common/fixtures.js index 829cfc7..75815b0 100644 --- a/test/common/fixtures.js +++ b/test/common/fixtures.js @@ -1,19 +1,59 @@ -// https://github.com/nodejs/node/blob/12c0571c8fece32d274eaf0ae197c0eb1948fe11/test/common/fixtures.js -'use strict' +'use strict'; -const path = require('path') -const { pathToFileURL } = require('url') +const path = require('path'); +const fs = require('fs'); +const { pathToFileURL } = require('url'); -const fixturesDir = path.join(__dirname, '..', 'fixtures') +const fixturesDir = path.join(__dirname, '..', 'fixtures'); -function fixturesPath (...args) { - return path.join(fixturesDir, ...args) +function fixturesPath(...args) { + return path.join(fixturesDir, ...args); } -function fixturesFileURL (...args) { - return pathToFileURL(fixturesPath(...args)) + +function fixturesFileURL(...args) { + return pathToFileURL(fixturesPath(...args)); +} + +function readFixtureSync(args, enc) { + if (Array.isArray(args)) + return fs.readFileSync(fixturesPath(...args), enc); + return fs.readFileSync(fixturesPath(args), enc); +} + +function readFixtureKey(name, enc) { + return fs.readFileSync(fixturesPath('keys', name), enc); } +function readFixtureKeys(enc, ...names) { + return names.map((name) => readFixtureKey(name, enc)); +} + +// This should be in sync with test/fixtures/utf8_test_text.txt. +// We copy them here as a string because this is supposed to be used +// in fs API tests. +const utf8TestText = '永和九年,嵗在癸丑,暮春之初,會於會稽山隂之蘭亭,脩稧事也。' + + '羣賢畢至,少長咸集。此地有崇山峻領,茂林脩竹;又有清流激湍,' + + '暎帶左右。引以為流觴曲水,列坐其次。雖無絲竹管弦之盛,一觴一詠,' + + '亦足以暢敘幽情。是日也,天朗氣清,恵風和暢;仰觀宇宙之大,' + + '俯察品類之盛;所以遊目騁懐,足以極視聽之娛,信可樂也。夫人之相與,' + + '俯仰一世,或取諸懐抱,悟言一室之內,或因寄所託,放浪形骸之外。' + + '雖趣舎萬殊,靜躁不同,當其欣扵所遇,暫得扵己,怏然自足,' + + '不知老之將至。及其所之既惓,情隨事遷,感慨係之矣。向之所欣,' + + '俛仰之閒以為陳跡,猶不能不以之興懐;況脩短隨化,終期扵盡。' + + '古人云:「死生亦大矣。」豈不痛哉!每攬昔人興感之由,若合一契,' + + '未嘗不臨文嗟悼,不能喻之扵懐。固知一死生為虛誕,齊彭殤為妄作。' + + '後之視今,亦由今之視昔,悲夫!故列敘時人,錄其所述,雖世殊事異,' + + '所以興懐,其致一也。後之攬者,亦將有感扵斯文。'; + module.exports = { + fixturesDir, path: fixturesPath, - fileURL: fixturesFileURL -} + fileURL: fixturesFileURL, + readSync: readFixtureSync, + readKey: readFixtureKey, + readKeys: readFixtureKeys, + utf8TestText, + get utf8TestTextPath() { + return fixturesPath('utf8_test_text.txt'); + }, +}; diff --git a/test/common/fixtures.mjs b/test/common/fixtures.mjs new file mode 100644 index 0000000..d6f7f6c --- /dev/null +++ b/test/common/fixtures.mjs @@ -0,0 +1,17 @@ +import fixtures from './fixtures.js'; + +const { + fixturesDir, + path, + fileURL, + readSync, + readKey, +} = fixtures; + +export { + fixturesDir, + path, + fileURL, + readSync, + readKey, +}; diff --git a/test/common/index.js b/test/common/index.js index a6269ac..75c3271 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -1,93 +1,231 @@ -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/common/index.js -const assert = require('assert') -const path = require('path') -const util = require('util') -const noop = () => {} +const os = require('node:os'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const cluster = require('node:cluster'); +const { isMainThread } = require('node:worker_threads'); +const { spawnSync, spawn } = require('node:child_process'); +const path = require('node:path'); +const { inspect } = require('node:util'); -const { bin } = require('../../package.json') +const noop = ()=>{}; +const isWindows = process.platform === 'win32'; +const hasCrypto = Boolean(process.versions.openssl) && !process.env.NODE_SKIP_CRYPTO; +const isRiscv64 = process.arch === 'riscv64'; +const isDebug = process.features.debug; +const isPi = (() => { + try { + // Normal Raspberry Pi detection is to find the `Raspberry Pi` string in + // the contents of `/sys/firmware/devicetree/base/model` but that doesn't + // work inside a container. Match the chipset model number instead. + const cpuinfo = fs.readFileSync('/proc/cpuinfo', { encoding: 'utf8' }); + const ok = /^Hardware\s*:\s*(.*)$/im.exec(cpuinfo)?.[1] === 'BCM2835'; + /^/.test(''); // Clear RegExp.$_, some tests expect it to be empty. + return ok; + } catch { + return false; + } +})(); +const isAIX = os.type() === 'AIX'; +const isIBMi = os.type() === 'OS400'; -process.execPath = path.resolve(__dirname, '..', '..', bin.test) +// [assertSnapshot.spawnAndAssert] -const mustCallChecks = [] +function parseTestFlags(filename = process.argv[1]) { + // The copyright notice is relatively big and the flags could come afterwards. + const bytesToRead = 1500; + const buffer = Buffer.allocUnsafe(bytesToRead); + const fd = fs.openSync(filename, 'r'); + const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead); + fs.closeSync(fd); + const source = buffer.toString('utf8', 0, bytesRead); -function runCallChecks (exitCode) { - if (exitCode !== 0) return + const flagStart = source.search(/\/\/ Flags:\s+--/) + 10; - const failed = mustCallChecks.filter(function (context) { - if ('minimum' in context) { - context.messageSegment = `at least ${context.minimum}` - return context.actual < context.minimum + if (flagStart === 9) { + return []; + } + let flagEnd = source.indexOf('\n', flagStart); + // Normalize different EOL. + if (source[flagEnd - 1] === '\r') { + flagEnd--; + } + return source + .substring(flagStart, flagEnd) + .split(/\s+/) + .filter(Boolean); +} + +if (process.argv.length === 2 && + isMainThread && + hasCrypto && + cluster.isPrimary && + fs.existsSync(process.argv[1])) { + const flags = parseTestFlags(); + for (const flag of flags) { + if (!process.execArgv.includes(flag) && (process.features.inspector || !flag.startsWith('--inspect'))) { + const args = [...process.execArgv, ...process.argv.slice(1), ...flags]; + const options = { encoding: 'utf8', stdio: 'inherit' }; + const result = spawnSync(process.execPath, args, options); + if (result.signal) { + process.kill(0, result.signal); + } else { + process.exit(result.status); + } } - context.messageSegment = `exactly ${context.exact}` - return context.actual !== context.exact - }) + } +} - failed.forEach(function (context) { - console.log('Mismatched %s function calls. Expected %s, actual %d.', - context.name, - context.messageSegment, - context.actual) - console.log(context.stack.split('\n').slice(2).join('\n')) - }) +function mustNotCall(msg) { + return function mustNotCall(...args) { + const argsInfo = args.length > 0 ? + `\ncalled with arguments: ${args.map((arg) => inspect(arg)).join(', ')}` : ''; + assert.fail(`${msg || 'function should not have been called'}` + argsInfo); + }; +} + + +function skip(msg) { + printSkipMessage(msg); + process.exit(0); +} + +function printSkipMessage(msg) { + console.log(`1..0 # Skipped: ${msg}`); +} + +function expectRequiredModule(mod, expectation, checkESModule = true) { + const clone = { ...mod }; + if (Object.hasOwn(mod, 'default') && checkESModule) { + assert.strictEqual(mod.__esModule, true); + delete clone.__esModule; + } + assert(isModuleNamespaceObject(mod)); + assert.deepStrictEqual(clone, { ...expectation }); +} - if (failed.length) process.exit(1) +function expectsError(validator, exact) { + return mustCall((...args) => { + if (args.length !== 1) { + // Do not use `assert.strictEqual()` to prevent `inspect` from + // always being called. + assert.fail(`Expected one argument, got ${inspect(args)}`); + } + const error = args.pop(); + assert.throws(() => { throw error; }, validator); + return true; + }, exact); } -function mustCall (fn, exact) { - return _mustCallInner(fn, exact, 'exact') +function platformTimeout(ms) { + const multipliers = typeof ms === 'bigint' ? + { two: 2n, four: 4n, seven: 7n } : { two: 2, four: 4, seven: 7 }; + + if (isDebug) + ms = multipliers.two * ms; + + if (isAIX || isIBMi) + return multipliers.two * ms; // Default localhost speed is slower on AIX + + if (isPi) + return multipliers.two * ms; // Raspberry Pi devices + + if (isRiscv64) { + return multipliers.four * ms; + } + + return ms; } -function getCallSite (top) { - const originalStackFormatter = Error.prepareStackTrace - Error.prepareStackTrace = (err, stack) => // eslint-disable-line n/handle-callback-err - `${stack[0].getFileName()}:${stack[0].getLineNumber()}` - const err = new Error() - Error.captureStackTrace(err, top) - // With the V8 Error API, the stack is not formatted until it is accessed - err.stack // eslint-disable-line no-unused-expressions - Error.prepareStackTrace = originalStackFormatter - return err.stack +function spawnPromisified(...args) { + let stderr = ''; + let stdout = ''; + + const child = spawn(...args); + child.stderr.setEncoding('utf8'); + child.stderr.on('data', (data) => { stderr += data; }); + child.stdout.setEncoding('utf8'); + child.stdout.on('data', (data) => { stdout += data; }); + + return new Promise((resolve, reject) => { + child.on('close', (code, signal) => { + resolve({ + code, + signal, + stderr, + stdout, + }); + }); + child.on('error', (code, signal) => { + reject({ + code, + signal, + stderr, + stdout, + }); + }); + }); } -function mustNotCall (msg) { - const callSite = getCallSite(mustNotCall) - return function mustNotCall (...args) { - const argsInfo = args.length > 0 - ? `\ncalled with arguments: ${args.map((arg) => util.inspect(arg)).join(', ')}` - : '' - assert.fail( - `${msg || 'function should not have been called'} at ${callSite}` + - argsInfo) +function skipIfInspectorDisabled() { + if (!process.features.inspector) { + skip('V8 inspector is disabled'); } } -function _mustCallInner (fn, criteria = 1, field) { - if (process._exiting) { throw new Error('Cannot use common.mustCall*() in process exit handler') } +const mustCallChecks = []; + +function runCallChecks(exitCode) { + if (exitCode !== 0) return; + + const failed = mustCallChecks.filter(function(context) { + if ('minimum' in context) { + context.messageSegment = `at least ${context.minimum}`; + return context.actual < context.minimum; + } + context.messageSegment = `exactly ${context.exact}`; + return context.actual !== context.exact; + }); + + failed.forEach(function(context) { + console.log('Mismatched %s function calls. Expected %s, actual %d.', + context.name, + context.messageSegment, + context.actual); + console.log(context.stack.split('\n').slice(2).join('\n')); + }); + + if (failed.length) process.exit(1); +} + +function _mustCallInner(fn, criteria = 1, field) { + if (process._exiting) + throw new Error('Cannot use common.mustCall*() in process exit handler'); if (typeof fn === 'number') { - criteria = fn - fn = noop + criteria = fn; + fn = noop; } else if (fn === undefined) { - fn = noop + fn = noop; } - if (typeof criteria !== 'number') { throw new TypeError(`Invalid ${field} value: ${criteria}`) } + if (typeof criteria !== 'number') + throw new TypeError(`Invalid ${field} value: ${criteria}`); const context = { [field]: criteria, actual: 0, - stack: util.inspect(new Error()), - name: fn.name || '' - } + stack: inspect(new Error()), + name: fn.name || '', + }; // Add the exit listener only once to avoid listener leak warnings - if (mustCallChecks.length === 0) process.on('exit', runCallChecks) + if (mustCallChecks.length === 0) process.on('exit', runCallChecks); - mustCallChecks.push(context) + mustCallChecks.push(context); - const _return = function () { // eslint-disable-line func-style - context.actual++ - return fn.apply(this, arguments) - } + const _return = function() { // eslint-disable-line func-style + context.actual++; + return fn.apply(this, arguments); + }; // Function instances have own properties that may be relevant. // Let's replicate those properties to the returned function. // Refs: https://tc39.es/ecma262/#sec-function-instances @@ -96,79 +234,38 @@ function _mustCallInner (fn, criteria = 1, field) { value: fn.name, writable: false, enumerable: false, - configurable: true + configurable: true, }, length: { value: fn.length, writable: false, enumerable: false, - configurable: true - } - }) - return _return -} - -// Useful for testing expected internal/error objects -function expectsError (validator, exact) { - return mustCall((...args) => { - if (args.length !== 1) { - // Do not use `assert.strictEqual()` to prevent `inspect` from - // always being called. - assert.fail(`Expected one argument, got ${util.inspect(args)}`) - } - const error = args.pop() - const descriptor = Object.getOwnPropertyDescriptor(error, 'message') - // The error message should be non-enumerable - assert.strictEqual(descriptor.enumerable, false) - - assert.throws(() => { throw error }, validator) - return true - }, exact) -} - -if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout !== 'function') { - // `AbortSignal.timeout` is not available on Node.js 14.x, we need to polyfill - // it because some tests are using it. End-users don't need to it. - - class AbortError extends Error { - constructor (message = 'The operation was aborted', options = undefined) { - super(message, options) - this.code = 23 - } - } - - AbortSignal.timeout = function timeout (delay) { - const ac = new AbortController() - setTimeout(() => ac.abort(new AbortError( - 'The operation was aborted due to timeout')), delay).unref() - return ac.signal - } + configurable: true, + }, + }); + return _return; } -if (typeof AbortSignal !== 'undefined' && (process.version.startsWith('v14.') || process.version.startsWith('v16.'))) { - // Implementation of AbortSignal and AbortController differ slightly with the - // v18.x one, creating some difference on the TAP output which makes the tests - // fail. We are overriding the built-ins to make the test pass, however that's - // not necessary to do for the library to work (i.e. end-users don't need it). - - const defaultAbortError = new Error('This operation was aborted') - defaultAbortError.code = 20 - - AbortSignal.abort = function abort (reason = defaultAbortError) { - const controller = new AbortController() - controller.abort(reason) - return controller.signal - } - const nativeAbort = AbortController.prototype.abort - AbortController.prototype.abort = function abort (reason = defaultAbortError) { - this.signal.reason = reason - nativeAbort.call(this, reason) - } +function mustCall(fn, exact) { + return _mustCallInner(fn, exact, 'exact'); } module.exports = { + spawnPromisified, + skipIfInspectorDisabled, + platformTimeout, + expectRequiredModule, expectsError, - isWindow: process.platform === 'win32', + printSkipMessage, + skip, + allowGlobals: () => { }, + mustNotCall, mustCall, - mustNotCall -} + parseTestFlags, + testRunnerPath: path.resolve(__dirname, '..', '..', 'lib', 'cli.js'), + isMainThread, + isWindows, + isAIX, + isIBMi, + hasCrypto, +} \ No newline at end of file diff --git a/test/common/index.mjs b/test/common/index.mjs new file mode 100644 index 0000000..e2619c8 --- /dev/null +++ b/test/common/index.mjs @@ -0,0 +1,36 @@ +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); +const common = require('./index.js'); + +const { + allowGlobals, + expectsError, + isAIX, + isIBMi, + isWindows, + mustCall, + mustNotCall, + platformTimeout, + printSkipMessage, + skip, + skipIfInspectorDisabled, + spawnPromisified, + testRunnerPath +} = common; + +export { + allowGlobals, + expectsError, + isAIX, + isIBMi, + isWindows, + mustCall, + mustNotCall, + platformTimeout, + printSkipMessage, + skip, + skipIfInspectorDisabled, + spawnPromisified, + testRunnerPath, +}; diff --git a/test/common/inspector-helper.js b/test/common/inspector-helper.js new file mode 100644 index 0000000..864538f --- /dev/null +++ b/test/common/inspector-helper.js @@ -0,0 +1,549 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const http = require('http'); +const fixtures = require('../common/fixtures'); +const { spawn } = require('child_process'); +const { URL, pathToFileURL } = require('url'); +const { EventEmitter } = require('events'); + +const _MAINSCRIPT = fixtures.path('loop.js'); +const DEBUG = false; +const TIMEOUT = common.platformTimeout(15 * 1000); + +function spawnChildProcess(inspectorFlags, scriptContents, scriptFile) { + const args = [].concat(inspectorFlags); + if (scriptContents) { + args.push('-e', scriptContents); + } else { + args.push(scriptFile); + } + const child = spawn(process.execPath, args); + + const handler = tearDown.bind(null, child); + process.on('exit', handler); + process.on('uncaughtException', handler); + process.on('unhandledRejection', handler); + process.on('SIGINT', handler); + + return child; +} + +function makeBufferingDataCallback(dataCallback) { + let buffer = Buffer.alloc(0); + return (data) => { + const newData = Buffer.concat([buffer, data]); + const str = newData.toString('utf8'); + const lines = str.replace(/\r/g, '').split('\n'); + if (str.endsWith('\n')) + buffer = Buffer.alloc(0); + else + buffer = Buffer.from(lines.pop(), 'utf8'); + for (const line of lines) + dataCallback(line); + }; +} + +function tearDown(child, err) { + child.kill(); + if (err) { + console.error(err); + process.exit(1); + } +} + +function parseWSFrame(buffer) { + // Protocol described in https://tools.ietf.org/html/rfc6455#section-5 + let message = null; + if (buffer.length < 2) + return { length: 0, message }; + if (buffer[0] === 0x88 && buffer[1] === 0x00) { + return { length: 2, message, closed: true }; + } + assert.strictEqual(buffer[0], 0x81); + let dataLen = 0x7F & buffer[1]; + let bodyOffset = 2; + if (buffer.length < bodyOffset + dataLen) + return 0; + if (dataLen === 126) { + dataLen = buffer.readUInt16BE(2); + bodyOffset = 4; + } else if (dataLen === 127) { + assert(buffer[2] === 0 && buffer[3] === 0, 'Inspector message too big'); + dataLen = buffer.readUIntBE(4, 6); + bodyOffset = 10; + } + if (buffer.length < bodyOffset + dataLen) + return { length: 0, message }; + const jsonPayload = + buffer.slice(bodyOffset, bodyOffset + dataLen).toString('utf8'); + try { + message = JSON.parse(jsonPayload); + } catch (e) { + console.error(`JSON.parse() failed for: ${jsonPayload}`); + throw e; + } + if (DEBUG) + console.log('[received]', JSON.stringify(message)); + return { length: bodyOffset + dataLen, message }; +} + +function formatWSFrame(message) { + const messageBuf = Buffer.from(JSON.stringify(message)); + + const wsHeaderBuf = Buffer.allocUnsafe(16); + wsHeaderBuf.writeUInt8(0x81, 0); + let byte2 = 0x80; + const bodyLen = messageBuf.length; + + let maskOffset = 2; + if (bodyLen < 126) { + byte2 = 0x80 + bodyLen; + } else if (bodyLen < 65536) { + byte2 = 0xFE; + wsHeaderBuf.writeUInt16BE(bodyLen, 2); + maskOffset = 4; + } else { + byte2 = 0xFF; + wsHeaderBuf.writeUInt32BE(bodyLen, 2); + wsHeaderBuf.writeUInt32BE(0, 6); + maskOffset = 10; + } + wsHeaderBuf.writeUInt8(byte2, 1); + wsHeaderBuf.writeUInt32BE(0x01020408, maskOffset); + + for (let i = 0; i < messageBuf.length; i++) + messageBuf[i] = messageBuf[i] ^ (1 << (i % 4)); + + return Buffer.concat([wsHeaderBuf.slice(0, maskOffset + 4), messageBuf]); +} + +class InspectorSession { + constructor(socket, instance) { + this._instance = instance; + this._socket = socket; + this._nextId = 1; + this._commandResponsePromises = new Map(); + this._unprocessedNotifications = []; + this._notificationCallback = null; + this._scriptsIdsByUrl = new Map(); + this._pausedDetails = null; + + let buffer = Buffer.alloc(0); + socket.on('data', (data) => { + buffer = Buffer.concat([buffer, data]); + do { + const { length, message, closed } = parseWSFrame(buffer); + if (!length) + break; + + if (closed) { + socket.write(Buffer.from([0x88, 0x00])); // WS close frame + } + buffer = buffer.slice(length); + if (message) + this._onMessage(message); + } while (true); + }); + this._terminationPromise = new Promise((resolve) => { + socket.once('close', resolve); + }); + } + + + waitForServerDisconnect() { + return this._terminationPromise; + } + + async disconnect() { + this._socket.destroy(); + return this.waitForServerDisconnect(); + } + + _onMessage(message) { + if (message.id) { + const { resolve, reject } = this._commandResponsePromises.get(message.id); + this._commandResponsePromises.delete(message.id); + if (message.result) + resolve(message.result); + else + reject(message.error); + } else { + if (message.method === 'Debugger.scriptParsed') { + const { scriptId, url } = message.params; + this._scriptsIdsByUrl.set(scriptId, url); + const fileUrl = url.startsWith('file:') ? + url : pathToFileURL(url).toString(); + if (fileUrl === this.scriptURL().toString()) { + this.mainScriptId = scriptId; + } + } + if (message.method === 'Debugger.paused') + this._pausedDetails = message.params; + if (message.method === 'Debugger.resumed') + this._pausedDetails = null; + + if (this._notificationCallback) { + // In case callback needs to install another + const callback = this._notificationCallback; + this._notificationCallback = null; + callback(message); + } else { + this._unprocessedNotifications.push(message); + } + } + } + + unprocessedNotifications() { + return this._unprocessedNotifications; + } + + _sendMessage(message) { + const msg = JSON.parse(JSON.stringify(message)); // Clone! + msg.id = this._nextId++; + if (DEBUG) + console.log('[sent]', JSON.stringify(msg)); + + const responsePromise = new Promise((resolve, reject) => { + this._commandResponsePromises.set(msg.id, { resolve, reject }); + }); + + return new Promise( + (resolve) => this._socket.write(formatWSFrame(msg), resolve)) + .then(() => responsePromise); + } + + send(commands) { + if (Array.isArray(commands)) { + // Multiple commands means the response does not matter. There might even + // never be a response. + return Promise + .all(commands.map((command) => this._sendMessage(command))) + .then(() => {}); + } + return this._sendMessage(commands); + } + + waitForNotification(methodOrPredicate, description) { + const desc = description || methodOrPredicate; + const message = `Timed out waiting for matching notification (${desc})`; + return fires( + this._asyncWaitForNotification(methodOrPredicate), message, TIMEOUT); + } + + async _asyncWaitForNotification(methodOrPredicate) { + function matchMethod(notification) { + return notification.method === methodOrPredicate; + } + const predicate = + typeof methodOrPredicate === 'string' ? matchMethod : methodOrPredicate; + let notification = null; + do { + if (this._unprocessedNotifications.length) { + notification = this._unprocessedNotifications.shift(); + } else { + notification = await new Promise( + (resolve) => this._notificationCallback = resolve); + } + } while (!predicate(notification)); + return notification; + } + + _isBreakOnLineNotification(message, line, expectedScriptPath) { + if (message.method === 'Debugger.paused') { + const callFrame = message.params.callFrames[0]; + const location = callFrame.location; + const scriptPath = this._scriptsIdsByUrl.get(location.scriptId); + assert.strictEqual(decodeURIComponent(scriptPath), + decodeURIComponent(expectedScriptPath), + `${scriptPath} !== ${expectedScriptPath}`); + assert.strictEqual(location.lineNumber, line); + return true; + } + } + + waitForBreakOnLine(line, url) { + return this + .waitForNotification( + (notification) => + this._isBreakOnLineNotification(notification, line, url), + `break on ${url}:${line}`); + } + + waitForPauseOnStart() { + return this + .waitForNotification( + (notification) => + notification.method === 'Debugger.paused' && notification.params.reason === 'Break on start', + 'break on start'); + } + + pausedDetails() { + return this._pausedDetails; + } + + _matchesConsoleOutputNotification(notification, type, values) { + if (!Array.isArray(values)) + values = [ values ]; + if (notification.method === 'Runtime.consoleAPICalled') { + const params = notification.params; + if (params.type === type) { + let i = 0; + for (const value of params.args) { + if (value.value !== values[i++]) + return false; + } + return i === values.length; + } + } + } + + waitForConsoleOutput(type, values) { + const desc = `Console output matching ${JSON.stringify(values)}`; + return this.waitForNotification( + (notification) => this._matchesConsoleOutputNotification(notification, + type, values), + desc); + } + + async runToCompletion() { + console.log('[test]', 'Verify node waits for the frontend to disconnect'); + await this.send({ 'method': 'Debugger.resume' }); + await this.waitForNotification((notification) => { + if (notification.method === 'Debugger.paused') { + this.send({ 'method': 'Debugger.resume' }); + } + return notification.method === 'Runtime.executionContextDestroyed' && + notification.params.executionContextId === 1; + }); + await this.waitForDisconnect(); + } + + async waitForDisconnect() { + while ((await this._instance.nextStderrString()) !== + 'Waiting for the debugger to disconnect...'); + await this.disconnect(); + } + + scriptPath() { + return this._instance.scriptPath(); + } + + script() { + return this._instance.script(); + } + + scriptURL() { + return pathToFileURL(this.scriptPath()); + } +} + +class NodeInstance extends EventEmitter { + constructor(inspectorFlags = ['--inspect-brk=0', '--expose-internals'], + scriptContents = '', + scriptFile = _MAINSCRIPT, + logger = console) { + super(); + + this._logger = logger; + this._scriptPath = scriptFile; + this._script = scriptFile ? null : scriptContents; + this._portCallback = null; + this.resetPort(); + this._process = spawnChildProcess(inspectorFlags, scriptContents, + scriptFile); + this._running = true; + this._stderrLineCallback = null; + this._unprocessedStderrLines = []; + + this._process.stdout.on('data', makeBufferingDataCallback( + (line) => { + this.emit('stdout', line); + this._logger.log('[out]', line); + })); + + this._process.stderr.on('data', makeBufferingDataCallback( + (message) => this.onStderrLine(message))); + + this._shutdownPromise = new Promise((resolve) => { + this._process.once('exit', (exitCode, signal) => { + if (signal) { + this._logger.error(`[err] child process crashed, signal ${signal}`); + } + resolve({ exitCode, signal }); + this._running = false; + }); + }); + } + + get pid() { + return this._process.pid; + } + + resetPort() { + this.portPromise = new Promise((resolve) => this._portCallback = resolve); + } + + static async startViaSignal(scriptContents) { + const instance = new NodeInstance( + ['--expose-internals', '--inspect-port=0'], + `${scriptContents}\nprocess._rawDebug('started');`, undefined); + const msg = 'Timed out waiting for process to start'; + while (await fires(instance.nextStderrString(), msg, TIMEOUT) !== 'started'); + process._debugProcess(instance._process.pid); + return instance; + } + + onStderrLine(line) { + this.emit('stderr', line); + this._logger.log('[err]', line); + if (this._portCallback) { + const matches = line.match(/Debugger listening on ws:\/\/.+:(\d+)\/.+/); + if (matches) { + this._portCallback(matches[1]); + this._portCallback = null; + } + } + if (this._stderrLineCallback) { + this._stderrLineCallback(line); + this._stderrLineCallback = null; + } else { + this._unprocessedStderrLines.push(line); + } + } + + httpGet(host, path, hostHeaderValue) { + this._logger.log('[test]', `Testing ${path}`); + const headers = hostHeaderValue ? { 'Host': hostHeaderValue } : null; + return this.portPromise.then((port) => new Promise((resolve, reject) => { + const req = http.get({ host, port, family: 4, path, headers }, (res) => { + let response = ''; + res.setEncoding('utf8'); + res + .on('data', (data) => response += data.toString()) + .on('end', () => { + resolve(response); + }); + }); + req.on('error', reject); + })).then((response) => { + try { + return JSON.parse(response); + } catch (e) { + e.body = response; + throw e; + } + }); + } + + async sendUpgradeRequest() { + const response = await this.httpGet(null, '/json/list'); + const devtoolsUrl = response[0].webSocketDebuggerUrl; + const port = await this.portPromise; + return http.get({ + port, + family: 4, + path: new URL(devtoolsUrl).pathname, + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Version': 13, + 'Sec-WebSocket-Key': 'key==', + }, + }); + } + + async connectInspectorSession() { + this._logger.log('[test]', 'Connecting to a child Node process'); + const upgradeRequest = await this.sendUpgradeRequest(); + return new Promise((resolve) => { + upgradeRequest + .on('upgrade', + (message, socket) => resolve(new InspectorSession(socket, this))) + .on('response', common.mustNotCall('Upgrade was not received')); + }); + } + + async expectConnectionDeclined() { + this._logger.log('[test]', 'Checking upgrade is not possible'); + const upgradeRequest = await this.sendUpgradeRequest(); + return new Promise((resolve) => { + upgradeRequest + .on('upgrade', common.mustNotCall('Upgrade was received')) + .on('response', (response) => + response.on('data', () => {}) + .on('end', () => resolve(response.statusCode))); + }); + } + + expectShutdown() { + return this._shutdownPromise; + } + + nextStderrString() { + if (this._unprocessedStderrLines.length) + return Promise.resolve(this._unprocessedStderrLines.shift()); + return new Promise((resolve) => this._stderrLineCallback = resolve); + } + + write(message) { + this._process.stdin.write(message); + } + + kill() { + this._process.kill(); + return this.expectShutdown(); + } + + scriptPath() { + return this._scriptPath; + } + + script() { + if (this._script === null) + this._script = fs.readFileSync(this.scriptPath(), 'utf8'); + return this._script; + } +} + +function onResolvedOrRejected(promise, callback) { + return promise.then((result) => { + callback(); + return result; + }, (error) => { + callback(); + throw error; + }); +} + +function timeoutPromise(error, timeoutMs) { + let clearCallback = null; + let done = false; + const promise = onResolvedOrRejected(new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(error), timeoutMs); + clearCallback = () => { + if (done) + return; + clearTimeout(timeout); + resolve(); + }; + }), () => done = true); + promise.clear = clearCallback; + return promise; +} + +// Returns a new promise that will propagate `promise` resolution or rejection +// if that happens within the `timeoutMs` timespan, or rejects with `error` as +// a reason otherwise. +function fires(promise, error, timeoutMs) { + const timeout = timeoutPromise(error, timeoutMs); + return Promise.race([ + onResolvedOrRejected(promise, () => timeout.clear()), + timeout, + ]); +} + +module.exports = { + NodeInstance, +}; diff --git a/test/common/pseudo-tty.py b/test/common/pseudo-tty.py new file mode 100644 index 0000000..9bb3948 --- /dev/null +++ b/test/common/pseudo-tty.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +import errno +import os +import pty +import select +import signal +import sys +import termios + +STDIN = 0 +STDOUT = 1 +STDERR = 2 + + +def pipe(sfd, dfd): + try: + data = os.read(sfd, 256) + except OSError as e: + if e.errno != errno.EIO: + raise + return True # EOF + + if not data: + return True # EOF + + if dfd == STDOUT: + # Work around platform quirks. Some platforms echo ^D as \x04 + # (AIX, BSDs) and some don't (Linux). + # + # The comparison against b' '[0] is because |data| is + # a string in python2 but a bytes object in python3. + filt = lambda c: c >= b' '[0] or c in b'\t\n\r\f' + data = filter(filt, data) + data = bytes(data) + + while data: + try: + n = os.write(dfd, data) + except OSError as e: + if e.errno != errno.EIO: + raise + return True # EOF + data = data[n:] + + +if __name__ == '__main__': + argv = sys.argv[1:] + + # Make select() interruptible by SIGCHLD. + signal.signal(signal.SIGCHLD, lambda nr, _: None) + + parent_fd, child_fd = pty.openpty() + assert parent_fd > STDIN + + mode = termios.tcgetattr(child_fd) + # Don't translate \n to \r\n. + mode[1] = mode[1] & ~termios.ONLCR # oflag + # Disable ECHOCTL. It's a BSD-ism that echoes e.g. \x04 as ^D but it + # doesn't work on platforms like AIX and Linux. I checked Linux's tty + # driver and it's a no-op, the driver is just oblivious to the flag. + mode[3] = mode[3] & ~termios.ECHOCTL # lflag + termios.tcsetattr(child_fd, termios.TCSANOW, mode) + + pid = os.fork() + if not pid: + os.setsid() + os.close(parent_fd) + + # Ensure the pty is a controlling tty. + name = os.ttyname(child_fd) + fd = os.open(name, os.O_RDWR) + os.dup2(fd, child_fd) + os.close(fd) + + os.dup2(child_fd, STDIN) + os.dup2(child_fd, STDOUT) + os.dup2(child_fd, STDERR) + + if child_fd > STDERR: + os.close(child_fd) + + os.execve(argv[0], argv, os.environ) + raise Exception('unreachable') + + os.close(child_fd) + + fds = [STDIN, parent_fd] + while fds: + try: + rfds, _, _ = select.select(fds, [], []) + except select.error as e: + if e[0] != errno.EINTR: + raise + if pid == os.waitpid(pid, os.WNOHANG)[0]: + break + + if STDIN in rfds: + if pipe(STDIN, parent_fd): + fds.remove(STDIN) + + if parent_fd in rfds: + if pipe(parent_fd, STDOUT): + break \ No newline at end of file diff --git a/test/common/tmpdir.js b/test/common/tmpdir.js index 5442efe..f1f0681 100644 --- a/test/common/tmpdir.js +++ b/test/common/tmpdir.js @@ -1,62 +1,95 @@ -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/common/tmpdir.js -'use strict' +'use strict'; -const fs = require('fs') -const path = require('path') -const { isMainThread } = require('worker_threads') +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const { pathToFileURL } = require('url'); +const { isMainThread } = require('worker_threads'); -function rmSync (pathname) { - fs.rmSync(pathname, { maxRetries: 3, recursive: true, force: true }) +function rmSync(pathname, useSpawn) { + if (useSpawn) { + const escapedPath = pathname.replaceAll('\\', '\\\\'); + spawnSync( + process.execPath, + [ + '-e', + `require("fs").rmSync("${escapedPath}", { maxRetries: 3, recursive: true, force: true });`, + ], + ); + } else { + fs.rmSync(pathname, { maxRetries: 3, recursive: true, force: true }); + } } -const testRoot = process.env.NODE_TEST_DIR - ? fs.realpathSync(process.env.NODE_TEST_DIR) - : path.resolve(__dirname, '..') +const testRoot = process.env.NODE_TEST_DIR ? + fs.realpathSync(process.env.NODE_TEST_DIR) : path.resolve(__dirname, '..'); // Using a `.` prefixed name, which is the convention for "hidden" on POSIX, // gets tools to ignore it by default or by simple rules, especially eslint. const tmpdirName = '.tmp.' + - (process.env.TEST_SERIAL_ID || process.env.TEST_THREAD_ID || '0') -const tmpPath = path.join(testRoot, tmpdirName) + (process.env.TEST_SERIAL_ID || process.env.TEST_THREAD_ID || '0'); +const tmpPath = path.join(testRoot, tmpdirName); -let firstRefresh = true -function refresh () { - rmSync(this.path) - fs.mkdirSync(this.path) +let firstRefresh = true; +function refresh(useSpawn = false) { + rmSync(tmpPath, useSpawn); + fs.mkdirSync(tmpPath); if (firstRefresh) { - firstRefresh = false + firstRefresh = false; // Clean only when a test uses refresh. This allows for child processes to // use the tmpdir and only the parent will clean on exit. - process.on('exit', onexit) + process.on('exit', () => { + return onexit(useSpawn); + }); } } -function onexit () { +function onexit(useSpawn) { // Change directory to avoid possible EBUSY - if (isMainThread) { process.chdir(testRoot) } + if (isMainThread) + process.chdir(testRoot); try { - rmSync(tmpPath) + rmSync(tmpPath, useSpawn); } catch (e) { - console.error('Can\'t clean tmpdir:', tmpPath) + console.error('Can\'t clean tmpdir:', tmpPath); - const files = fs.readdirSync(tmpPath) - console.error('Files blocking:', files) + const files = fs.readdirSync(tmpPath); + console.error('Files blocking:', files); if (files.some((f) => f.startsWith('.nfs'))) { // Warn about NFS "silly rename" console.error('Note: ".nfs*" might be files that were open and ' + - 'unlinked but not closed.') - console.error('See http://nfs.sourceforge.net/#faq_d2 for details.') + 'unlinked but not closed.'); + console.error('See http://nfs.sourceforge.net/#faq_d2 for details.'); } - console.error() - throw e + console.error(); + throw e; } } +function resolve(...paths) { + return path.resolve(tmpPath, ...paths); +} + +function hasEnoughSpace(size) { + const { bavail, bsize } = fs.statfsSync(tmpPath); + return bavail >= Math.ceil(size / bsize); +} + +function fileURL(...paths) { + // When called without arguments, add explicit trailing slash + const fullPath = path.resolve(tmpPath + path.sep, ...paths); + + return pathToFileURL(fullPath); +} + module.exports = { + fileURL, + hasEnoughSpace, path: tmpPath, - refresh -} + refresh, + resolve, +}; diff --git a/test/fixtures/empty.js b/test/fixtures/empty.js new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/module-mocking/basic-cjs.js b/test/fixtures/module-mocking/basic-cjs.js new file mode 100644 index 0000000..35483ad --- /dev/null +++ b/test/fixtures/module-mocking/basic-cjs.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + string: 'original cjs string', +}; \ No newline at end of file diff --git a/test/fixtures/module-mocking/basic-esm-without-extension.js b/test/fixtures/module-mocking/basic-esm-without-extension.js new file mode 100644 index 0000000..fc63ebd --- /dev/null +++ b/test/fixtures/module-mocking/basic-esm-without-extension.js @@ -0,0 +1 @@ +export let string = 'original esm string'; \ No newline at end of file diff --git a/test/fixtures/module-mocking/basic-esm.mjs b/test/fixtures/module-mocking/basic-esm.mjs new file mode 100644 index 0000000..fc63ebd --- /dev/null +++ b/test/fixtures/module-mocking/basic-esm.mjs @@ -0,0 +1 @@ +export let string = 'original esm string'; \ No newline at end of file diff --git a/test/fixtures/module-mocking/wrong-import-after-module-mocking.mjs b/test/fixtures/module-mocking/wrong-import-after-module-mocking.mjs new file mode 100644 index 0000000..b82a72b --- /dev/null +++ b/test/fixtures/module-mocking/wrong-import-after-module-mocking.mjs @@ -0,0 +1,9 @@ +import pkg from '#node:test'; +const { mock } = pkg; + +try { + mock.module?.('Whatever, this is not significant', { namedExports: {} }); +} catch {} + +const { string } = await import('./basic-esm-without-extension'); +console.log(`Found string: ${string}`); // prints 'original esm string' \ No newline at end of file diff --git a/test/fixtures/node-core-test/esm.test.mjs b/test/fixtures/node-core-test/esm.test.mjs deleted file mode 100644 index 9e77808..0000000 --- a/test/fixtures/node-core-test/esm.test.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import test from '#node:test' - -test('this should pass') diff --git a/test/fixtures/node-core-test/finished-tla-with-explicit-exitCode-modification.mjs b/test/fixtures/node-core-test/finished-tla-with-explicit-exitCode-modification.mjs deleted file mode 100644 index a52fe7c..0000000 --- a/test/fixtures/node-core-test/finished-tla-with-explicit-exitCode-modification.mjs +++ /dev/null @@ -1,2 +0,0 @@ -await Promise.resolve() -process.exitCode = 13 diff --git a/test/fixtures/node-core-test/finished-tla-with-explicit-process.exit-call.mjs b/test/fixtures/node-core-test/finished-tla-with-explicit-process.exit-call.mjs deleted file mode 100644 index d5f0afe..0000000 --- a/test/fixtures/node-core-test/finished-tla-with-explicit-process.exit-call.mjs +++ /dev/null @@ -1,2 +0,0 @@ -await Promise.resolve() -process.exit() diff --git a/test/fixtures/node-core-test/unfinished-tla.mjs b/test/fixtures/node-core-test/unfinished-tla.mjs deleted file mode 100644 index 0390112..0000000 --- a/test/fixtures/node-core-test/unfinished-tla.mjs +++ /dev/null @@ -1 +0,0 @@ -await new Promise(() => {}) diff --git a/test/fixtures/test-runner/.gitignore b/test/fixtures/test-runner/.gitignore deleted file mode 100644 index 736e8ae..0000000 --- a/test/fixtures/test-runner/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!node_modules \ No newline at end of file diff --git a/test/fixtures/test-runner/aborts/failed-test-still-call-abort.js b/test/fixtures/test-runner/aborts/failed-test-still-call-abort.js new file mode 100644 index 0000000..1479815 --- /dev/null +++ b/test/fixtures/test-runner/aborts/failed-test-still-call-abort.js @@ -0,0 +1,25 @@ +const {test, afterEach} = require('#node:test'); +const assert = require('node:assert'); +const { waitForAbort } = require('./wait-for-abort-helper'); + +let testCount = 0; +let signal; + +afterEach(() => { + assert.equal(signal.aborted, false); + + waitForAbort({ testNumber: ++testCount, signal }); +}); + +test("sync", (t) => { + signal = t.signal; + assert.equal(signal.aborted, false); + throw new Error('failing the sync test'); +}); + +test("async", async (t) => { + await null; + signal = t.signal; + assert.equal(signal.aborted, false); + throw new Error('failing the async test'); +}); diff --git a/test/fixtures/test-runner/aborts/successful-test-still-call-abort.js b/test/fixtures/test-runner/aborts/successful-test-still-call-abort.js new file mode 100644 index 0000000..92d688d --- /dev/null +++ b/test/fixtures/test-runner/aborts/successful-test-still-call-abort.js @@ -0,0 +1,23 @@ +const {test, afterEach} = require('#node:test'); +const assert = require('node:assert'); +const {waitForAbort} = require("./wait-for-abort-helper"); + +let testCount = 0; +let signal; + +afterEach(() => { + assert.equal(signal.aborted, false); + + waitForAbort({ testNumber: ++testCount, signal }); +}); + +test("sync", (t) => { + signal = t.signal; + assert.equal(signal.aborted, false); +}); + +test("async", async (t) => { + await null; + signal = t.signal; + assert.equal(signal.aborted, false); +}); diff --git a/test/fixtures/test-runner/aborts/wait-for-abort-helper.js b/test/fixtures/test-runner/aborts/wait-for-abort-helper.js new file mode 100644 index 0000000..89eda7e --- /dev/null +++ b/test/fixtures/test-runner/aborts/wait-for-abort-helper.js @@ -0,0 +1,19 @@ +module.exports = { + waitForAbort: function ({ testNumber, signal }) { + let retries = 0; + + const interval = setInterval(() => { + retries++; + if(signal.aborted) { + console.log(`abort called for test ${testNumber}`); + clearInterval(interval); + return; + } + + if(retries > 100) { + clearInterval(interval); + throw new Error(`abort was not called for test ${testNumber}`); + } + }, 10); + } +} diff --git a/test/fixtures/test-runner/async-error-in-test-hook.mjs b/test/fixtures/test-runner/async-error-in-test-hook.mjs new file mode 100644 index 0000000..073e8c3 --- /dev/null +++ b/test/fixtures/test-runner/async-error-in-test-hook.mjs @@ -0,0 +1,28 @@ +import pkg from '#node:test' +const { after, afterEach, before, beforeEach, test } = pkg; + +before(() => { + setTimeout(() => { + throw new Error('before') + }, 100) +}) + +beforeEach(() => { + setTimeout(() => { + throw new Error('beforeEach') + }, 100) +}) + +after(() => { + setTimeout(() => { + throw new Error('after') + }, 100) +}) + +afterEach(() => { + setTimeout(() => { + throw new Error('afterEach') + }, 100) +}) + +test('ok', () => {}) diff --git a/test/fixtures/test-runner/concurrency/a.mjs b/test/fixtures/test-runner/concurrency/a.mjs new file mode 100644 index 0000000..a34b87e --- /dev/null +++ b/test/fixtures/test-runner/concurrency/a.mjs @@ -0,0 +1,13 @@ +import tmpdir from '../../../common/tmpdir.js'; +import { setTimeout } from 'node:timers/promises'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +await fs.writeFile(tmpdir.resolve('test-runner-concurrency'), 'a.mjs'); +while (true) { + const file = await fs.readFile(tmpdir.resolve('test-runner-concurrency'), 'utf8'); + if (file === 'b.mjs') { + break; + } + await setTimeout(10); +} diff --git a/test/fixtures/test-runner/concurrency/b.mjs b/test/fixtures/test-runner/concurrency/b.mjs new file mode 100644 index 0000000..395cea1 --- /dev/null +++ b/test/fixtures/test-runner/concurrency/b.mjs @@ -0,0 +1,13 @@ +import tmpdir from '../../../common/tmpdir.js'; +import { setTimeout } from 'node:timers/promises'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +while (true) { + const file = await fs.readFile(tmpdir.resolve('test-runner-concurrency'), 'utf8'); + if (file === 'a.mjs') { + await fs.writeFile(tmpdir.resolve('test-runner-concurrency'), 'b.mjs'); + break; + } + await setTimeout(10); +} diff --git a/test/fixtures/test-runner/custom_reporters/coverage.mjs b/test/fixtures/test-runner/custom_reporters/coverage.mjs new file mode 100644 index 0000000..c1b8848 --- /dev/null +++ b/test/fixtures/test-runner/custom_reporters/coverage.mjs @@ -0,0 +1,15 @@ +import { Transform } from 'node:stream'; + +export default class CoverageReporter extends Transform { + constructor(options) { + super({ ...options, writableObjectMode: true }); + } + + _transform(event, _encoding, callback) { + if (event.type === 'test:coverage') { + callback(null, JSON.stringify(event.data, null, 2)); + } else { + callback(null); + } + } +} diff --git a/test/fixtures/test-runner/custom_reporters/custom.cjs b/test/fixtures/test-runner/custom_reporters/custom.cjs index 3eb0d54..a3f653d 100644 --- a/test/fixtures/test-runner/custom_reporters/custom.cjs +++ b/test/fixtures/test-runner/custom_reporters/custom.cjs @@ -1,18 +1,17 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/test/fixtures/test-runner/custom_reporters/custom.cjs -const { Transform } = require('node:stream') +const { Transform } = require('node:stream'); const customReporter = new Transform({ writableObjectMode: true, - transform (event, encoding, callback) { - this.counters = this.counters ?? {} - this.counters[event.type] = (this.counters[event.type] ?? 0) + 1 - callback() + transform(event, encoding, callback) { + this.counters ??= {}; + this.counters[event.type] = (this.counters[event.type] ?? 0) + 1; + callback(); }, - flush (callback) { + flush(callback) { this.push('custom.cjs ') - this.push(JSON.stringify(this.counters)) - callback() + this.push(JSON.stringify(this.counters)); + callback(); } -}) +}); -module.exports = customReporter +module.exports = customReporter; diff --git a/test/fixtures/test-runner/custom_reporters/custom.js b/test/fixtures/test-runner/custom_reporters/custom.js index bdde0d2..aa85eab 100644 --- a/test/fixtures/test-runner/custom_reporters/custom.js +++ b/test/fixtures/test-runner/custom_reporters/custom.js @@ -1,14 +1,14 @@ -// https://github.com/nodejs/node/blob/9eb363a3e00dbba572756c7ed314273f17ea8e2e/test/fixtures/test-runner/custom_reporters/custom.js -const assert = require('assert') -const path = require('path') -module.exports = async function * customReporter (source) { - const counters = {} +const assert = require('assert'); +const path = require('path'); + +module.exports = async function * customReporter(source) { + const counters = {}; for await (const event of source) { if (event.data.file) { - assert.strictEqual(event.data.file, path.resolve(__dirname, '../reporters.js')) + assert.strictEqual(event.data.file, path.resolve(__dirname, '../reporters.js')); } - counters[event.type] = (counters[event.type] ?? 0) + 1 + counters[event.type] = (counters[event.type] ?? 0) + 1; } - yield 'custom.js ' - yield JSON.stringify(counters) -} + yield "custom.js "; + yield JSON.stringify(counters); +}; diff --git a/test/fixtures/test-runner/custom_reporters/custom.mjs b/test/fixtures/test-runner/custom_reporters/custom.mjs index 17e6869..b202d77 100644 --- a/test/fixtures/test-runner/custom_reporters/custom.mjs +++ b/test/fixtures/test-runner/custom_reporters/custom.mjs @@ -1,9 +1,8 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/test/fixtures/test-runner/custom_reporters/custom.mjs -export default async function * customReporter (source) { - const counters = {} +export default async function * customReporter(source) { + const counters = {}; for await (const event of source) { - counters[event.type] = (counters[event.type] ?? 0) + 1 + counters[event.type] = (counters[event.type] ?? 0) + 1; } - yield 'custom.mjs ' - yield JSON.stringify(counters) + yield "custom.mjs "; + yield JSON.stringify(counters); } diff --git a/test/fixtures/test-runner/custom_reporters/throwing-async.js b/test/fixtures/test-runner/custom_reporters/throwing-async.js new file mode 100644 index 0000000..b24a632 --- /dev/null +++ b/test/fixtures/test-runner/custom_reporters/throwing-async.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = async function * customReporter() { + yield 'Going to throw an error\n'; + setImmediate(() => { + throw new Error('Reporting error'); + }); +}; diff --git a/test/fixtures/test-runner/custom_reporters/throwing.js b/test/fixtures/test-runner/custom_reporters/throwing.js new file mode 100644 index 0000000..8d04901 --- /dev/null +++ b/test/fixtures/test-runner/custom_reporters/throwing.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = async function * customReporter() { + yield 'Going to throw an error\n'; + throw new Error('Reporting error'); +}; diff --git a/test/fixtures/test-runner/cwd/test.cjs b/test/fixtures/test-runner/cwd/test.cjs new file mode 100644 index 0000000..9a3b3b8 --- /dev/null +++ b/test/fixtures/test-runner/cwd/test.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('this should pass'); diff --git a/test/fixtures/test-runner/default-behavior/index.test.js b/test/fixtures/test-runner/default-behavior/index.test.js new file mode 100644 index 0000000..9a3b3b8 --- /dev/null +++ b/test/fixtures/test-runner/default-behavior/index.test.js @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('this should pass'); diff --git a/test/fixtures/test-runner/default-behavior/random.test.mjs b/test/fixtures/test-runner/default-behavior/random.test.mjs new file mode 100644 index 0000000..eccf482 --- /dev/null +++ b/test/fixtures/test-runner/default-behavior/random.test.mjs @@ -0,0 +1,5 @@ +import test from '#node:test'; + +test('this should fail', () => { + throw new Error('this is a failing test'); +}); diff --git a/test/fixtures/test-runner/default-behavior/subdir/subdir_test.js b/test/fixtures/test-runner/default-behavior/subdir/subdir_test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/test-runner/default-behavior/test/random.cjs b/test/fixtures/test-runner/default-behavior/test/random.cjs new file mode 100644 index 0000000..9a3b3b8 --- /dev/null +++ b/test/fixtures/test-runner/default-behavior/test/random.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('this should pass'); diff --git a/test/fixtures/test-runner/default-behavior/test/skip_by_name.cjs b/test/fixtures/test-runner/default-behavior/test/skip_by_name.cjs new file mode 100644 index 0000000..8c0721f --- /dev/null +++ b/test/fixtures/test-runner/default-behavior/test/skip_by_name.cjs @@ -0,0 +1,5 @@ +'use strict'; +const test = require('#node:test'); + +test('this should be skipped'); +test('this should be executed'); diff --git a/test/fixtures/test-runner/extraneous_set_immediate_async.mjs b/test/fixtures/test-runner/extraneous_set_immediate_async.mjs index 241e262..fc866fb 100644 --- a/test/fixtures/test-runner/extraneous_set_immediate_async.mjs +++ b/test/fixtures/test-runner/extraneous_set_immediate_async.mjs @@ -1,6 +1,5 @@ -// https://github.com/nodejs/node/blob/06603c44a5b0e92b1a3591ace467ce9770bf9658/test/fixtures/test-runner/extraneous_set_immediate_async.mjs -import test from '#node:test' +import test from '#node:test'; test('extraneous async activity test', () => { - setImmediate(() => { throw new Error() }) -}) + setImmediate(() => { throw new Error(); }); +}); diff --git a/test/fixtures/test-runner/extraneous_set_timeout_async.mjs b/test/fixtures/test-runner/extraneous_set_timeout_async.mjs index 2b5ee72..b34a055 100644 --- a/test/fixtures/test-runner/extraneous_set_timeout_async.mjs +++ b/test/fixtures/test-runner/extraneous_set_timeout_async.mjs @@ -1,6 +1,5 @@ -// https://github.com/nodejs/node/blob/06603c44a5b0e92b1a3591ace467ce9770bf9658/test/fixtures/test-runner/extraneous_set_timeout_async.mjs -import test from '#node:test' +import test from '#node:test'; test('extraneous async activity test', () => { - setTimeout(() => { throw new Error() }, 100) -}) + setTimeout(() => { throw new Error(); }, 100); +}); diff --git a/test/fixtures/test-runner/index.js b/test/fixtures/test-runner/index.js index 6e00ebd..fcf4b4d 100644 --- a/test/fixtures/test-runner/index.js +++ b/test/fixtures/test-runner/index.js @@ -1,3 +1,2 @@ -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/fixtures/test-runner/index.js -'use strict' -throw new Error('thrown from index.js') +'use strict'; +throw new Error('thrown from index.js'); diff --git a/test/fixtures/test-runner/index.test.js b/test/fixtures/test-runner/index.test.js deleted file mode 100644 index 7d2d1f4..0000000 --- a/test/fixtures/test-runner/index.test.js +++ /dev/null @@ -1,5 +0,0 @@ -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/fixtures/test-runner/index.test.js -'use strict' -const test = require('#node:test') - -test('this should pass') diff --git a/test/fixtures/test-runner/invalid-tap.js b/test/fixtures/test-runner/invalid-tap.js index 1073bd8..d43fb8a 100644 --- a/test/fixtures/test-runner/invalid-tap.js +++ b/test/fixtures/test-runner/invalid-tap.js @@ -1,3 +1 @@ -// https://github.com/nodejs/node/blob/f8ce9117b19702487eb600493d941f7876e00e01/test/fixtures/test-runner/invalid-tap.js - -console.log('invalid tap output') +console.log('invalid tap output'); diff --git a/test/fixtures/test-runner/issue-54726/latest.js b/test/fixtures/test-runner/issue-54726/latest.js new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/test-runner/issue-54726/non-matching.js b/test/fixtures/test-runner/issue-54726/non-matching.js new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/test-runner/matching-patterns/index-test.cjs b/test/fixtures/test-runner/matching-patterns/index-test.cjs new file mode 100644 index 0000000..9a3b3b8 --- /dev/null +++ b/test/fixtures/test-runner/matching-patterns/index-test.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('this should pass'); diff --git a/test/fixtures/test-runner/matching-patterns/index-test.js b/test/fixtures/test-runner/matching-patterns/index-test.js new file mode 100644 index 0000000..9a3b3b8 --- /dev/null +++ b/test/fixtures/test-runner/matching-patterns/index-test.js @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('this should pass'); diff --git a/test/fixtures/test-runner/matching-patterns/index-test.mjs b/test/fixtures/test-runner/matching-patterns/index-test.mjs new file mode 100644 index 0000000..c97c00e --- /dev/null +++ b/test/fixtures/test-runner/matching-patterns/index-test.mjs @@ -0,0 +1,4 @@ +'use strict'; +import test from '#node:test'; + +test('this should pass'); diff --git a/test/fixtures/test-runner/matching-patterns/typescript-test.cts b/test/fixtures/test-runner/matching-patterns/typescript-test.cts new file mode 100644 index 0000000..fcce553 --- /dev/null +++ b/test/fixtures/test-runner/matching-patterns/typescript-test.cts @@ -0,0 +1,4 @@ +const test = require('#node:test'); + +// 'as string' ensures that type stripping actually occurs +test('this should pass' as string); diff --git a/test/fixtures/test-runner/matching-patterns/typescript-test.mts b/test/fixtures/test-runner/matching-patterns/typescript-test.mts new file mode 100644 index 0000000..dce3e70 --- /dev/null +++ b/test/fixtures/test-runner/matching-patterns/typescript-test.mts @@ -0,0 +1,4 @@ +import { test } from '#node:test'; + +// 'as string' ensures that type stripping actually occurs +test('this should pass' as string); diff --git a/test/fixtures/test-runner/matching-patterns/typescript-test.ts b/test/fixtures/test-runner/matching-patterns/typescript-test.ts new file mode 100644 index 0000000..fcce553 --- /dev/null +++ b/test/fixtures/test-runner/matching-patterns/typescript-test.ts @@ -0,0 +1,4 @@ +const test = require('#node:test'); + +// 'as string' ensures that type stripping actually occurs +test('this should pass' as string); diff --git a/test/fixtures/test-runner/mock-nm.js b/test/fixtures/test-runner/mock-nm.js new file mode 100644 index 0000000..a182fe7 --- /dev/null +++ b/test/fixtures/test-runner/mock-nm.js @@ -0,0 +1,21 @@ +'use strict'; +const assert = require('node:assert'); +const { test } = require('#node:test'); +const fixture = 'reporter-cjs'; + +test('mock node_modules dependency', async (t) => { + const mock = t.mock.module(fixture, { + namedExports: { fn() { return 42; } }, + }); + let cjsImpl = require(fixture); + let esmImpl = await import(fixture); + + assert.strictEqual(cjsImpl.fn(), 42); + assert.strictEqual(esmImpl.fn(), 42); + + mock.restore(); + cjsImpl = require(fixture); + esmImpl = await import(fixture); + assert.strictEqual(cjsImpl.fn, undefined); + assert.strictEqual(esmImpl.fn, undefined); +}); diff --git a/test/fixtures/test-runner/nested.js b/test/fixtures/test-runner/nested.js index 3d01534..424e278 100644 --- a/test/fixtures/test-runner/nested.js +++ b/test/fixtures/test-runner/nested.js @@ -1,23 +1,24 @@ -// https://github.com/nodejs/node/blob/f8ce9117b19702487eb600493d941f7876e00e01/test/fixtures/test-runner/nested.js -'use strict' -const test = require('#node:test') +'use strict'; +const test = require('#node:test'); + + test('level 0a', { concurrency: 4 }, async (t) => { t.test('level 1a', async (t) => { - }) + }); t.test('level 1b', async (t) => { - throw new Error('level 1b error') - }) + throw new Error('level 1b error'); + }); t.test('level 1c', { skip: 'aaa' }, async (t) => { - }) + }); t.test('level 1d', async (t) => { - t.diagnostic('level 1d diagnostic') - }) -}) + t.diagnostic('level 1d diagnostic'); + }); +}); test('level 0b', async (t) => { - throw new Error('level 0b error') -}) + throw new Error('level 0b error'); +}); \ No newline at end of file diff --git a/test/fixtures/test-runner/never_ending_async.js b/test/fixtures/test-runner/never_ending_async.js index a47e8b9..c06c7b7 100644 --- a/test/fixtures/test-runner/never_ending_async.js +++ b/test/fixtures/test-runner/never_ending_async.js @@ -1,7 +1,6 @@ -// https://github.com/nodejs/node/blob/26e27424ad91c60a44d3d4c58b62a39b555ba75d/test/fixtures/test-runner/never_ending_async.js -const test = require('#node:test') -const { setTimeout } = require('#timers/promises') +const test = require('#node:test'); +const { setTimeout } = require('timers/promises'); // We are using a very large timeout value to ensure that the parent process // will have time to send a SIGINT signal to cancel the test. -test('never ending test', () => setTimeout(100_000_000)) +test('never ending test', () => setTimeout(100_000_000)); diff --git a/test/fixtures/test-runner/never_ending_sync.js b/test/fixtures/test-runner/never_ending_sync.js index 2bc80a5..d217819 100644 --- a/test/fixtures/test-runner/never_ending_sync.js +++ b/test/fixtures/test-runner/never_ending_sync.js @@ -1,6 +1,5 @@ -// https://github.com/nodejs/node/blob/26e27424ad91c60a44d3d4c58b62a39b555ba75d/test/fixtures/test-runner/never_ending_sync.js -const test = require('#node:test') +const test = require('#node:test'); test('never ending test', () => { while (true); -}) +}); diff --git a/test/fixtures/test-runner/no-isolation/global-hooks.js b/test/fixtures/test-runner/no-isolation/global-hooks.js new file mode 100644 index 0000000..ad34512 --- /dev/null +++ b/test/fixtures/test-runner/no-isolation/global-hooks.js @@ -0,0 +1,6 @@ +import('#node:test').then((test) => { + test.before(() => console.log('before(): global')); + test.beforeEach(() => console.log('beforeEach(): global')); + test.after(() => console.log('after(): global')); + test.afterEach(() => console.log('afterEach(): global')); +}); diff --git a/test/fixtures/test-runner/no-isolation/one.test.js b/test/fixtures/test-runner/no-isolation/one.test.js new file mode 100644 index 0000000..fe913a2 --- /dev/null +++ b/test/fixtures/test-runner/no-isolation/one.test.js @@ -0,0 +1,37 @@ +'use strict'; +const { before, beforeEach, after, afterEach, test, suite } = require('#node:test'); + +globalThis.GLOBAL_ORDER = []; + +function record(data) { + globalThis.GLOBAL_ORDER.push(data); + console.log(data); +} + +before(function() { + record(`before one: ${this.name}`); +}); + +beforeEach(function() { + record(`beforeEach one: ${this.name}`); +}); + +after(function() { + record(`after one: ${this.name}`); +}); + +afterEach(function() { + record(`afterEach one: ${this.name}`); +}); + +suite('suite one', function() { + record(this.name); + + test('suite one - test', { only: true }, function() { + record(this.name); + }); +}); + +test('test one', function() { + record(this.name); +}); diff --git a/test/fixtures/test-runner/no-isolation/two.test.js b/test/fixtures/test-runner/no-isolation/two.test.js new file mode 100644 index 0000000..52baad4 --- /dev/null +++ b/test/fixtures/test-runner/no-isolation/two.test.js @@ -0,0 +1,35 @@ +'use strict'; +const { before, beforeEach, after, afterEach, test, suite } = require('#node:test'); + +function record(data) { + globalThis.GLOBAL_ORDER.push(data); + console.log(data); +} + +before(function() { + record(`before two: ${this.name}`); +}); + +beforeEach(function() { + record(`beforeEach two: ${this.name}`); +}); + +after(function() { + record(`after two: ${this.name}`); +}); + +afterEach(function() { + record(`afterEach two: ${this.name}`); +}); + +suite('suite two', function() { + record(this.name); + + before(function() { + record(`before suite two: ${this.name}`); + }); + + test('suite two - test', { only: true }, function() { + record(this.name); + }); +}); diff --git a/test/fixtures/test-runner/node_modules/test-nm.js b/test/fixtures/test-runner/node_modules/test-nm.js deleted file mode 100644 index 5b91813..0000000 --- a/test/fixtures/test-runner/node_modules/test-nm.js +++ /dev/null @@ -1,3 +0,0 @@ -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/fixtures/test-runner/node_modules/test-nm.js -'use strict'; -throw new Error('thrown from node_modules'); diff --git a/test/fixtures/test-runner/output/abort-runs-after-hook.js b/test/fixtures/test-runner/output/abort-runs-after-hook.js new file mode 100644 index 0000000..c31250c --- /dev/null +++ b/test/fixtures/test-runner/output/abort-runs-after-hook.js @@ -0,0 +1,14 @@ +'use strict'; +const { test } = require('#node:test'); + +test('test that aborts', (t, done) => { + t.after(() => { + // This should still run. + console.log('AFTER'); + }); + + setImmediate(() => { + // This creates an uncaughtException, which aborts the test. + throw new Error('boom'); + }); +}); diff --git a/test/fixtures/test-runner/output/abort-runs-after-hook.snapshot b/test/fixtures/test-runner/output/abort-runs-after-hook.snapshot new file mode 100644 index 0000000..a99f9a1 --- /dev/null +++ b/test/fixtures/test-runner/output/abort-runs-after-hook.snapshot @@ -0,0 +1,25 @@ +TAP version 13 +AFTER +# Subtest: test that aborts +not ok 1 - test that aborts + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/abort-runs-after-hook.js:(LINE):1' + failureType: 'uncaughtException' + message: 'boom' + toString: [Function (anonymous)] + error: 'boom' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + ... +1..1 +# tests 1 +# suites 0 +# pass 0 +# fail 1 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/message/test_runner_abort.js b/test/fixtures/test-runner/output/abort.js similarity index 59% rename from test/message/test_runner_abort.js rename to test/fixtures/test-runner/output/abort.js index 5e1c768..a00a716 100644 --- a/test/message/test_runner_abort.js +++ b/test/fixtures/test-runner/output/abort.js @@ -1,8 +1,6 @@ -// https://github.com/nodejs/node/blob/389b7e138e89a339fabe4ad628bf09cd9748f957/test/message/test_runner_abort.js -// Flags: --no-warnings -'use strict' -require('../common') -const test = require('#node:test') +'use strict'; +require('../../../common'); +const test = require('#node:test'); test('promise timeout signal', { signal: AbortSignal.timeout(1) }, async (t) => { await Promise.all([ @@ -15,34 +13,34 @@ test('promise timeout signal', { signal: AbortSignal.timeout(1) }, async (t) => t.test('not ok 3', { signal: t.signal }, () => new Promise(() => {})), t.test('not ok 4', { signal: t.signal }, (t, done) => {}), t.test('not ok 5', { signal: t.signal }, (t, done) => { - t.signal.addEventListener('abort', done) - }) - ]) -}) + t.signal.addEventListener('abort', done); + }), + ]); +}); test('promise abort signal', { signal: AbortSignal.abort() }, async (t) => { - await t.test('should not appear', () => {}) -}) + await t.test('should not appear', () => {}); +}); test('callback timeout signal', { signal: AbortSignal.timeout(1) }, (t, done) => { - t.test('ok 1', async () => {}) - t.test('ok 2', () => {}) - t.test('ok 3', { signal: t.signal }, async () => {}) - t.test('ok 4', { signal: t.signal }, () => {}) - t.test('not ok 1', () => new Promise(() => {})) - t.test('not ok 2', (t, done) => {}) - t.test('not ok 3', { signal: t.signal }, () => new Promise(() => {})) - t.test('not ok 4', { signal: t.signal }, (t, done) => {}) + t.test('ok 1', async () => {}); + t.test('ok 2', () => {}); + t.test('ok 3', { signal: t.signal }, async () => {}); + t.test('ok 4', { signal: t.signal }, () => {}); + t.test('not ok 1', () => new Promise(() => {})); + t.test('not ok 2', (t, done) => {}); + t.test('not ok 3', { signal: t.signal }, () => new Promise(() => {})); + t.test('not ok 4', { signal: t.signal }, (t, done) => {}); t.test('not ok 5', { signal: t.signal }, (t, done) => { - t.signal.addEventListener('abort', done) - }) -}) + t.signal.addEventListener('abort', done); + }); +}); test('callback abort signal', { signal: AbortSignal.abort() }, (t, done) => { - t.test('should not appear', done) -}) + t.test('should not appear', done); +}); // AbortSignal.timeout(1) doesn't prevent process from closing // thus we have to keep the process open to prevent cancelation // of the entire test tree -setTimeout(() => {}, 1000) +setTimeout(() => {}, 1000); diff --git a/test/message/test_runner_abort.out b/test/fixtures/test-runner/output/abort.snapshot similarity index 62% rename from test/message/test_runner_abort.out rename to test/fixtures/test-runner/output/abort.snapshot index 26f89a2..0b0f3e5 100644 --- a/test/message/test_runner_abort.out +++ b/test/fixtures/test-runner/output/abort.snapshot @@ -24,24 +24,53 @@ TAP version 13 not ok 5 - not ok 1 --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):7' failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] error: 'test did not finish before its parent and was cancelled' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * ... # Subtest: not ok 2 not ok 6 - not ok 2 --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):7' failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] error: 'test did not finish before its parent and was cancelled' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * ... # Subtest: not ok 3 not ok 7 - not ok 3 --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):7' + failureType: 'testAborted' error: 'This operation was aborted' code: 20 + name: 'AbortError' stack: |- * * @@ -58,8 +87,11 @@ TAP version 13 not ok 8 - not ok 4 --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):7' + failureType: 'testAborted' error: 'This operation was aborted' code: 20 + name: 'AbortError' stack: |- * * @@ -76,8 +108,11 @@ TAP version 13 not ok 9 - not ok 5 --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):7' + failureType: 'testAborted' error: 'This operation was aborted' code: 20 + name: 'AbortError' stack: |- * * @@ -94,8 +129,11 @@ TAP version 13 not ok 1 - promise timeout signal --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):1' + failureType: 'testAborted' error: 'The operation was aborted due to timeout' code: 23 + name: 'TimeoutError' stack: |- * * @@ -106,8 +144,11 @@ not ok 1 - promise timeout signal not ok 2 - promise abort signal --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):1' + failureType: 'testAborted' error: 'This operation was aborted' code: 20 + name: 'AbortError' stack: |- * * @@ -144,24 +185,47 @@ not ok 2 - promise abort signal not ok 5 - not ok 1 --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):5' failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] error: 'test did not finish before its parent and was cancelled' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * ... # Subtest: not ok 2 not ok 6 - not ok 2 --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):5' failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] error: 'test did not finish before its parent and was cancelled' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * ... # Subtest: not ok 3 not ok 7 - not ok 3 --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):5' + failureType: 'testAborted' error: 'This operation was aborted' code: 20 + name: 'AbortError' stack: |- * * @@ -178,8 +242,11 @@ not ok 2 - promise abort signal not ok 8 - not ok 4 --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):5' + failureType: 'testAborted' error: 'This operation was aborted' code: 20 + name: 'AbortError' stack: |- * * @@ -196,8 +263,11 @@ not ok 2 - promise abort signal not ok 9 - not ok 5 --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):5' + failureType: 'testAborted' error: 'This operation was aborted' code: 20 + name: 'AbortError' stack: |- * * @@ -214,8 +284,11 @@ not ok 2 - promise abort signal not ok 3 - callback timeout signal --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):1' + failureType: 'testAborted' error: 'The operation was aborted due to timeout' code: 23 + name: 'TimeoutError' stack: |- * * @@ -226,8 +299,11 @@ not ok 3 - callback timeout signal not ok 4 - callback abort signal --- duration_ms: * + location: '/test/fixtures/test-runner/output/abort.js:(LINE):1' + failureType: 'testAborted' error: 'This operation was aborted' code: 20 + name: 'AbortError' stack: |- * * @@ -240,10 +316,11 @@ not ok 4 - callback abort signal * ... 1..4 -# tests 4 -# pass 0 +# tests 22 +# suites 0 +# pass 8 # fail 0 -# cancelled 4 +# cancelled 14 # skipped 0 # todo 0 # duration_ms * diff --git a/test/fixtures/test-runner/output/abort_hooks.js b/test/fixtures/test-runner/output/abort_hooks.js new file mode 100644 index 0000000..c88736c --- /dev/null +++ b/test/fixtures/test-runner/output/abort_hooks.js @@ -0,0 +1,62 @@ +'use strict'; +const { before, beforeEach, describe, it, after, afterEach } = require('#node:test'); + +describe('1 before describe', () => { + const ac = new AbortController(); + before(() => { + console.log('before'); + ac.abort() + }, {signal: ac.signal}); + + it('test 1', () => { + console.log('1.1'); + }); + it('test 2', () => { + console.log('1.2'); + }); +}); + +describe('2 after describe', () => { + const ac = new AbortController(); + after(() => { + console.log('after'); + ac.abort() + }, {signal: ac.signal}); + + it('test 1', () => { + console.log('2.1'); + }); + it('test 2', () => { + console.log('2.2'); + }); +}); + +describe('3 beforeEach describe', () => { + const ac = new AbortController(); + beforeEach(() => { + console.log('beforeEach'); + ac.abort() + }, {signal: ac.signal}); + + it('test 1', () => { + console.log('3.1'); + }); + it('test 2', () => { + console.log('3.2'); + }); +}); + +describe('4 afterEach describe', () => { + const ac = new AbortController(); + afterEach(() => { + console.log('afterEach'); + ac.abort() + }, {signal: ac.signal}); + + it('test 1', () => { + console.log('4.1'); + }); + it('test 2', () => { + console.log('4.2'); + }); +}); diff --git a/test/fixtures/test-runner/output/abort_hooks.snapshot b/test/fixtures/test-runner/output/abort_hooks.snapshot new file mode 100644 index 0000000..f243475 --- /dev/null +++ b/test/fixtures/test-runner/output/abort_hooks.snapshot @@ -0,0 +1,255 @@ +before +2.1 +2.2 +after +beforeEach +4.1 +afterEach +4.2 +TAP version 13 +# Subtest: 1 before describe + # Subtest: test 1 + not ok 1 - test 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/abort_hooks.js:(LINE):3' + failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + # Subtest: test 2 + not ok 2 - test 2 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/abort_hooks.js:(LINE):3' + failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + 1..2 +not ok 1 - 1 before describe + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/abort_hooks.js:(LINE):1' + failureType: 'hookFailed' + message: 'failed running before hook' + toString: [Function (anonymous)] + error: 'This operation was aborted' + code: 20 + name: 'AbortError' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... +# Subtest: 2 after describe + # Subtest: test 1 + ok 1 - test 1 + --- + duration_ms: * + ... + # Subtest: test 2 + ok 2 - test 2 + --- + duration_ms: * + ... + 1..2 +not ok 2 - 2 after describe + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/abort_hooks.js:(LINE):1' + failureType: 'hookFailed' + message: 'failed running after hook' + toString: [Function (anonymous)] + error: 'This operation was aborted' + code: 20 + name: 'AbortError' + stack: |- + * + * + * + * + * + * + * + * + * + * + * + ... +# Subtest: 3 beforeEach describe + # Subtest: test 1 + not ok 1 - test 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/abort_hooks.js:(LINE):3' + failureType: 'hookFailed' + message: 'failed running beforeEach hook' + toString: [Function (anonymous)] + error: 'This operation was aborted' + code: 20 + name: 'AbortError' + stack: |- + * + * + * + * + * + * + * + * + * + async Promise.all (index 0) + * + * + ... + # Subtest: test 2 + not ok 2 - test 2 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/abort_hooks.js:(LINE):3' + failureType: 'hookFailed' + message: 'failed running beforeEach hook' + toString: [Function (anonymous)] + error: 'This operation was aborted' + code: 20 + name: 'AbortError' + stack: |- + * + * + * + * + * + * + * + * + * + async Promise.all (index 0) + * + * + ... + 1..2 +not ok 3 - 3 beforeEach describe + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/abort_hooks.js:(LINE):1' + failureType: 'subtestsFailed' + message: '2 subtests failed' + toString: [Function (anonymous)] + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: 4 afterEach describe + # Subtest: test 1 + not ok 1 - test 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/abort_hooks.js:(LINE):3' + failureType: 'hookFailed' + message: 'failed running afterEach hook' + toString: [Function (anonymous)] + error: 'This operation was aborted' + code: 20 + name: 'AbortError' + stack: |- + * + * + * + * + * + * + * + * + * + * + async Promise.all (index 0) + * + * + ... + # Subtest: test 2 + not ok 2 - test 2 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/abort_hooks.js:(LINE):3' + failureType: 'hookFailed' + message: 'failed running afterEach hook' + toString: [Function (anonymous)] + error: 'This operation was aborted' + code: 20 + name: 'AbortError' + stack: |- + * + * + * + * + * + * + * + * + * + * + async Promise.all (index 0) + * + * + ... + 1..2 +not ok 4 - 4 afterEach describe + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/abort_hooks.js:(LINE):1' + failureType: 'subtestsFailed' + message: '2 subtests failed' + toString: [Function (anonymous)] + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +1..4 +# tests 8 +# suites 4 +# pass 2 +# fail 4 +# cancelled 2 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/abort_suite.js b/test/fixtures/test-runner/output/abort_suite.js new file mode 100644 index 0000000..5aea9aa --- /dev/null +++ b/test/fixtures/test-runner/output/abort_suite.js @@ -0,0 +1,26 @@ +'use strict'; +require('../../../common'); +const { describe, it } = require('#node:test'); + +describe('describe timeout signal', { signal: AbortSignal.timeout(1) }, (t) => { + it('ok 1', async () => {}); + it('ok 2', () => {}); + it('ok 3', { signal: t.signal }, async () => {}); + it('ok 4', { signal: t.signal }, () => {}); + it('not ok 1', () => new Promise(() => {})); + it('not ok 2', (done) => {}); + it('not ok 3', { signal: t.signal }, () => new Promise(() => {})); + it('not ok 4', { signal: t.signal }, (done) => {}); + it('not ok 5', { signal: t.signal }, function(done) { + this.signal.addEventListener('abort', done); + }); +}); + +describe('describe abort signal', { signal: AbortSignal.abort() }, () => { + it('should not appear', () => {}); +}); + +// AbortSignal.timeout(1) doesn't prevent process from closing +// thus we have to keep the process open to prevent cancelation +// of the entire test tree +setTimeout(() => {}, 1000); diff --git a/test/fixtures/test-runner/output/abort_suite.snapshot b/test/fixtures/test-runner/output/abort_suite.snapshot new file mode 100644 index 0000000..b9e93aa --- /dev/null +++ b/test/fixtures/test-runner/output/abort_suite.snapshot @@ -0,0 +1,167 @@ +TAP version 13 +# Subtest: describe timeout signal + # Subtest: ok 1 + ok 1 - ok 1 + --- + duration_ms: * + ... + # Subtest: ok 2 + ok 2 - ok 2 + --- + duration_ms: * + ... + # Subtest: ok 3 + ok 3 - ok 3 + --- + duration_ms: * + ... + # Subtest: ok 4 + ok 4 - ok 4 + --- + duration_ms: * + ... + # Subtest: not ok 1 + not ok 5 - not ok 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/abort_suite.js:(LINE):3' + failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + # Subtest: not ok 2 + not ok 6 - not ok 2 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/abort_suite.js:(LINE):3' + failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + # Subtest: not ok 3 + not ok 7 - not ok 3 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/abort_suite.js:(LINE):3' + failureType: 'testAborted' + error: 'This operation was aborted' + code: 20 + name: 'AbortError' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... + # Subtest: not ok 4 + not ok 8 - not ok 4 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/abort_suite.js:(LINE):3' + failureType: 'testAborted' + error: 'This operation was aborted' + code: 20 + name: 'AbortError' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... + # Subtest: not ok 5 + not ok 9 - not ok 5 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/abort_suite.js:(LINE):3' + failureType: 'testAborted' + error: 'This operation was aborted' + code: 20 + name: 'AbortError' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... + 1..9 +not ok 1 - describe timeout signal + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/abort_suite.js:(LINE):1' + failureType: 'testAborted' + error: 'The operation was aborted due to timeout' + code: 23 + name: 'TimeoutError' + stack: |- + * + * + * + * + ... +# Subtest: describe abort signal +not ok 2 - describe abort signal + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/abort_suite.js:(LINE):1' + failureType: 'testAborted' + error: 'This operation was aborted' + code: 20 + name: 'AbortError' + stack: |- + * + * + * + * + * + * + * + * + * + ... +1..2 +# tests 9 +# suites 2 +# pass 4 +# fail 0 +# cancelled 5 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/arbitrary-output-colored-1.js b/test/fixtures/test-runner/output/arbitrary-output-colored-1.js new file mode 100644 index 0000000..4648df1 --- /dev/null +++ b/test/fixtures/test-runner/output/arbitrary-output-colored-1.js @@ -0,0 +1,7 @@ +'use strict'; + +const test = require('#node:test'); +console.log({ foo: 'bar' }); +test('passing test', () => { + console.log(1); +}); diff --git a/test/fixtures/test-runner/output/arbitrary-output-colored.js b/test/fixtures/test-runner/output/arbitrary-output-colored.js new file mode 100644 index 0000000..af23e67 --- /dev/null +++ b/test/fixtures/test-runner/output/arbitrary-output-colored.js @@ -0,0 +1,12 @@ +'use strict'; +const common = require('../../../common'); +const { once } = require('node:events'); +const { spawn } = require('node:child_process'); +const fixtures = require('../../../common/fixtures'); + +(async function run() { + const test = fixtures.path('test-runner/output/arbitrary-output-colored-1.js'); + const reset = fixtures.path('test-runner/output/reset-color-depth.js'); + await once(spawn(process.execPath, ['-r', reset, '--test', test], { stdio: 'inherit' }), 'exit'); + await once(spawn(process.execPath, ['-r', reset, '--test', '--test-reporter', 'tap', test], { stdio: 'inherit' }), 'exit'); +})().then(common.mustCall()); diff --git a/test/fixtures/test-runner/output/arbitrary-output-colored.snapshot b/test/fixtures/test-runner/output/arbitrary-output-colored.snapshot new file mode 100644 index 0000000..34b5c04 --- /dev/null +++ b/test/fixtures/test-runner/output/arbitrary-output-colored.snapshot @@ -0,0 +1,28 @@ +{ foo: [32m'bar'[39m } +[33m1[39m +[32m✔ passing test [90m(*ms)[39m[39m +[34mℹ tests 1[39m +[34mℹ suites 0[39m +[34mℹ pass 1[39m +[34mℹ fail 0[39m +[34mℹ cancelled 0[39m +[34mℹ skipped 0[39m +[34mℹ todo 0[39m +[34mℹ duration_ms *[39m +TAP version 13 +# { foo: [32m'bar'[39m } +# [33m1[39m +# Subtest: passing test +ok 1 - passing test + --- + duration_ms: * + ... +1..1 +# tests 1 +# suites 0 +# pass 1 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/arbitrary-output.js b/test/fixtures/test-runner/output/arbitrary-output.js new file mode 100644 index 0000000..9fe3ee8 --- /dev/null +++ b/test/fixtures/test-runner/output/arbitrary-output.js @@ -0,0 +1,20 @@ +// Flags: --test --expose-internals +'use strict'; + +const v8_reporter = require('#internal/test_runner/reporter/v8-serializer'); +const { Buffer } = require('buffer'); + + +(async function () { + const reported = v8_reporter([ + { type: "test:pass", data: { name: "test", nesting: 0, file: __filename, testNumber: 1, details: { duration_ms: 0 } } } + ]); + + for await (const chunk of reported) { + process.stdout.write(chunk); + process.stdout.write(Buffer.concat([Buffer.from("arbitrary - pre"), chunk])); + process.stdout.write(Buffer.from("arbitrary - mid")); + process.stdout.write(Buffer.concat([chunk, Buffer.from("arbitrary - post")])); + } +})(); + diff --git a/test/fixtures/test-runner/output/arbitrary-output.snapshot b/test/fixtures/test-runner/output/arbitrary-output.snapshot new file mode 100644 index 0000000000000000000000000000000000000000..7aa43dcc071352d4b435d0b372455f2eb851ee82 GIT binary patch literal 550 zcmdUpO$viB5QS5*NZW;1kl9=Jy@8uvp+qv46!XJm0u|3D9?2^-w$M8WkAe3M559Go zOCiL%%qgXVoJWsvVhnuqG6cw#WSO{?2GcijC~UggK3f5KuLp#_*?80IH%G$oZ8b$} zks24-M{Xo_nn}`F|G@PX;JmHw(R4^{28BYFdI%0KDw1UF?8AgsOo!<>W&eFuSDtW_ O-J7=2YLD991^xi5CdbJD literal 0 HcmV?d00001 diff --git a/test/fixtures/test-runner/output/async-test-scheduling.mjs b/test/fixtures/test-runner/output/async-test-scheduling.mjs new file mode 100644 index 0000000..1e9ef34 --- /dev/null +++ b/test/fixtures/test-runner/output/async-test-scheduling.mjs @@ -0,0 +1,14 @@ +import * as common from '../../../common/index.mjs'; +import pkg from '#node:test'; +const { describe, test } = pkg; +import { setTimeout } from 'node:timers/promises'; + +test('test', common.mustCall()); +describe('suite', common.mustCall(async () => { + test('test', common.mustCall()); + await setTimeout(10); + test('scheduled async', common.mustCall()); +})); + +await setTimeout(10); +test('scheduled async', common.mustCall()); diff --git a/test/fixtures/test-runner/output/async-test-scheduling.snapshot b/test/fixtures/test-runner/output/async-test-scheduling.snapshot new file mode 100644 index 0000000..64c3004 --- /dev/null +++ b/test/fixtures/test-runner/output/async-test-scheduling.snapshot @@ -0,0 +1,37 @@ +TAP version 13 +# Subtest: test +ok 1 - test + --- + duration_ms: * + ... +# Subtest: suite + # Subtest: test + ok 1 - test + --- + duration_ms: * + ... + # Subtest: scheduled async + ok 2 - scheduled async + --- + duration_ms: * + ... + 1..2 +ok 2 - suite + --- + duration_ms: * + type: 'suite' + ... +# Subtest: scheduled async +ok 3 - scheduled async + --- + duration_ms: * + ... +1..3 +# tests 4 +# suites 1 +# pass 4 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/before-and-after-each-too-many-listeners.js b/test/fixtures/test-runner/output/before-and-after-each-too-many-listeners.js new file mode 100644 index 0000000..7385709 --- /dev/null +++ b/test/fixtures/test-runner/output/before-and-after-each-too-many-listeners.js @@ -0,0 +1,8 @@ +'use strict'; +const { beforeEach, afterEach, test} = require("node:test"); +beforeEach(() => {}); +afterEach(() => {}); + +for (let i = 1; i <= 11; ++i) { + test(`${i}`, () => {}); +} diff --git a/test/fixtures/test-runner/output/before-and-after-each-too-many-listeners.snapshot b/test/fixtures/test-runner/output/before-and-after-each-too-many-listeners.snapshot new file mode 100644 index 0000000..4300e21 --- /dev/null +++ b/test/fixtures/test-runner/output/before-and-after-each-too-many-listeners.snapshot @@ -0,0 +1,65 @@ +TAP version 13 +# Subtest: 1 +ok 1 - 1 + --- + duration_ms: * + ... +# Subtest: 2 +ok 2 - 2 + --- + duration_ms: * + ... +# Subtest: 3 +ok 3 - 3 + --- + duration_ms: * + ... +# Subtest: 4 +ok 4 - 4 + --- + duration_ms: * + ... +# Subtest: 5 +ok 5 - 5 + --- + duration_ms: * + ... +# Subtest: 6 +ok 6 - 6 + --- + duration_ms: * + ... +# Subtest: 7 +ok 7 - 7 + --- + duration_ms: * + ... +# Subtest: 8 +ok 8 - 8 + --- + duration_ms: * + ... +# Subtest: 9 +ok 9 - 9 + --- + duration_ms: * + ... +# Subtest: 10 +ok 10 - 10 + --- + duration_ms: * + ... +# Subtest: 11 +ok 11 - 11 + --- + duration_ms: * + ... +1..11 +# tests 11 +# suites 0 +# pass 11 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/before-and-after-each-with-timeout-too-many-listeners.js b/test/fixtures/test-runner/output/before-and-after-each-with-timeout-too-many-listeners.js new file mode 100644 index 0000000..87d645d --- /dev/null +++ b/test/fixtures/test-runner/output/before-and-after-each-with-timeout-too-many-listeners.js @@ -0,0 +1,8 @@ +'use strict'; +const { beforeEach, afterEach, test} = require("node:test"); +beforeEach(() => {}, {timeout: 10000}); +afterEach(() => {}, {timeout: 10000}); + +for (let i = 1; i <= 11; ++i) { + test(`${i}`, () => {}); +} diff --git a/test/fixtures/test-runner/output/before-and-after-each-with-timeout-too-many-listeners.snapshot b/test/fixtures/test-runner/output/before-and-after-each-with-timeout-too-many-listeners.snapshot new file mode 100644 index 0000000..4300e21 --- /dev/null +++ b/test/fixtures/test-runner/output/before-and-after-each-with-timeout-too-many-listeners.snapshot @@ -0,0 +1,65 @@ +TAP version 13 +# Subtest: 1 +ok 1 - 1 + --- + duration_ms: * + ... +# Subtest: 2 +ok 2 - 2 + --- + duration_ms: * + ... +# Subtest: 3 +ok 3 - 3 + --- + duration_ms: * + ... +# Subtest: 4 +ok 4 - 4 + --- + duration_ms: * + ... +# Subtest: 5 +ok 5 - 5 + --- + duration_ms: * + ... +# Subtest: 6 +ok 6 - 6 + --- + duration_ms: * + ... +# Subtest: 7 +ok 7 - 7 + --- + duration_ms: * + ... +# Subtest: 8 +ok 8 - 8 + --- + duration_ms: * + ... +# Subtest: 9 +ok 9 - 9 + --- + duration_ms: * + ... +# Subtest: 10 +ok 10 - 10 + --- + duration_ms: * + ... +# Subtest: 11 +ok 11 - 11 + --- + duration_ms: * + ... +1..11 +# tests 11 +# suites 0 +# pass 11 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/default_output.js b/test/fixtures/test-runner/output/default_output.js new file mode 100644 index 0000000..89003a3 --- /dev/null +++ b/test/fixtures/test-runner/output/default_output.js @@ -0,0 +1,17 @@ +'use strict'; +process.env.FORCE_COLOR = '1'; +delete process.env.NODE_DISABLE_COLORS; +delete process.env.NO_COLOR; + +require('../../../common'); +const test = require('#node:test'); + +test('should pass', () => {}); +test('should fail', () => { throw new Error('fail'); }); +test('should skip', { skip: true }, () => {}); +test('parent', (t) => { + t.test('should fail', () => { throw new Error('fail'); }); + t.test('should pass but parent fail', (t, done) => { + setImmediate(done); + }); +}); diff --git a/test/fixtures/test-runner/output/default_output.snapshot b/test/fixtures/test-runner/output/default_output.snapshot new file mode 100644 index 0000000..07b9ab8 --- /dev/null +++ b/test/fixtures/test-runner/output/default_output.snapshot @@ -0,0 +1,73 @@ +[32m✔ should pass [90m(*ms)[39m[39m +[31m✖ should fail [90m(*ms)[39m[39m + Error: fail + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + +[90m﹣ should skip [90m(*ms)[39m # SKIP[39m +▶ parent + [31m✖ should fail [90m(*ms)[39m[39m + Error: fail + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + + [31m✖ should pass but parent fail [90m(*ms)[39m[39m + [32m'test did not finish before its parent and was cancelled'[39m + +[31m✖ parent [90m(*ms)[39m[39m +[34mℹ tests 6[39m +[34mℹ suites 0[39m +[34mℹ pass 1[39m +[34mℹ fail 3[39m +[34mℹ cancelled 1[39m +[34mℹ skipped 1[39m +[34mℹ todo 0[39m +[34mℹ duration_ms *[39m + +[31m✖ failing tests:[39m + +* +[31m✖ should fail [90m(*ms)[39m[39m + Error: fail + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + +* +[31m✖ should fail [90m(*ms)[39m[39m + Error: fail + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + *[39m + +* +[31m✖ should pass but parent fail [90m(*ms)[39m[39m + [32m'test did not finish before its parent and was cancelled'[39m diff --git a/test/fixtures/test-runner/output/describe_it.js b/test/fixtures/test-runner/output/describe_it.js new file mode 100644 index 0000000..b3c3953 --- /dev/null +++ b/test/fixtures/test-runner/output/describe_it.js @@ -0,0 +1,392 @@ +'use strict'; +require('../../../common'); +const assert = require('node:assert'); +const { describe, it, test } = require('#node:test'); +const util = require('util'); + + +it.todo('sync pass todo', () => { + +}); + +it('sync pass todo with message', { todo: 'this is a passing todo' }, () => { +}); + +it.todo('sync todo', () => { + throw new Error('should not count as a failure'); +}); + +it('sync todo with message', { todo: 'this is a failing todo' }, () => { + throw new Error('should not count as a failure'); +}); + +it.skip('sync skip pass', () => { +}); + +it('sync skip pass with message', { skip: 'this is skipped' }, () => { +}); + +it('sync pass', () => { +}); + +it('sync throw fail', () => { + throw new Error('thrown from sync throw fail'); +}); + +it.skip('async skip pass', async () => { +}); + +it('async pass', async () => { + +}); + +test('mixing describe/it and test should work', () => {}); + +it('async throw fail', async () => { + throw new Error('thrown from async throw fail'); +}); + +it('async skip fail', async (t, done) => { + t.skip(); + throw new Error('thrown from async throw fail'); +}); + +it('async assertion fail', async () => { + // Make sure the assert module is handled. + assert.strictEqual(true, false); +}); + +it('resolve pass', () => { + return Promise.resolve(); +}); + +it('reject fail', () => { + return Promise.reject(new Error('rejected from reject fail')); +}); + +it('unhandled rejection - passes but warns', () => { + Promise.reject(new Error('rejected from unhandled rejection fail')); +}); + +it('async unhandled rejection - passes but warns', async () => { + Promise.reject(new Error('rejected from async unhandled rejection fail')); +}); + +it('immediate throw - passes but warns', () => { + setImmediate(() => { + throw new Error('thrown from immediate throw fail'); + }); +}); + +it('immediate reject - passes but warns', () => { + setImmediate(() => { + Promise.reject(new Error('rejected from immediate reject fail')); + }); +}); + +it('immediate resolve pass', () => { + return new Promise((resolve) => { + setImmediate(() => { + resolve(); + }); + }); +}); + +describe('subtest sync throw fail', () => { + it('+sync throw fail', () => { + throw new Error('thrown from subtest sync throw fail'); + }); + test('mixing describe/it and test should work', () => {}); +}); + +it('sync throw non-error fail', async () => { + throw Symbol('thrown symbol from sync throw non-error fail'); +}); + +describe('level 0a', { concurrency: 4 }, () => { + it('level 1a', async () => { + const p1a = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 100); + }); + + return p1a; + }); + + it('level 1b', async () => { + const p1b = new Promise((resolve) => { + resolve(); + }); + + return p1b; + }); + + it('level 1c', async () => { + const p1c = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 200); + }); + + return p1c; + }); + + it('level 1d', async () => { + const p1c = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 150); + }); + + return p1c; + }); + + const p0a = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 300); + }); + + return p0a; +}); + + +describe('invalid subtest - pass but subtest fails', () => { + setImmediate(() => { + it('invalid subtest fail', () => { + throw new Error('this should not be thrown'); + }); + }); +}); + +it.skip('sync skip option', () => { + throw new Error('this should not be executed'); +}); + +it('sync skip option with message', { skip: 'this is skipped' }, () => { + throw new Error('this should not be executed'); +}); + +it('sync skip option is false fail', { skip: false }, () => { + throw new Error('this should be executed'); +}); + +// A test with no arguments provided. +it(); + +// A test with only a named function provided. +it(function functionOnly() {}); + +// A test with only an anonymous function provided. +it(() => {}); + +// A test with only a name provided. +it('test with only a name provided'); + +// A test with an empty string name. +it(''); + +// A test with only options provided. +it({ skip: true }); + +// A test with only a name and options provided. +it('test with a name and options provided', { skip: true }); + +// A test with only options and a function provided. +it({ skip: true }, function functionAndOptions() {}); + +it('callback pass', (t, done) => { + setImmediate(done); +}); + +it('callback fail', (t, done) => { + setImmediate(() => { + done(new Error('callback failure')); + }); +}); + +it('sync t is this in test', function(t) { + assert.strictEqual(this, t); +}); + +it('async t is this in test', async function(t) { + assert.strictEqual(this, t); +}); + +it('callback t is this in test', function(t, done) { + assert.strictEqual(this, t); + done(); +}); + +it('callback also returns a Promise', async (t, done) => { + throw new Error('thrown from callback also returns a Promise'); +}); + +it('callback throw', (t, done) => { + throw new Error('thrown from callback throw'); +}); + +it('callback called twice', (t, done) => { + done(); + done(); +}); + +it('callback called twice in different ticks', (t, done) => { + setImmediate(done); + done(); +}); + +it('callback called twice in future tick', (t, done) => { + setImmediate(() => { + done(); + done(); + }); +}); + +it('callback async throw', (t, done) => { + setImmediate(() => { + throw new Error('thrown from callback async throw'); + }); +}); + +it('callback async throw after done', (t, done) => { + setImmediate(() => { + throw new Error('thrown from callback async throw after done'); + }); + + done(); +}); + +it('custom inspect symbol fail', () => { + const obj = { + [util.inspect.custom]() { + return 'customized'; + }, + foo: 1, + }; + + throw obj; +}); + +it('custom inspect symbol that throws fail', () => { + const obj = { + [util.inspect.custom]() { + throw new Error('bad-inspect'); + }, + foo: 1, + }; + + throw obj; +}); + +describe('subtest sync throw fails', () => { + it('sync throw fails at first', () => { + throw new Error('thrown from subtest sync throw fails at first'); + }); + it('sync throw fails at second', () => { + throw new Error('thrown from subtest sync throw fails at second'); + }); +}); + +describe('describe sync throw fails', () => { + it('should not run', () => {}); + throw new Error('thrown from describe'); +}); + +describe('describe async throw fails', async () => { + it('should not run', () => {}); + throw new Error('thrown from describe'); +}); + +describe('timeouts', () => { + it('timed out async test', { timeout: 5 }, async () => { + return new Promise((resolve) => { + setTimeout(() => { + // Empty timer so the process doesn't exit before the timeout triggers. + }, 5); + setTimeout(resolve, 30_000_000).unref(); + }); + }); + + it('timed out callback test', { timeout: 5 }, (t, done) => { + setTimeout(() => { + // Empty timer so the process doesn't exit before the timeout triggers. + }, 5); + setTimeout(done, 30_000_000).unref(); + }); + + + it('large timeout async test is ok', { timeout: 30_000_000 }, async () => { + return new Promise((resolve) => { + setTimeout(resolve, 10); + }); + }); + + it('large timeout callback test is ok', { timeout: 30_000_000 }, (t, done) => { + setTimeout(done, 10); + }); +}); + +describe('successful thenable', () => { + it('successful thenable', () => { + let thenCalled = false; + return { + get then() { + if (thenCalled) throw new Error(); + thenCalled = true; + return (successHandler) => successHandler(); + }, + }; + }); + + it('rejected thenable', () => { + let thenCalled = false; + return { + get then() { + if (thenCalled) throw new Error(); + thenCalled = true; + return (_, errorHandler) => errorHandler(new Error('custom error')); + }, + }; + }); + + let thenCalled = false; + return { + get then() { + if (thenCalled) throw new Error(); + thenCalled = true; + return (successHandler) => successHandler(); + }, + }; +}); + +describe('rejected thenable', () => { + let thenCalled = false; + return { + get then() { + if (thenCalled) throw new Error(); + thenCalled = true; + return (_, errorHandler) => errorHandler(new Error('custom error')); + }, + }; +}); + +describe("async describe function", async () => { + await null; + + await it("it inside describe 1", async () => { + await null + }); + await it("it inside describe 2", async () => { + await null; + }); + + describe("inner describe", async () => { + await null; + + it("it inside inner describe", async () => { + await null; + }); + }); +}); diff --git a/test/fixtures/test-runner/output/describe_it.snapshot b/test/fixtures/test-runner/output/describe_it.snapshot new file mode 100644 index 0000000..eb4f572 --- /dev/null +++ b/test/fixtures/test-runner/output/describe_it.snapshot @@ -0,0 +1,893 @@ +TAP version 13 +# Subtest: sync pass todo +ok 1 - sync pass todo # TODO + --- + duration_ms: * + ... +# Subtest: sync pass todo with message +ok 2 - sync pass todo with message # TODO this is a passing todo + --- + duration_ms: * + ... +# Subtest: sync todo +not ok 3 - sync todo # TODO + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):4' + failureType: 'testCodeFailure' + message: 'should not count as a failure' + toString: [Function (anonymous)] + error: 'should not count as a failure' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: sync todo with message +not ok 4 - sync todo with message # TODO this is a failing todo + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'should not count as a failure' + toString: [Function (anonymous)] + error: 'should not count as a failure' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: sync skip pass +ok 5 - sync skip pass # SKIP + --- + duration_ms: * + ... +# Subtest: sync skip pass with message +ok 6 - sync skip pass with message # SKIP this is skipped + --- + duration_ms: * + ... +# Subtest: sync pass +ok 7 - sync pass + --- + duration_ms: * + ... +# Subtest: sync throw fail +not ok 8 - sync throw fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'thrown from sync throw fail' + toString: [Function (anonymous)] + error: 'thrown from sync throw fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: async skip pass +ok 9 - async skip pass # SKIP + --- + duration_ms: * + ... +# Subtest: async pass +ok 10 - async pass + --- + duration_ms: * + ... +# Subtest: mixing describe/it and test should work +ok 11 - mixing describe/it and test should work + --- + duration_ms: * + ... +# Subtest: async throw fail +not ok 12 - async throw fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'thrown from async throw fail' + toString: [Function (anonymous)] + error: 'thrown from async throw fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: async skip fail +not ok 13 - async skip fail # SKIP + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'callbackAndPromisePresent' + message: 'passed a callback but also returned a Promise' + toString: [Function (anonymous)] + error: 'passed a callback but also returned a Promise' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: async assertion fail +not ok 14 - async assertion fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: |- + Expected values to be strictly equal: + + true !== false + + toString: [Function (anonymous)] + error: |- + Expected values to be strictly equal: + + true !== false + + code: 'ERR_ASSERTION' + name: 'AssertionError' + expected: false + actual: true + operator: 'strictEqual' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: resolve pass +ok 15 - resolve pass + --- + duration_ms: * + ... +# Subtest: reject fail +not ok 16 - reject fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'rejected from reject fail' + toString: [Function (anonymous)] + error: 'rejected from reject fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: unhandled rejection - passes but warns +ok 17 - unhandled rejection - passes but warns + --- + duration_ms: * + ... +# Subtest: async unhandled rejection - passes but warns +ok 18 - async unhandled rejection - passes but warns + --- + duration_ms: * + ... +# Subtest: immediate throw - passes but warns +ok 19 - immediate throw - passes but warns + --- + duration_ms: * + ... +# Subtest: immediate reject - passes but warns +ok 20 - immediate reject - passes but warns + --- + duration_ms: * + ... +# Subtest: immediate resolve pass +ok 21 - immediate resolve pass + --- + duration_ms: * + ... +# Subtest: subtest sync throw fail + # Subtest: +sync throw fail + not ok 1 - +sync throw fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):3' + failureType: 'testCodeFailure' + message: 'thrown from subtest sync throw fail' + toString: [Function (anonymous)] + error: 'thrown from subtest sync throw fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + Array.map () + * + * + * + ... + # Subtest: mixing describe/it and test should work + ok 2 - mixing describe/it and test should work + --- + duration_ms: * + ... + 1..2 +not ok 22 - subtest sync throw fail + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'subtestsFailed' + message: '1 subtest failed' + toString: [Function (anonymous)] + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: sync throw non-error fail +not ok 23 - sync throw non-error fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'Symbol(thrown symbol from sync throw non-error fail)' + toString: [Function (anonymous)] + error: 'Symbol(thrown symbol from sync throw non-error fail)' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + ... +# Subtest: level 0a + # Subtest: level 1a + ok 1 - level 1a + --- + duration_ms: * + ... + # Subtest: level 1b + ok 2 - level 1b + --- + duration_ms: * + ... + # Subtest: level 1c + ok 3 - level 1c + --- + duration_ms: * + ... + # Subtest: level 1d + ok 4 - level 1d + --- + duration_ms: * + ... + 1..4 +ok 24 - level 0a + --- + duration_ms: * + type: 'suite' + ... +# Subtest: invalid subtest - pass but subtest fails +ok 25 - invalid subtest - pass but subtest fails + --- + duration_ms: * + type: 'suite' + ... +# Subtest: sync skip option +ok 26 - sync skip option # SKIP + --- + duration_ms: * + ... +# Subtest: sync skip option with message +ok 27 - sync skip option with message # SKIP this is skipped + --- + duration_ms: * + ... +# Subtest: sync skip option is false fail +not ok 28 - sync skip option is false fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'this should be executed' + toString: [Function (anonymous)] + error: 'this should be executed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: +ok 29 - + --- + duration_ms: * + ... +# Subtest: functionOnly +ok 30 - functionOnly + --- + duration_ms: * + ... +# Subtest: +ok 31 - + --- + duration_ms: * + ... +# Subtest: test with only a name provided +ok 32 - test with only a name provided + --- + duration_ms: * + ... +# Subtest: +ok 33 - + --- + duration_ms: * + ... +# Subtest: +ok 34 - # SKIP + --- + duration_ms: * + ... +# Subtest: test with a name and options provided +ok 35 - test with a name and options provided # SKIP + --- + duration_ms: * + ... +# Subtest: functionAndOptions +ok 36 - functionAndOptions # SKIP + --- + duration_ms: * + ... +# Subtest: callback pass +ok 37 - callback pass + --- + duration_ms: * + ... +# Subtest: callback fail +not ok 38 - callback fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'callback failure' + toString: [Function (anonymous)] + error: 'callback failure' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + ... +# Subtest: sync t is this in test +ok 39 - sync t is this in test + --- + duration_ms: * + ... +# Subtest: async t is this in test +ok 40 - async t is this in test + --- + duration_ms: * + ... +# Subtest: callback t is this in test +ok 41 - callback t is this in test + --- + duration_ms: * + ... +# Subtest: callback also returns a Promise +not ok 42 - callback also returns a Promise + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'callbackAndPromisePresent' + message: 'passed a callback but also returned a Promise' + toString: [Function (anonymous)] + error: 'passed a callback but also returned a Promise' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: callback throw +not ok 43 - callback throw + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'thrown from callback throw' + toString: [Function (anonymous)] + error: 'thrown from callback throw' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: callback called twice +not ok 44 - callback called twice + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'multipleCallbackInvocations' + message: 'callback invoked multiple times' + toString: [Function (anonymous)] + error: 'callback invoked multiple times' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... +# Subtest: callback called twice in different ticks +ok 45 - callback called twice in different ticks + --- + duration_ms: * + ... +# Subtest: callback called twice in future tick +not ok 46 - callback called twice in future tick + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'uncaughtException' + message: 'callback invoked multiple times' + toString: [Function (anonymous)] + error: 'callback invoked multiple times' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: callback async throw +not ok 47 - callback async throw + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'uncaughtException' + message: 'thrown from callback async throw' + toString: [Function (anonymous)] + error: 'thrown from callback async throw' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + ... +# Subtest: callback async throw after done +ok 48 - callback async throw after done + --- + duration_ms: * + ... +# Subtest: custom inspect symbol fail +not ok 49 - custom inspect symbol fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'customized' + toString: [Function (anonymous)] + error: 'customized' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: custom inspect symbol that throws fail +not ok 50 - custom inspect symbol that throws fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: |- + { + foo: 1, + [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] + } + toString: [Function (anonymous)] + error: |- + { + foo: 1, + [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] + } + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: subtest sync throw fails + # Subtest: sync throw fails at first + not ok 1 - sync throw fails at first + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):3' + failureType: 'testCodeFailure' + message: 'thrown from subtest sync throw fails at first' + toString: [Function (anonymous)] + error: 'thrown from subtest sync throw fails at first' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + Array.map () + * + * + * + ... + # Subtest: sync throw fails at second + not ok 2 - sync throw fails at second + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):3' + failureType: 'testCodeFailure' + message: 'thrown from subtest sync throw fails at second' + toString: [Function (anonymous)] + error: 'thrown from subtest sync throw fails at second' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + async Promise.all (index 0) + * + * + ... + 1..2 +not ok 51 - subtest sync throw fails + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'subtestsFailed' + message: '2 subtests failed' + toString: [Function (anonymous)] + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: describe sync throw fails + # Subtest: should not run + not ok 1 - should not run + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):3' + failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + 1..1 +not ok 52 - describe sync throw fails + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'thrown from describe' + toString: [Function (anonymous)] + error: 'thrown from describe' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... +# Subtest: describe async throw fails + # Subtest: should not run + not ok 1 - should not run + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):3' + failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + 1..1 +not ok 53 - describe async throw fails + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'thrown from describe' + toString: [Function (anonymous)] + error: 'thrown from describe' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + * + * + * + ... +# Subtest: timeouts + # Subtest: timed out async test + not ok 1 - timed out async test + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):3' + failureType: 'testTimeoutFailure' + message: 'test timed out after 5ms' + toString: [Function (anonymous)] + error: 'test timed out after 5ms' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + async Promise.all (index 0) + * + * + ... + # Subtest: timed out callback test + not ok 2 - timed out callback test + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):3' + failureType: 'testTimeoutFailure' + message: 'test timed out after 5ms' + toString: [Function (anonymous)] + error: 'test timed out after 5ms' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + ... + # Subtest: large timeout async test is ok + ok 3 - large timeout async test is ok + --- + duration_ms: * + ... + # Subtest: large timeout callback test is ok + ok 4 - large timeout callback test is ok + --- + duration_ms: * + ... + 1..4 +not ok 54 - timeouts + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'subtestsFailed' + message: '2 subtests failed' + toString: [Function (anonymous)] + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: successful thenable + # Subtest: successful thenable + ok 1 - successful thenable + --- + duration_ms: * + ... + # Subtest: rejected thenable + not ok 2 - rejected thenable + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):3' + failureType: 'testCodeFailure' + message: 'custom error' + toString: [Function (anonymous)] + error: 'custom error' + code: 'ERR_TEST_FAILURE' + stack: |- + * + ... + 1..2 +not ok 55 - successful thenable + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'subtestsFailed' + message: '1 subtest failed' + toString: [Function (anonymous)] + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: rejected thenable +not ok 56 - rejected thenable + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'custom error' + toString: [Function (anonymous)] + error: 'custom error' + code: 'ERR_TEST_FAILURE' + stack: |- + * + ... +# Subtest: async describe function + # Subtest: it inside describe 1 + ok 1 - it inside describe 1 + --- + duration_ms: * + ... + # Subtest: it inside describe 2 + ok 2 - it inside describe 2 + --- + duration_ms: * + ... + # Subtest: inner describe + # Subtest: it inside inner describe + ok 1 - it inside inner describe + --- + duration_ms: * + ... + 1..1 + ok 3 - inner describe + --- + duration_ms: * + type: 'suite' + ... + 1..3 +ok 57 - async describe function + --- + duration_ms: * + type: 'suite' + ... +# Subtest: invalid subtest fail +not ok 58 - invalid subtest fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/describe_it.js:(LINE):5' + failureType: 'parentAlreadyFinished' + message: 'test could not be started because its parent finished' + toString: [Function (anonymous)] + error: 'test could not be started because its parent finished' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +1..58 +# Error: Test "unhandled rejection - passes but warns" at test/fixtures/test-runner/output/describe_it.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. +# Error: Test "async unhandled rejection - passes but warns" at test/fixtures/test-runner/output/describe_it.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. +# Error: Test "immediate throw - passes but warns" at test/fixtures/test-runner/output/describe_it.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. +# Error: Test "immediate reject - passes but warns" at test/fixtures/test-runner/output/describe_it.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. +# Error: Test "callback called twice in different ticks" at test/fixtures/test-runner/output/describe_it.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. +# Error: Test "callback async throw after done" at test/fixtures/test-runner/output/describe_it.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. +# tests 67 +# suites 11 +# pass 31 +# fail 19 +# cancelled 4 +# skipped 9 +# todo 4 +# duration_ms * diff --git a/test/fixtures/test-runner/output/describe_nested.js b/test/fixtures/test-runner/output/describe_nested.js new file mode 100644 index 0000000..cc4869d --- /dev/null +++ b/test/fixtures/test-runner/output/describe_nested.js @@ -0,0 +1,9 @@ +'use strict'; +require('../../../common'); +const { describe, it } = require('#node:test'); + +describe('nested - no tests', () => { + describe('nested', () => { + it('nested', () => {}); + }); +}); diff --git a/test/message/test_runner_describe_nested.out b/test/fixtures/test-runner/output/describe_nested.snapshot similarity index 89% rename from test/message/test_runner_describe_nested.out rename to test/fixtures/test-runner/output/describe_nested.snapshot index 1d3fe31..fe96d2a 100644 --- a/test/message/test_runner_describe_nested.out +++ b/test/fixtures/test-runner/output/describe_nested.snapshot @@ -10,14 +10,17 @@ TAP version 13 ok 1 - nested --- duration_ms: * + type: 'suite' ... 1..1 ok 1 - nested - no tests --- duration_ms: * + type: 'suite' ... 1..1 # tests 1 +# suites 2 # pass 1 # fail 0 # cancelled 0 diff --git a/test/fixtures/test-runner/output/dot_output_custom_columns.js b/test/fixtures/test-runner/output/dot_output_custom_columns.js new file mode 100644 index 0000000..f06391e --- /dev/null +++ b/test/fixtures/test-runner/output/dot_output_custom_columns.js @@ -0,0 +1,21 @@ +// Flags: --test-reporter=dot +'use strict'; +process.stdout.columns = 30; +process.env.FORCE_COLOR = '1'; +delete process.env.NODE_DISABLE_COLORS; +delete process.env.NO_COLOR; + +const test = require('#node:test'); +const { setTimeout } = require('timers/promises'); + +for (let i = 0; i < 100; i++) { + test(i + ' example', async () => { + if (i === 0) { + // So the reporter will run before all tests has started + await setTimeout(10); + } + // resize + if (i === 28) + process.stdout.columns = 41; + }); +} diff --git a/test/fixtures/test-runner/output/dot_output_custom_columns.snapshot b/test/fixtures/test-runner/output/dot_output_custom_columns.snapshot new file mode 100644 index 0000000..2c9c5ae --- /dev/null +++ b/test/fixtures/test-runner/output/dot_output_custom_columns.snapshot @@ -0,0 +1,3 @@ +[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m +[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m +[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m[32m.[0m diff --git a/test/fixtures/test-runner/output/dot_reporter.js b/test/fixtures/test-runner/output/dot_reporter.js new file mode 100644 index 0000000..e9b8f5c --- /dev/null +++ b/test/fixtures/test-runner/output/dot_reporter.js @@ -0,0 +1,7 @@ +'use strict'; +require('../../../common'); +const fixtures = require('../../../common/fixtures'); +const spawn = require('node:child_process').spawn; + +spawn(process.execPath, + ['--no-warnings', '--test-reporter', 'dot', fixtures.path('test-runner/output/output.js')], { stdio: 'inherit' }); diff --git a/test/fixtures/test-runner/output/dot_reporter.snapshot b/test/fixtures/test-runner/output/dot_reporter.snapshot new file mode 100644 index 0000000..81d830c --- /dev/null +++ b/test/fixtures/test-runner/output/dot_reporter.snapshot @@ -0,0 +1,234 @@ +..XX...X..XXX.X..... +XXX.....X..X...X.... +.....X...XXX.XX..... +XXXXXXX...XXXXX + +Failed tests: + +✖ sync fail todo (*ms) # TODO + Error: thrown from sync fail todo + * + * + * + * + * + * + * +✖ sync fail todo with message (*ms) # this is a failing todo + Error: thrown from sync fail todo with message + * + * + * + * + * + * + * +✖ sync throw fail (*ms) + Error: thrown from sync throw fail + * + * + * + * + * + * + * +✖ async throw fail (*ms) + Error: thrown from async throw fail + * + * + * + * + * + * + * +﹣ async skip fail (*ms) # SKIP + Error: thrown from async throw fail + * + * + * + * + * + * + * +✖ async assertion fail (*ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + * + * + * + * + * + * + * { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: true, + expected: false, + operator: 'strictEqual' + } +✖ reject fail (*ms) + Error: rejected from reject fail + * + * + * + * + * + * + * +✖ +sync throw fail (*ms) + Error: thrown from subtest sync throw fail + * + * + * + * + * + * + * + * + * + * + * + * +✖ subtest sync throw fail (*ms) + '1 subtest failed' +✖ sync throw non-error fail (*ms) + Symbol(thrown symbol from sync throw non-error fail) +✖ +long running (*ms) + 'test did not finish before its parent and was cancelled' +✖ top level (*ms) + '1 subtest failed' +✖ sync skip option is false fail (*ms) + Error: this should be executed + * + * + * + * + * + * + * +✖ callback fail (*ms) + Error: callback failure + * + * +✖ callback also returns a Promise (*ms) + 'passed a callback but also returned a Promise' +✖ callback throw (*ms) + Error: thrown from callback throw + * + * + * + * + * + * + * +✖ callback called twice (*ms) + 'callback invoked multiple times' +✖ callback called twice in future tick (*ms) + Error: callback invoked multiple times + * + * + * + * + * { + failureType: 'multipleCallbackInvocations', + cause: 'callback invoked multiple times', + code: 'ERR_TEST_FAILURE', + toString: [Function (anonymous)] + } +✖ callback async throw (*ms) + Error: thrown from callback async throw + * + * +✖ custom inspect symbol fail (*ms) + customized +✖ custom inspect symbol that throws fail (*ms) + { foo: 1, [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] } +✖ sync throw fails at first (*ms) + Error: thrown from subtest sync throw fails at first + * + * + * + * + * + * + * + * + * + * + * + * +✖ sync throw fails at second (*ms) + Error: thrown from subtest sync throw fails at second + * + * + * + * + * + * + * + * +✖ subtest sync throw fails (*ms) + '2 subtests failed' +✖ timed out async test (*ms) + 'test timed out after *ms' +✖ timed out callback test (*ms) + 'test timed out after *ms' +✖ rejected thenable (*ms) + 'custom error' +✖ unfinished test with uncaughtException (*ms) + Error: foo + * + * + * +✖ unfinished test with unhandledRejection (*ms) + Error: bar + * + * + * +✖ assertion errors display actual and expected properly (*ms) + AssertionError [ERR_ASSERTION]: Expected values to be loosely deep-equal: + + { + bar: 1, + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + foo: 1 + } + + should loosely deep-equal + + { + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + circular: { + bar: 2, + c: [Circular *1] + } + } + * { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: [Object], + expected: [Object], + operator: 'deepEqual' + } +✖ invalid subtest fail (*ms) + 'test could not be started because its parent finished' diff --git a/test/fixtures/test-runner/output/eval_dot.js b/test/fixtures/test-runner/output/eval_dot.js new file mode 100644 index 0000000..7b05c1e --- /dev/null +++ b/test/fixtures/test-runner/output/eval_dot.js @@ -0,0 +1,10 @@ +// Flags: --test-reporter=dot +'use strict'; +eval(` + const { test } = require('#node:test'); + + test('passes'); + test('fails', () => { + throw new Error('fail'); + }); +`); diff --git a/test/fixtures/test-runner/output/eval_dot.snapshot b/test/fixtures/test-runner/output/eval_dot.snapshot new file mode 100644 index 0000000..8a22cfd --- /dev/null +++ b/test/fixtures/test-runner/output/eval_dot.snapshot @@ -0,0 +1,13 @@ +.X + +Failed tests: + +✖ fails (*ms) + Error: fail + * + * + * + * + * + * + * diff --git a/test/fixtures/test-runner/output/eval_spec.js b/test/fixtures/test-runner/output/eval_spec.js new file mode 100644 index 0000000..a65157b --- /dev/null +++ b/test/fixtures/test-runner/output/eval_spec.js @@ -0,0 +1,10 @@ +// Flags: --test-reporter=spec +'use strict'; +eval(` + const { test } = require('#node:test'); + + test('passes'); + test('fails', () => { + throw new Error('fail'); + }); +`); diff --git a/test/fixtures/test-runner/output/eval_spec.snapshot b/test/fixtures/test-runner/output/eval_spec.snapshot new file mode 100644 index 0000000..5c9a530 --- /dev/null +++ b/test/fixtures/test-runner/output/eval_spec.snapshot @@ -0,0 +1,31 @@ +✔ passes (*ms) +✖ fails (*ms) + Error: fail + * + * + * + * + * + * + * + +ℹ tests 2 +ℹ suites 0 +ℹ pass 1 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms * + +✖ failing tests: + +✖ fails (*ms) + Error: fail + * + * + * + * + * + * + * diff --git a/test/fixtures/test-runner/output/eval_tap.js b/test/fixtures/test-runner/output/eval_tap.js new file mode 100644 index 0000000..301e7bb --- /dev/null +++ b/test/fixtures/test-runner/output/eval_tap.js @@ -0,0 +1,10 @@ +// Flags: --test-reporter=tap +'use strict'; +eval(` + const { test } = require('#node:test'); + + test('passes'); + test('fails', () => { + throw new Error('fail'); + }); +`); diff --git a/test/fixtures/test-runner/output/eval_tap.snapshot b/test/fixtures/test-runner/output/eval_tap.snapshot new file mode 100644 index 0000000..1aee19b --- /dev/null +++ b/test/fixtures/test-runner/output/eval_tap.snapshot @@ -0,0 +1,33 @@ +TAP version 13 +# Subtest: passes +ok 1 - passes + --- + duration_ms: * + ... +# Subtest: fails +not ok 2 - fails + --- + duration_ms: * + failureType: 'testCodeFailure' + message: 'fail' + toString: [Function (anonymous)] + error: 'fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +1..2 +# tests 2 +# suites 0 +# pass 1 +# fail 1 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/filtered-suite-delayed-build.js b/test/fixtures/test-runner/output/filtered-suite-delayed-build.js new file mode 100644 index 0000000..154f911 --- /dev/null +++ b/test/fixtures/test-runner/output/filtered-suite-delayed-build.js @@ -0,0 +1,16 @@ +// Flags: --test-name-pattern=enabled +'use strict'; +const common = require('../../../common'); +const { suite, test } = require('#node:test'); + +suite('async suite', async () => { + await 1; + test('enabled 1', common.mustCall()); + await 1; + test('not run', common.mustNotCall()); + await 1; +}); + +suite('sync suite', () => { + test('enabled 2', common.mustCall()); +}); diff --git a/test/fixtures/test-runner/output/filtered-suite-delayed-build.snapshot b/test/fixtures/test-runner/output/filtered-suite-delayed-build.snapshot new file mode 100644 index 0000000..dbe3048 --- /dev/null +++ b/test/fixtures/test-runner/output/filtered-suite-delayed-build.snapshot @@ -0,0 +1,34 @@ +TAP version 13 +# Subtest: async suite + # Subtest: enabled 1 + ok 1 - enabled 1 + --- + duration_ms: * + ... + 1..1 +ok 1 - async suite + --- + duration_ms: * + type: 'suite' + ... +# Subtest: sync suite + # Subtest: enabled 2 + ok 1 - enabled 2 + --- + duration_ms: * + ... + 1..1 +ok 2 - sync suite + --- + duration_ms: * + type: 'suite' + ... +1..2 +# tests 2 +# suites 2 +# pass 2 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/filtered-suite-order.mjs b/test/fixtures/test-runner/output/filtered-suite-order.mjs new file mode 100644 index 0000000..f4a6470 --- /dev/null +++ b/test/fixtures/test-runner/output/filtered-suite-order.mjs @@ -0,0 +1,50 @@ +// Flags: --test-only +import pkg from '#node:test'; +const { describe, test, after } = pkg; + +after(() => { console.log('with global after()'); }); +await Promise.resolve(); + +console.log('Execution order was:'); +const ll = (t) => { console.log(` * ${t.fullName}`) }; + +describe('A', () => { + test.only('A', ll); + test('B', ll); + describe.only('C', () => { + test.only('A', ll); + test('B', ll); + }); + describe('D', () => { + test.only('A', ll); + test('B', ll); + }); +}); +describe.only('B', () => { + test('A', ll); + test('B', ll); + describe('C', () => { + test('A', ll); + }); +}); +describe('C', () => { + test.only('A', ll); + test('B', ll); + describe.only('C', () => { + test('A', ll); + test('B', ll); + }); + describe('D', () => { + test('A', ll); + test.only('B', ll); + }); +}); +describe('D', () => { + test('A', ll); + test.only('B', ll); +}); +describe.only('E', () => { + test('A', ll); + test('B', ll); +}); +test.only('F', ll); diff --git a/test/fixtures/test-runner/output/filtered-suite-order.snapshot b/test/fixtures/test-runner/output/filtered-suite-order.snapshot new file mode 100644 index 0000000..7a18df8 --- /dev/null +++ b/test/fixtures/test-runner/output/filtered-suite-order.snapshot @@ -0,0 +1,166 @@ +Execution order was: + * A > A + * A > C > A + * A > D > A + * B > A + * B > B + * B > C > A + * C > A + * C > C > A + * C > C > B + * C > D > B + * D > B + * E > A + * E > B + * F +with global after() +TAP version 13 +# Subtest: A + # Subtest: A + ok 1 - A + --- + duration_ms: * + ... + # Subtest: C + # Subtest: A + ok 1 - A + --- + duration_ms: * + ... + 1..1 + ok 2 - C + --- + duration_ms: * + type: 'suite' + ... + # Subtest: D + # Subtest: A + ok 1 - A + --- + duration_ms: * + ... + 1..1 + ok 3 - D + --- + duration_ms: * + type: 'suite' + ... + 1..3 +ok 1 - A + --- + duration_ms: * + type: 'suite' + ... +# Subtest: B + # Subtest: A + ok 1 - A + --- + duration_ms: * + ... + # Subtest: B + ok 2 - B + --- + duration_ms: * + ... + # Subtest: C + # Subtest: A + ok 1 - A + --- + duration_ms: * + ... + 1..1 + ok 3 - C + --- + duration_ms: * + type: 'suite' + ... + 1..3 +ok 2 - B + --- + duration_ms: * + type: 'suite' + ... +# Subtest: C + # Subtest: A + ok 1 - A + --- + duration_ms: * + ... + # Subtest: C + # Subtest: A + ok 1 - A + --- + duration_ms: * + ... + # Subtest: B + ok 2 - B + --- + duration_ms: * + ... + 1..2 + ok 2 - C + --- + duration_ms: * + type: 'suite' + ... + # Subtest: D + # Subtest: B + ok 1 - B + --- + duration_ms: * + ... + 1..1 + ok 3 - D + --- + duration_ms: * + type: 'suite' + ... + 1..3 +ok 3 - C + --- + duration_ms: * + type: 'suite' + ... +# Subtest: D + # Subtest: B + ok 1 - B + --- + duration_ms: * + ... + 1..1 +ok 4 - D + --- + duration_ms: * + type: 'suite' + ... +# Subtest: E + # Subtest: A + ok 1 - A + --- + duration_ms: * + ... + # Subtest: B + ok 2 - B + --- + duration_ms: * + ... + 1..2 +ok 5 - E + --- + duration_ms: * + type: 'suite' + ... +# Subtest: F +ok 6 - F + --- + duration_ms: * + ... +1..6 +# tests 14 +# suites 10 +# pass 14 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/filtered-suite-throws.js b/test/fixtures/test-runner/output/filtered-suite-throws.js new file mode 100644 index 0000000..e9df3e8 --- /dev/null +++ b/test/fixtures/test-runner/output/filtered-suite-throws.js @@ -0,0 +1,14 @@ +// Flags: --test-name-pattern=enabled +'use strict'; +const common = require('../../../common'); +const { suite, test } = require('#node:test'); + +suite('suite 1', () => { + throw new Error('boom 1'); + test('enabled - should not run', common.mustNotCall()); +}); + +suite('suite 2', () => { + test('enabled - should get cancelled', common.mustNotCall()); + throw new Error('boom 1'); +}); diff --git a/test/fixtures/test-runner/output/filtered-suite-throws.snapshot b/test/fixtures/test-runner/output/filtered-suite-throws.snapshot new file mode 100644 index 0000000..6b81b05 --- /dev/null +++ b/test/fixtures/test-runner/output/filtered-suite-throws.snapshot @@ -0,0 +1,78 @@ +TAP version 13 +# Subtest: suite 1 +not ok 1 - suite 1 + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/filtered-suite-throws.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'boom 1' + toString: [Function (anonymous)] + error: 'boom 1' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... +# Subtest: suite 2 + # Subtest: enabled - should get cancelled + not ok 1 - enabled - should get cancelled + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/filtered-suite-throws.js:(LINE):3' + failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + 1..1 +not ok 2 - suite 2 + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/filtered-suite-throws.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'boom 1' + toString: [Function (anonymous)] + error: 'boom 1' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + * + * + * + ... +1..2 +# tests 1 +# suites 2 +# pass 0 +# fail 0 +# cancelled 1 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/force_exit.js b/test/fixtures/test-runner/output/force_exit.js new file mode 100644 index 0000000..c0d48a3 --- /dev/null +++ b/test/fixtures/test-runner/output/force_exit.js @@ -0,0 +1,27 @@ +// Flags: --test-force-exit --test-reporter=spec +'use strict'; +const { after, afterEach, before, beforeEach, test } = require('#node:test'); + +before(() => { + console.log('BEFORE'); +}); + +beforeEach(() => { + console.log('BEFORE EACH'); +}); + +after(() => { + console.log('AFTER'); +}); + +afterEach(() => { + console.log('AFTER EACH'); +}); + +test('passes but oops', () => { + setTimeout(() => { + throw new Error('this should not have a chance to be thrown'); + }, 1000); +}); + +test('also passes'); diff --git a/test/fixtures/test-runner/output/force_exit.snapshot b/test/fixtures/test-runner/output/force_exit.snapshot new file mode 100644 index 0000000..b45901f --- /dev/null +++ b/test/fixtures/test-runner/output/force_exit.snapshot @@ -0,0 +1,16 @@ +BEFORE +BEFORE EACH +AFTER EACH +BEFORE EACH +AFTER EACH +AFTER +✔ passes but oops (*ms) +✔ also passes (*ms) +ℹ tests 2 +ℹ suites 0 +ℹ pass 2 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms * diff --git a/test/fixtures/test-runner/output/global-hooks-with-no-tests.js b/test/fixtures/test-runner/output/global-hooks-with-no-tests.js new file mode 100644 index 0000000..2486f6c --- /dev/null +++ b/test/fixtures/test-runner/output/global-hooks-with-no-tests.js @@ -0,0 +1,6 @@ +'use strict'; +const common = require('../../../common'); +const { before, after } = require('#node:test'); + +before(common.mustCall(() => console.log('before'))); +after(common.mustCall(() => console.log('after'))); diff --git a/test/fixtures/test-runner/output/global-hooks-with-no-tests.snapshot b/test/fixtures/test-runner/output/global-hooks-with-no-tests.snapshot new file mode 100644 index 0000000..9d622d8 --- /dev/null +++ b/test/fixtures/test-runner/output/global-hooks-with-no-tests.snapshot @@ -0,0 +1,12 @@ +before +TAP version 13 +after +1..0 +# tests 0 +# suites 0 +# pass 0 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/global_after_should_fail_the_test.js b/test/fixtures/test-runner/output/global_after_should_fail_the_test.js new file mode 100644 index 0000000..becb126 --- /dev/null +++ b/test/fixtures/test-runner/output/global_after_should_fail_the_test.js @@ -0,0 +1,10 @@ +'use strict'; +const { it, after } = require('#node:test'); + +after(() => { + throw new Error('this should fail the test') +}); + +it('this is a test', () => { + console.log('this is a test') +}); diff --git a/test/fixtures/test-runner/output/global_after_should_fail_the_test.snapshot b/test/fixtures/test-runner/output/global_after_should_fail_the_test.snapshot new file mode 100644 index 0000000..fdec141 --- /dev/null +++ b/test/fixtures/test-runner/output/global_after_should_fail_the_test.snapshot @@ -0,0 +1,36 @@ +this is a test +TAP version 13 +# Subtest: this is a test +ok 1 - this is a test + --- + duration_ms: * + ... +not ok 2 - /test/fixtures/test-runner/output/global_after_should_fail_the_test.js + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/global_after_should_fail_the_test.js:(LINE):1' + failureType: 'hookFailed' + message: 'this should fail the test' + toString: [Function (anonymous)] + error: 'this should fail the test' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + ... +1..1 +# tests 1 +# suites 0 +# pass 1 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/hooks-with-no-global-test.js b/test/fixtures/test-runner/output/hooks-with-no-global-test.js new file mode 100644 index 0000000..cd3b114 --- /dev/null +++ b/test/fixtures/test-runner/output/hooks-with-no-global-test.js @@ -0,0 +1,78 @@ +'use strict'; +const { test, describe, it, before, after, beforeEach, afterEach } = require('#node:test'); +const assert = require("assert"); + +// This file should not have any global tests to reproduce bug #48844 +const testArr = []; + +before(() => testArr.push('global before')); +after(() => { + testArr.push('global after'); + + assert.deepStrictEqual(testArr, [ + 'global before', + 'describe before', + + 'describe beforeEach', + 'describe it 1', + 'describe afterEach', + + 'describe beforeEach', + 'describe test 2', + 'describe afterEach', + + 'describe nested before', + + 'describe beforeEach', + 'describe nested beforeEach', + 'describe nested it 1', + 'describe nested afterEach', + 'describe afterEach', + + 'describe beforeEach', + 'describe nested beforeEach', + 'describe nested test 2', + 'describe nested afterEach', + 'describe afterEach', + + 'describe nested after', + 'describe after', + 'global after', + ]); +}); + +describe('describe hooks with no global tests', () => { + before(() => { + testArr.push('describe before'); + }); + after(()=> { + testArr.push('describe after'); + }); + beforeEach(() => { + testArr.push('describe beforeEach'); + }); + afterEach(() => { + testArr.push('describe afterEach'); + }); + + it('1', () => testArr.push('describe it 1')); + test('2', () => testArr.push('describe test 2')); + + describe('nested', () => { + before(() => { + testArr.push('describe nested before') + }); + after(() => { + testArr.push('describe nested after') + }); + beforeEach(() => { + testArr.push('describe nested beforeEach') + }); + afterEach(() => { + testArr.push('describe nested afterEach') + }); + + it('nested 1', () => testArr.push('describe nested it 1')); + test('nested 2', () => testArr.push('describe nested test 2')); + }); +}); diff --git a/test/fixtures/test-runner/output/hooks-with-no-global-test.snapshot b/test/fixtures/test-runner/output/hooks-with-no-global-test.snapshot new file mode 100644 index 0000000..722a3a4 --- /dev/null +++ b/test/fixtures/test-runner/output/hooks-with-no-global-test.snapshot @@ -0,0 +1,44 @@ +TAP version 13 +# Subtest: describe hooks with no global tests + # Subtest: 1 + ok 1 - 1 + --- + duration_ms: * + ... + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + # Subtest: nested + # Subtest: nested 1 + ok 1 - nested 1 + --- + duration_ms: * + ... + # Subtest: nested 2 + ok 2 - nested 2 + --- + duration_ms: * + ... + 1..2 + ok 3 - nested + --- + duration_ms: * + type: 'suite' + ... + 1..3 +ok 1 - describe hooks with no global tests + --- + duration_ms: * + type: 'suite' + ... +1..1 +# tests 4 +# suites 2 +# pass 4 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/hooks.js b/test/fixtures/test-runner/output/hooks.js new file mode 100644 index 0000000..9abd027 --- /dev/null +++ b/test/fixtures/test-runner/output/hooks.js @@ -0,0 +1,306 @@ +'use strict'; +const common = require('../../../common'); +const assert = require('assert'); +const { test, describe, it, before, after, beforeEach, afterEach } = require('#node:test'); +const { setTimeout } = require('node:timers/promises'); + +before((t) => t.diagnostic('before 1 called')); +after((t) => t.diagnostic('after 1 called')); + +describe('describe hooks', () => { + const testArr = []; + before(function() { + testArr.push('before ' + this.name); + }); + after(common.mustCall(function() { + testArr.push('after ' + this.name); + assert.deepStrictEqual(testArr, [ + 'before describe hooks', + 'beforeEach 1', '1', 'afterEach 1', + 'beforeEach 2', '2', 'afterEach 2', + 'before nested', + 'beforeEach nested 1', '+beforeEach nested 1', 'nested 1', '+afterEach nested 1', 'afterEach nested 1', + 'beforeEach nested 2', '+beforeEach nested 2', 'nested 2', '+afterEach nested 2', 'afterEach nested 2', + 'after nested', + 'after describe hooks', + ]); + })); + beforeEach(function() { + testArr.push('beforeEach ' + this.name); + }); + afterEach(function() { + testArr.push('afterEach ' + this.name); + }); + + it('1', () => testArr.push('1')); + test('2', () => testArr.push('2')); + + describe('nested', () => { + before(function() { + testArr.push('before ' + this.name); + }); + after(function() { + testArr.push('after ' + this.name); + }); + beforeEach(function() { + testArr.push('+beforeEach ' + this.name); + }); + afterEach(function() { + testArr.push('+afterEach ' + this.name); + }); + it('nested 1', () => testArr.push('nested 1')); + test('nested 2', () => testArr.push('nested 2')); + }); +}); + +describe('describe hooks - no subtests', () => { + const testArr = []; + before(function() { + testArr.push('before ' + this.name); + }); + after(common.mustCall(function() { + testArr.push('after ' + this.name); + assert.deepStrictEqual(testArr, [ + 'before describe hooks - no subtests', + 'after describe hooks - no subtests', + ]); + })); + beforeEach(common.mustNotCall()); + afterEach(common.mustNotCall()); +}); + +describe('before throws', () => { + before(() => { throw new Error('before'); }); + it('1', () => {}); + test('2', () => {}); +}); + +describe('before throws - no subtests', () => { + before(() => { throw new Error('before'); }); + after(common.mustCall()); +}); + +describe('after throws', () => { + after(() => { throw new Error('after'); }); + it('1', () => {}); + test('2', () => {}); +}); + +describe('after throws - no subtests', () => { + after(() => { throw new Error('after'); }); +}); + +describe('beforeEach throws', () => { + beforeEach(() => { throw new Error('beforeEach'); }); + it('1', () => {}); + test('2', () => {}); +}); + +describe('afterEach throws', () => { + afterEach(() => { throw new Error('afterEach'); }); + it('1', () => {}); + test('2', () => {}); +}); + +describe('afterEach when test fails', () => { + afterEach(common.mustCall(2)); + it('1', () => { throw new Error('test'); }); + test('2', () => {}); +}); + +describe('afterEach throws and test fails', () => { + afterEach(() => { throw new Error('afterEach'); }); + it('1', () => { throw new Error('test'); }); + test('2', () => {}); +}); + +test('test hooks', async (t) => { + const testArr = []; + + t.before((t) => testArr.push('before ' + t.name)); + t.after(common.mustCall((t) => testArr.push('after ' + t.name))); + t.beforeEach((t) => testArr.push('beforeEach ' + t.name)); + t.afterEach((t) => testArr.push('afterEach ' + t.name)); + await t.test('1', () => testArr.push('1')); + await t.test('2', () => testArr.push('2')); + + await t.test('nested', async (t) => { + t.before((t) => testArr.push('nested before ' + t.name)); + t.after((t) => testArr.push('nested after ' + t.name)); + t.beforeEach((t) => testArr.push('nested beforeEach ' + t.name)); + t.afterEach((t) => testArr.push('nested afterEach ' + t.name)); + await t.test('nested 1', () => testArr.push('nested1')); + await t.test('nested 2', () => testArr.push('nested 2')); + }); + + t.after(common.mustCall(() => { + assert.deepStrictEqual(testArr, [ + 'before test hooks', + 'beforeEach 1', '1', 'afterEach 1', + 'beforeEach 2', '2', 'afterEach 2', + 'beforeEach nested', + 'nested before nested', + 'beforeEach nested 1', 'nested beforeEach nested 1', 'nested1', 'nested afterEach nested 1', 'afterEach nested 1', + 'beforeEach nested 2', 'nested beforeEach nested 2', 'nested 2', 'nested afterEach nested 2', 'afterEach nested 2', + 'afterEach nested', + 'nested after nested', + 'after test hooks', + ]); + })); +}); + +test('test hooks - no subtests', async (t) => { + const testArr = []; + + t.before((t) => testArr.push('before ' + t.name)); + t.after(common.mustCall((t) => testArr.push('after ' + t.name))); + t.beforeEach(common.mustNotCall()); + t.afterEach(common.mustNotCall()); + + t.after(common.mustCall(() => { + assert.deepStrictEqual(testArr, [ + 'before test hooks - no subtests', + 'after test hooks - no subtests', + ]); + })); +}); + +test('t.before throws', async (t) => { + t.after(common.mustCall()); + t.before(() => { throw new Error('before'); }); + await t.test('1', () => {}); + await t.test('2', () => {}); +}); + +test('t.before throws - no subtests', async (t) => { + t.after(common.mustCall()); + t.before(() => { throw new Error('before'); }); +}); + +test('t.after throws', async (t) => { + t.before(common.mustCall()); + t.after(() => { throw new Error('after'); }); + await t.test('1', () => {}); + await t.test('2', () => {}); +}); + +test('t.after throws - no subtests', async (t) => { + t.before(common.mustCall()); + t.after(() => { throw new Error('after'); }); +}); + + +test('t.beforeEach throws', async (t) => { + t.after(common.mustCall()); + t.beforeEach(() => { throw new Error('beforeEach'); }); + await t.test('1', () => {}); + await t.test('2', () => {}); +}); + +test('t.afterEach throws', async (t) => { + t.after(common.mustCall()); + t.afterEach(() => { throw new Error('afterEach'); }); + await t.test('1', () => {}); + await t.test('2', () => {}); +}); + + +test('afterEach when test fails', async (t) => { + t.after(common.mustCall()); + t.afterEach(common.mustCall(2)); + await t.test('1', () => { throw new Error('test'); }); + await t.test('2', () => {}); +}); + +test('afterEach context when test passes', async (t) => { + t.afterEach(common.mustCall((ctx) => { + assert.strictEqual(ctx.name, '1'); + assert.strictEqual(ctx.passed, true); + assert.strictEqual(ctx.error, null); + })); + await t.test('1', () => {}); +}); + +test('afterEach context when test fails', async (t) => { + const err = new Error('test'); + t.afterEach(common.mustCall((ctx) => { + assert.strictEqual(ctx.name, '1'); + assert.strictEqual(ctx.passed, false); + assert.strictEqual(ctx.error, err); + })); + await t.test('1', () => { throw err }); +}); + +test('afterEach throws and test fails', async (t) => { + t.after(common.mustCall()); + t.afterEach(() => { throw new Error('afterEach'); }); + await t.test('1', () => { throw new Error('test'); }); + await t.test('2', () => {}); +}); + +test('t.after() is called if test body throws', (t) => { + t.after(() => { + t.diagnostic('- after() called'); + }); + throw new Error('bye'); +}); + +describe('run after when before throws', () => { + after(common.mustCall(() => { + console.log("- after() called") + })); + before(() => { throw new Error('before')}); + it('1', () => {}); +}); + + +test('test hooks - async', async (t) => { + const testArr = []; + + t.before(async (t) => { + testArr.push('before starting ' + t.name); + await setTimeout(10); + testArr.push('before ending ' + t.name); + }); + t.after(async (t) => { + testArr.push('after starting ' + t.name); + await setTimeout(10); + testArr.push('after ending ' + t.name); + }); + t.beforeEach(async (t) => { + testArr.push('beforeEach starting ' + t.name); + await setTimeout(10); + testArr.push('beforeEach ending ' + t.name); + }); + t.afterEach(async (t) => { + testArr.push('afterEach starting ' + t.name); + await setTimeout(10); + testArr.push('afterEach ending ' + t.name); + }); + await t.test('1', async () => { + testArr.push('1 starting'); + await setTimeout(10); + testArr.push('1 ending'); + }); + await t.test('2', async () => { + testArr.push('2 starting'); + await setTimeout(10); + testArr.push('2 ending'); + }); + + t.after(common.mustCall(() => { + assert.deepStrictEqual(testArr, [ + 'before starting test hooks - async', 'before ending test hooks - async', + 'beforeEach starting 1', 'beforeEach ending 1', + '1 starting', '1 ending', + 'afterEach starting 1', 'afterEach ending 1', + 'beforeEach starting 2', 'beforeEach ending 2', + '2 starting', '2 ending', + 'afterEach starting 2', 'afterEach ending 2', + 'after starting test hooks - async', 'after ending test hooks - async', + ]); + })); +}); + +before((t) => t.diagnostic('before 2 called')); +after((t) => t.diagnostic('after 2 called')); diff --git a/test/fixtures/test-runner/output/hooks.snapshot b/test/fixtures/test-runner/output/hooks.snapshot new file mode 100644 index 0000000..0f6e7f8 --- /dev/null +++ b/test/fixtures/test-runner/output/hooks.snapshot @@ -0,0 +1,957 @@ +- after() called +TAP version 13 +# Subtest: describe hooks + # Subtest: 1 + ok 1 - 1 + --- + duration_ms: * + ... + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + # Subtest: nested + # Subtest: nested 1 + ok 1 - nested 1 + --- + duration_ms: * + ... + # Subtest: nested 2 + ok 2 - nested 2 + --- + duration_ms: * + ... + 1..2 + ok 3 - nested + --- + duration_ms: * + type: 'suite' + ... + 1..3 +ok 1 - describe hooks + --- + duration_ms: * + type: 'suite' + ... +# Subtest: describe hooks - no subtests +ok 2 - describe hooks - no subtests + --- + duration_ms: * + type: 'suite' + ... +# Subtest: before throws + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):3' + failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):3' + failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + 1..2 +not ok 3 - before throws + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'hookFailed' + message: 'failed running before hook' + toString: [Function (anonymous)] + error: 'before' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + ... +# Subtest: before throws - no subtests +not ok 4 - before throws - no subtests + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'hookFailed' + message: 'failed running before hook' + toString: [Function (anonymous)] + error: 'before' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + ... +# Subtest: after throws + # Subtest: 1 + ok 1 - 1 + --- + duration_ms: * + ... + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + 1..2 +not ok 5 - after throws + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'hookFailed' + message: 'failed running after hook' + toString: [Function (anonymous)] + error: 'after' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + ... +# Subtest: after throws - no subtests +not ok 6 - after throws - no subtests + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'hookFailed' + message: 'failed running after hook' + toString: [Function (anonymous)] + error: 'after' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + ... +# Subtest: beforeEach throws + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):3' + failureType: 'hookFailed' + message: 'failed running beforeEach hook' + toString: [Function (anonymous)] + error: 'beforeEach' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + async Promise.all (index 0) + * + * + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):3' + failureType: 'hookFailed' + message: 'failed running beforeEach hook' + toString: [Function (anonymous)] + error: 'beforeEach' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + ... + 1..2 +not ok 7 - beforeEach throws + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'subtestsFailed' + message: '2 subtests failed' + toString: [Function (anonymous)] + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: afterEach throws + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):3' + failureType: 'hookFailed' + message: 'failed running afterEach hook' + toString: [Function (anonymous)] + error: 'afterEach' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + async Promise.all (index 0) + * + * + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):3' + failureType: 'hookFailed' + message: 'failed running afterEach hook' + toString: [Function (anonymous)] + error: 'afterEach' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + ... + 1..2 +not ok 8 - afterEach throws + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'subtestsFailed' + message: '2 subtests failed' + toString: [Function (anonymous)] + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: afterEach when test fails + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):3' + failureType: 'testCodeFailure' + message: 'test' + toString: [Function (anonymous)] + error: 'test' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + Array.map () + * + * + * + ... + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + 1..2 +not ok 9 - afterEach when test fails + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'subtestsFailed' + message: '1 subtest failed' + toString: [Function (anonymous)] + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: afterEach throws and test fails + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):3' + failureType: 'testCodeFailure' + message: 'test' + toString: [Function (anonymous)] + error: 'test' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + Array.map () + * + * + * + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):3' + failureType: 'hookFailed' + message: 'failed running afterEach hook' + toString: [Function (anonymous)] + error: 'afterEach' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + ... + 1..2 +not ok 10 - afterEach throws and test fails + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'subtestsFailed' + message: '2 subtests failed' + toString: [Function (anonymous)] + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: test hooks + # Subtest: 1 + ok 1 - 1 + --- + duration_ms: * + ... + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + # Subtest: nested + # Subtest: nested 1 + ok 1 - nested 1 + --- + duration_ms: * + ... + # Subtest: nested 2 + ok 2 - nested 2 + --- + duration_ms: * + ... + 1..2 + ok 3 - nested + --- + duration_ms: * + ... + 1..3 +ok 11 - test hooks + --- + duration_ms: * + ... +# Subtest: test hooks - no subtests +ok 12 - test hooks - no subtests + --- + duration_ms: * + ... +# Subtest: t.before throws + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):11' + failureType: 'hookFailed' + message: 'failed running before hook' + toString: [Function (anonymous)] + error: 'before' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + * + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):11' + failureType: 'hookFailed' + message: 'failed running before hook' + toString: [Function (anonymous)] + error: 'before' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + * + ... + 1..2 +not ok 13 - t.before throws + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'before' + toString: [Function (anonymous)] + error: 'before' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + * + ... +# Subtest: t.before throws - no subtests +not ok 14 - t.before throws - no subtests + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'before' + toString: [Function (anonymous)] + error: 'before' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + * + ... +# Subtest: t.after throws + # Subtest: 1 + ok 1 - 1 + --- + duration_ms: * + ... + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + 1..2 +not ok 15 - t.after throws + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'hookFailed' + message: 'failed running after hook' + toString: [Function (anonymous)] + error: 'after' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... +# Subtest: t.after throws - no subtests +not ok 16 - t.after throws - no subtests + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'hookFailed' + message: 'failed running after hook' + toString: [Function (anonymous)] + error: 'after' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... +# Subtest: t.beforeEach throws + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):11' + failureType: 'hookFailed' + message: 'failed running beforeEach hook' + toString: [Function (anonymous)] + error: 'beforeEach' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):11' + failureType: 'hookFailed' + message: 'failed running beforeEach hook' + toString: [Function (anonymous)] + error: 'beforeEach' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... + 1..2 +not ok 17 - t.beforeEach throws + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'subtestsFailed' + message: '2 subtests failed' + toString: [Function (anonymous)] + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: t.afterEach throws + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):11' + failureType: 'hookFailed' + message: 'failed running afterEach hook' + toString: [Function (anonymous)] + error: 'afterEach' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + * + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):11' + failureType: 'hookFailed' + message: 'failed running afterEach hook' + toString: [Function (anonymous)] + error: 'afterEach' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + * + ... + 1..2 +not ok 18 - t.afterEach throws + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'subtestsFailed' + message: '2 subtests failed' + toString: [Function (anonymous)] + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: afterEach when test fails + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):11' + failureType: 'testCodeFailure' + message: 'test' + toString: [Function (anonymous)] + error: 'test' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + ... + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + 1..2 +not ok 19 - afterEach when test fails + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'subtestsFailed' + message: '1 subtest failed' + toString: [Function (anonymous)] + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: afterEach context when test passes + # Subtest: 1 + ok 1 - 1 + --- + duration_ms: * + ... + 1..1 +ok 20 - afterEach context when test passes + --- + duration_ms: * + ... +# Subtest: afterEach context when test fails + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):11' + failureType: 'testCodeFailure' + message: 'test' + toString: [Function (anonymous)] + error: 'test' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + ... + 1..1 +not ok 21 - afterEach context when test fails + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'subtestsFailed' + message: '1 subtest failed' + toString: [Function (anonymous)] + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: afterEach throws and test fails + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):11' + failureType: 'testCodeFailure' + message: 'test' + toString: [Function (anonymous)] + error: 'test' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):11' + failureType: 'hookFailed' + message: 'failed running afterEach hook' + toString: [Function (anonymous)] + error: 'afterEach' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + * + ... + 1..2 +not ok 22 - afterEach throws and test fails + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'subtestsFailed' + message: '2 subtests failed' + toString: [Function (anonymous)] + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: t.after() is called if test body throws +not ok 23 - t.after() is called if test body throws + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'bye' + toString: [Function (anonymous)] + error: 'bye' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + ... +# - after() called +# Subtest: run after when before throws + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):3' + failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + 1..1 +not ok 24 - run after when before throws + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/hooks.js:(LINE):1' + failureType: 'hookFailed' + message: 'failed running before hook' + toString: [Function (anonymous)] + error: 'before' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + ... +# Subtest: test hooks - async + # Subtest: 1 + ok 1 - 1 + --- + duration_ms: * + ... + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + 1..2 +ok 25 - test hooks - async + --- + duration_ms: * + ... +1..25 +# before 1 called +# before 2 called +# after 1 called +# after 2 called +# tests 52 +# suites 12 +# pass 22 +# fail 27 +# cancelled 3 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/hooks_spec_reporter.js b/test/fixtures/test-runner/output/hooks_spec_reporter.js new file mode 100644 index 0000000..75bb4b6 --- /dev/null +++ b/test/fixtures/test-runner/output/hooks_spec_reporter.js @@ -0,0 +1,11 @@ +'use strict'; +require('../../../common'); +const fixtures = require('../../../common/fixtures'); +const spawn = require('node:child_process').spawn; + +const child = spawn(process.execPath, + ['--no-warnings', '--test-reporter', 'spec', fixtures.path('test-runner/output/hooks.js')], + { stdio: 'pipe' }); +// eslint-disable-next-line no-control-regex +child.stdout.on('data', (d) => process.stdout.write(d.toString().replace(/[^\x00-\x7F]/g, '').replace(/\u001b\[\d+m/g, ''))); +child.stderr.pipe(process.stderr); diff --git a/test/fixtures/test-runner/output/hooks_spec_reporter.snapshot b/test/fixtures/test-runner/output/hooks_spec_reporter.snapshot new file mode 100644 index 0000000..ae84d29 --- /dev/null +++ b/test/fixtures/test-runner/output/hooks_spec_reporter.snapshot @@ -0,0 +1,791 @@ +- after() called + describe hooks + 1 (*ms) + 2 (*ms) + nested + nested 1 (*ms) + nested 2 (*ms) + nested (*ms) + describe hooks (*ms) + describe hooks - no subtests (*ms) + before throws + 1 + 'test did not finish before its parent and was cancelled' + + 2 + 'test did not finish before its parent and was cancelled' + + before throws (*ms) + + Error: before + * + * + * + * + * + * + * + * + * + + before throws - no subtests (*ms) + Error: before + * + * + * + * + * + * + * + * + * + + after throws + 1 (*ms) + 2 (*ms) + after throws (*ms) + + Error: after + * + * + * + * + * + * + * + * + * + + after throws - no subtests (*ms) + Error: after + * + * + * + * + * + * + * + * + * + + beforeEach throws + 1 (*ms) + Error: beforeEach + * + * + * + * + * + * + * + at async Promise.all (index 0) + * + * + + 2 (*ms) + Error: beforeEach + * + * + * + * + * + * + * + * + + beforeEach throws (*ms) + afterEach throws + 1 (*ms) + Error: afterEach + * + * + * + * + * + * + * + * + at async Promise.all (index 0) + * + * + + 2 (*ms) + Error: afterEach + * + * + * + * + * + * + * + * + * + + afterEach throws (*ms) + afterEach when test fails + 1 (*ms) + Error: test + * + * + * + * + * + at Array.map () + * + * + * + + 2 (*ms) + afterEach when test fails (*ms) + afterEach throws and test fails + 1 (*ms) + Error: test + * + * + * + * + * + at Array.map () + * + * + * + + 2 (*ms) + Error: afterEach + * + * + * + * + * + * + * + * + * + + afterEach throws and test fails (*ms) + test hooks + 1 (*ms) + 2 (*ms) + nested + nested 1 (*ms) + nested 2 (*ms) + nested (*ms) + test hooks (*ms) + test hooks - no subtests (*ms) + t.before throws + 1 (*ms) + Error: before + * + * + * + * + * + * + * + * + * + * + * + + 2 (*ms) + Error: before + * + * + * + * + * + * + * + * + * + * + * + + t.before throws (*ms) + + Error: before + * + * + * + * + * + * + * + * + * + * + * + + t.before throws - no subtests (*ms) + Error: before + * + * + * + * + * + * + * + * + * + * + * + + t.after throws + 1 (*ms) + 2 (*ms) + t.after throws (*ms) + + Error: after + * + * + * + * + * + * + * + * + * + * + + t.after throws - no subtests (*ms) + Error: after + * + * + * + * + * + * + * + * + * + * + + t.beforeEach throws + 1 (*ms) + Error: beforeEach + * + * + * + * + * + * + * + * + * + * + + 2 (*ms) + Error: beforeEach + * + * + * + * + * + * + * + * + * + * + + t.beforeEach throws (*ms) + t.afterEach throws + 1 (*ms) + Error: afterEach + * + * + * + * + * + * + * + * + * + * + * + + 2 (*ms) + Error: afterEach + * + * + * + * + * + * + * + * + * + * + * + + t.afterEach throws (*ms) + afterEach when test fails + 1 (*ms) + Error: test + * + * + * + * + * + * + * + * + * + + 2 (*ms) + afterEach when test fails (*ms) + afterEach context when test passes + 1 (*ms) + afterEach context when test passes (*ms) + afterEach context when test fails + 1 (*ms) + Error: test + * + * + * + * + + afterEach context when test fails (*ms) + afterEach throws and test fails + 1 (*ms) + Error: test + * + * + * + * + * + * + * + * + * + + 2 (*ms) + Error: afterEach + * + * + * + * + * + * + * + * + * + * + * + + afterEach throws and test fails (*ms) + t.after() is called if test body throws (*ms) + Error: bye + * + * + * + * + + - after() called + run after when before throws + 1 + 'test did not finish before its parent and was cancelled' + + run after when before throws (*ms) + + Error: before + * + * + * + * + * + * + * + * + * + + test hooks - async + 1 (*ms) + 2 (*ms) + test hooks - async (*ms) + before 1 called + before 2 called + after 1 called + after 2 called + tests 52 + suites 12 + pass 22 + fail 27 + cancelled 3 + skipped 0 + todo 0 + duration_ms * + + failing tests: + +* + 1 + 'test did not finish before its parent and was cancelled' + +* + 2 + 'test did not finish before its parent and was cancelled' + +* + before throws (*ms) + Error: before + * + * + * + * + * + * + * + * + * + +* + before throws - no subtests (*ms) + Error: before + * + * + * + * + * + * + * + * + * + +* + after throws (*ms) + Error: after + * + * + * + * + * + * + * + * + * + +* + after throws - no subtests (*ms) + Error: after + * + * + * + * + * + * + * + * + * + +* + 1 (*ms) + Error: beforeEach + * + * + * + * + * + * + * + at async Promise.all (index 0) + * + * + +* + 2 (*ms) + Error: beforeEach + * + * + * + * + * + * + * + * + +* + 1 (*ms) + Error: afterEach + * + * + * + * + * + * + * + * + at async Promise.all (index 0) + * + * + +* + 2 (*ms) + Error: afterEach + * + * + * + * + * + * + * + * + * + +* + 1 (*ms) + Error: test + * + * + * + * + * + at Array.map () + * + * + * + +* + 1 (*ms) + Error: test + * + * + * + * + * + at Array.map () + * + * + * + +* + 2 (*ms) + Error: afterEach + * + * + * + * + * + * + * + * + * + +* + 1 (*ms) + Error: before + * + * + * + * + * + * + * + * + * + * + * + +* + 2 (*ms) + Error: before + * + * + * + * + * + * + * + * + * + * + * + +* + t.before throws (*ms) + Error: before + * + * + * + * + * + * + * + * + * + * + * + +* + t.before throws - no subtests (*ms) + Error: before + * + * + * + * + * + * + * + * + * + * + * + +* + t.after throws (*ms) + Error: after + * + * + * + * + * + * + * + * + * + * + +* + t.after throws - no subtests (*ms) + Error: after + * + * + * + * + * + * + * + * + * + * + +* + 1 (*ms) + Error: beforeEach + * + * + * + * + * + * + * + * + * + * + +* + 2 (*ms) + Error: beforeEach + * + * + * + * + * + * + * + * + * + * + +* + 1 (*ms) + Error: afterEach + * + * + * + * + * + * + * + * + * + * + * + +* + 2 (*ms) + Error: afterEach + * + * + * + * + * + * + * + * + * + * + * + +* + 1 (*ms) + Error: test + * + * + * + * + * + * + * + * + * + +* + 1 (*ms) + Error: test + * + * + * + * + +* + 1 (*ms) + Error: test + * + * + * + * + * + * + * + * + * + +* + 2 (*ms) + Error: afterEach + * + * + * + * + * + * + * + * + * + * + * + +* + t.after() is called if test body throws (*ms) + Error: bye + * + * + * + * + +* + 1 + 'test did not finish before its parent and was cancelled' + +* + run after when before throws (*ms) + Error: before + * + * + * + * + * + * + * + * + * diff --git a/test/fixtures/test-runner/output/lcov_reporter.js b/test/fixtures/test-runner/output/lcov_reporter.js new file mode 100644 index 0000000..a6d1743 --- /dev/null +++ b/test/fixtures/test-runner/output/lcov_reporter.js @@ -0,0 +1,7 @@ +'use strict'; +require('../../../common'); +const fixtures = require('../../../common/fixtures'); +const spawn = require('node:child_process').spawn; + +spawn(process.execPath, + ['--no-warnings', '--experimental-test-coverage', '--test-reporter', 'lcov', fixtures.path('test-runner/output/output.js')], { stdio: 'inherit' }); diff --git a/test/fixtures/test-runner/output/lcov_reporter.snapshot b/test/fixtures/test-runner/output/lcov_reporter.snapshot new file mode 100644 index 0000000..dce6ead --- /dev/null +++ b/test/fixtures/test-runner/output/lcov_reporter.snapshot @@ -0,0 +1,2 @@ +/lib/internal/validators.js:6 + diff --git a/test/fixtures/test-runner/output/name_and_skip_patterns.js b/test/fixtures/test-runner/output/name_and_skip_patterns.js new file mode 100644 index 0000000..80dda19 --- /dev/null +++ b/test/fixtures/test-runner/output/name_and_skip_patterns.js @@ -0,0 +1,10 @@ +// Flags: --test-skip-pattern=disabled --test-name-pattern=enabled +'use strict'; +const common = require('../../../common'); +const { + test, +} = require('#node:test'); + +test('disabled', common.mustNotCall()); +test('enabled', common.mustCall()); +test('enabled disabled', common.mustNotCall()); diff --git a/test/fixtures/test-runner/output/name_and_skip_patterns.snapshot b/test/fixtures/test-runner/output/name_and_skip_patterns.snapshot new file mode 100644 index 0000000..d5fd874 --- /dev/null +++ b/test/fixtures/test-runner/output/name_and_skip_patterns.snapshot @@ -0,0 +1,15 @@ +TAP version 13 +# Subtest: enabled +ok 1 - enabled + --- + duration_ms: * + ... +1..1 +# tests 1 +# suites 0 +# pass 1 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/name_pattern.js b/test/fixtures/test-runner/output/name_pattern.js new file mode 100644 index 0000000..c83f9c4 --- /dev/null +++ b/test/fixtures/test-runner/output/name_pattern.js @@ -0,0 +1,89 @@ +// Flags: --test-name-pattern=enabled --test-name-pattern=yes --test-name-pattern=/pattern/i --test-name-pattern=/^DescribeForMatchWithAncestors\sNestedDescribeForMatchWithAncestors\sNestedTest$/ +'use strict'; +const common = require('../../../common'); +const { + after, + afterEach, + before, + beforeEach, + describe, + it, + test, +} = require('#node:test'); + +test('top level test disabled', common.mustNotCall()); +test('top level skipped test disabled', { skip: true }, common.mustNotCall()); +test('top level skipped test enabled', { skip: true }, common.mustNotCall()); +it('top level it enabled', common.mustCall()); +it('top level it disabled', common.mustNotCall()); +it.skip('top level skipped it disabled', common.mustNotCall()); +it.skip('top level skipped it enabled', common.mustNotCall()); +describe('top level describe never disabled', common.mustCall()); +describe.skip('top level skipped describe disabled', common.mustNotCall()); +describe.skip('top level skipped describe enabled', common.mustNotCall()); +test('top level runs because name includes PaTtErN', common.mustCall()); + +test('top level test enabled', common.mustCall(async (t) => { + t.beforeEach(common.mustCall()); + t.afterEach(common.mustCall()); + await t.test( + 'nested test runs because name includes PATTERN', + common.mustCall(), + ); +})); + +describe('top level describe enabled', () => { + before(common.mustCall()); + beforeEach(common.mustCall(3)); + afterEach(common.mustCall(3)); + after(common.mustCall()); + + it('nested it not disabled', common.mustCall()); + it('nested it enabled', common.mustCall()); + describe('nested describe not disabled', common.mustCall()); + describe('nested describe enabled', common.mustCall(() => { + it('is enabled', common.mustCall()); + })); +}); + +describe('yes', function() { + it('no', () => {}); + it('yes', () => {}); + + describe('maybe', function() { + it('no', () => {}); + it('yes', () => {}); + }); +}); + +describe('no', function() { + it('no', () => {}); + it('yes', () => {}); + + describe('maybe', function() { + it('no', () => {}); + it('yes', () => {}); + }); +}); + +describe('no with todo', { todo: true }, () => { + it('no', () => {}); + it('yes', () => {}); + + describe('maybe', function() { + it('no', () => {}); + it('yes', () => {}); + }); +}); + +describe('DescribeForMatchWithAncestors', () => { + it('NestedTest', () => common.mustNotCall()); + + describe('NestedDescribeForMatchWithAncestors', () => { + it('NestedTest', common.mustCall()); + }); +}) + +describe('DescribeForMatchWithAncestors', () => { + it('NestedTest', () => common.mustNotCall()); +}) diff --git a/test/fixtures/test-runner/output/name_pattern.snapshot b/test/fixtures/test-runner/output/name_pattern.snapshot new file mode 100644 index 0000000..0b13848 --- /dev/null +++ b/test/fixtures/test-runner/output/name_pattern.snapshot @@ -0,0 +1,183 @@ +TAP version 13 +# Subtest: top level skipped test enabled +ok 1 - top level skipped test enabled # SKIP + --- + duration_ms: * + ... +# Subtest: top level it enabled +ok 2 - top level it enabled + --- + duration_ms: * + ... +# Subtest: top level skipped it enabled +ok 3 - top level skipped it enabled # SKIP + --- + duration_ms: * + ... +# Subtest: top level skipped describe enabled +ok 4 - top level skipped describe enabled # SKIP + --- + duration_ms: * + type: 'suite' + ... +# Subtest: top level runs because name includes PaTtErN +ok 5 - top level runs because name includes PaTtErN + --- + duration_ms: * + ... +# Subtest: top level test enabled + # Subtest: nested test runs because name includes PATTERN + ok 1 - nested test runs because name includes PATTERN + --- + duration_ms: * + ... + 1..1 +ok 6 - top level test enabled + --- + duration_ms: * + ... +# Subtest: top level describe enabled + # Subtest: nested it not disabled + ok 1 - nested it not disabled + --- + duration_ms: * + ... + # Subtest: nested it enabled + ok 2 - nested it enabled + --- + duration_ms: * + ... + # Subtest: nested describe not disabled + ok 3 - nested describe not disabled + --- + duration_ms: * + type: 'suite' + ... + # Subtest: nested describe enabled + # Subtest: is enabled + ok 1 - is enabled + --- + duration_ms: * + ... + 1..1 + ok 4 - nested describe enabled + --- + duration_ms: * + type: 'suite' + ... + 1..4 +ok 7 - top level describe enabled + --- + duration_ms: * + type: 'suite' + ... +# Subtest: yes + # Subtest: no + ok 1 - no + --- + duration_ms: * + ... + # Subtest: yes + ok 2 - yes + --- + duration_ms: * + ... + # Subtest: maybe + # Subtest: no + ok 1 - no + --- + duration_ms: * + ... + # Subtest: yes + ok 2 - yes + --- + duration_ms: * + ... + 1..2 + ok 3 - maybe + --- + duration_ms: * + type: 'suite' + ... + 1..3 +ok 8 - yes + --- + duration_ms: * + type: 'suite' + ... +# Subtest: no + # Subtest: yes + ok 1 - yes + --- + duration_ms: * + ... + # Subtest: maybe + # Subtest: yes + ok 1 - yes + --- + duration_ms: * + ... + 1..1 + ok 2 - maybe + --- + duration_ms: * + type: 'suite' + ... + 1..2 +ok 9 - no + --- + duration_ms: * + type: 'suite' + ... +# Subtest: no with todo + # Subtest: yes + ok 1 - yes + --- + duration_ms: * + ... + # Subtest: maybe + # Subtest: yes + ok 1 - yes + --- + duration_ms: * + ... + 1..1 + ok 2 - maybe + --- + duration_ms: * + type: 'suite' + ... + 1..2 +ok 10 - no with todo # TODO + --- + duration_ms: * + type: 'suite' + ... +# Subtest: DescribeForMatchWithAncestors + # Subtest: NestedDescribeForMatchWithAncestors + # Subtest: NestedTest + ok 1 - NestedTest + --- + duration_ms: * + ... + 1..1 + ok 1 - NestedDescribeForMatchWithAncestors + --- + duration_ms: * + type: 'suite' + ... + 1..1 +ok 11 - DescribeForMatchWithAncestors + --- + duration_ms: * + type: 'suite' + ... +1..11 +# tests 18 +# suites 12 +# pass 16 +# fail 0 +# cancelled 0 +# skipped 2 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/name_pattern_with_only.js b/test/fixtures/test-runner/output/name_pattern_with_only.js new file mode 100644 index 0000000..d772461 --- /dev/null +++ b/test/fixtures/test-runner/output/name_pattern_with_only.js @@ -0,0 +1,13 @@ +// Flags: --test-only --test-name-pattern=enabled +'use strict'; +const common = require('../../../common'); +const { test } = require('#node:test'); + +test('enabled and only', { only: true }, common.mustCall(async (t) => { + await t.test('enabled', common.mustCall()); + await t.test('disabled but parent not', common.mustCall()); +})); + +test('enabled but not only', common.mustNotCall()); +test('only does not match pattern', { only: true }, common.mustNotCall()); +test('not only and does not match pattern', common.mustNotCall()); diff --git a/test/fixtures/test-runner/output/name_pattern_with_only.snapshot b/test/fixtures/test-runner/output/name_pattern_with_only.snapshot new file mode 100644 index 0000000..64a390d --- /dev/null +++ b/test/fixtures/test-runner/output/name_pattern_with_only.snapshot @@ -0,0 +1,26 @@ +TAP version 13 +# Subtest: enabled and only + # Subtest: enabled + ok 1 - enabled + --- + duration_ms: * + ... + # Subtest: disabled but parent not + ok 2 - disabled but parent not + --- + duration_ms: * + ... + 1..2 +ok 1 - enabled and only + --- + duration_ms: * + ... +1..1 +# tests 3 +# suites 0 +# pass 3 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/no_refs.js b/test/fixtures/test-runner/output/no_refs.js new file mode 100644 index 0000000..f3675d0 --- /dev/null +++ b/test/fixtures/test-runner/output/no_refs.js @@ -0,0 +1,12 @@ +'use strict'; +require('../../../common'); +const test = require('#node:test'); + +// When run alone, the test below does not keep the event loop alive. +test('does not keep event loop alive', async (t) => { + await t.test('+does not keep event loop alive', async (t) => { + return new Promise((resolve) => { + setTimeout(resolve, 1000).unref(); + }); + }); +}); diff --git a/test/message/test_runner_no_refs.out b/test/fixtures/test-runner/output/no_refs.snapshot similarity index 58% rename from test/message/test_runner_no_refs.out rename to test/fixtures/test-runner/output/no_refs.snapshot index e8560c5..9325b1b 100644 --- a/test/message/test_runner_no_refs.out +++ b/test/fixtures/test-runner/output/no_refs.snapshot @@ -4,27 +4,40 @@ TAP version 13 not ok 1 - +does not keep event loop alive --- duration_ms: * + location: '/test/fixtures/test-runner/output/no_refs.js:(LINE):11' failureType: 'cancelledByParent' + message: 'Promise resolution is still pending but the event loop has already resolved' + toString: [Function (anonymous)] error: 'Promise resolution is still pending but the event loop has already resolved' code: 'ERR_TEST_FAILURE' stack: |- * + * + * + * ... 1..1 not ok 1 - does not keep event loop alive --- duration_ms: * + location: '/test/fixtures/test-runner/output/no_refs.js:(LINE):1' failureType: 'cancelledByParent' + message: 'Promise resolution is still pending but the event loop has already resolved' + toString: [Function (anonymous)] error: 'Promise resolution is still pending but the event loop has already resolved' code: 'ERR_TEST_FAILURE' stack: |- * + * + * + * ... 1..1 -# tests 1 +# tests 2 +# suites 0 # pass 0 # fail 0 -# cancelled 1 +# cancelled 2 # skipped 0 # todo 0 # duration_ms * diff --git a/test/fixtures/test-runner/output/no_tests.js b/test/fixtures/test-runner/output/no_tests.js new file mode 100644 index 0000000..d340457 --- /dev/null +++ b/test/fixtures/test-runner/output/no_tests.js @@ -0,0 +1,6 @@ +'use strict'; +require('../../../common'); +const test = require('#node:test'); + +// No TAP output should be generated. +console.log(test.name); diff --git a/test/message/test_runner_no_tests.out b/test/fixtures/test-runner/output/no_tests.snapshot similarity index 100% rename from test/message/test_runner_no_tests.out rename to test/fixtures/test-runner/output/no_tests.snapshot diff --git a/test/fixtures/test-runner/output/non-tty-forced-color-output.js b/test/fixtures/test-runner/output/non-tty-forced-color-output.js new file mode 100644 index 0000000..ebbfb27 --- /dev/null +++ b/test/fixtures/test-runner/output/non-tty-forced-color-output.js @@ -0,0 +1,6 @@ +'use strict'; + +process.env.FORCE_COLOR = 1; + +const test = require('#node:test'); +test('passing test', () => {}); diff --git a/test/fixtures/test-runner/output/non-tty-forced-color-output.snapshot b/test/fixtures/test-runner/output/non-tty-forced-color-output.snapshot new file mode 100644 index 0000000..3d1f593 --- /dev/null +++ b/test/fixtures/test-runner/output/non-tty-forced-color-output.snapshot @@ -0,0 +1,9 @@ +✔ passing test (*ms) +ℹ tests 1 +ℹ suites 0 +ℹ pass 1 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms * diff --git a/test/fixtures/test-runner/output/only_tests.js b/test/fixtures/test-runner/output/only_tests.js new file mode 100644 index 0000000..5258c42 --- /dev/null +++ b/test/fixtures/test-runner/output/only_tests.js @@ -0,0 +1,142 @@ +// Flags: --test-only +'use strict'; +const common = require('../../../common'); +const { test, describe, it } = require('#node:test'); + +// These tests should be skipped based on the 'only' option. +test('only = undefined', common.mustNotCall()); +test('only = undefined, skip = string', { skip: 'skip message' }, common.mustNotCall()); +test('only = undefined, skip = true', { skip: true }, common.mustNotCall()); +test('only = undefined, skip = false', { skip: false }, common.mustNotCall()); +test('only = false', { only: false }, common.mustNotCall()); +test('only = false, skip = string', { only: false, skip: 'skip message' }, common.mustNotCall()); +test('only = false, skip = true', { only: false, skip: true }, common.mustNotCall()); +test('only = false, skip = false', { only: false, skip: false }, common.mustNotCall()); + +// These tests should be skipped based on the 'skip' option. +test('only = true, skip = string', { only: true, skip: 'skip message' }, common.mustNotCall()); +test('only = true, skip = true', { only: true, skip: true }, common.mustNotCall()); + +// An 'only' test with subtests. +test('only = true, with subtests', { only: true }, common.mustCall(async (t) => { + // These subtests should run. + await t.test('running subtest 1', common.mustCall()); + await t.test('running subtest 2', common.mustCall()); + + // Switch the context to only execute 'only' tests. + t.runOnly(true); + await t.test('skipped subtest 1', common.mustNotCall()); + await t.test('skipped subtest 2'), common.mustNotCall(); + await t.test('running subtest 3', { only: true }, common.mustCall()); + + // Switch the context back to execute all tests. + t.runOnly(false); + await t.test('running subtest 4', common.mustCall(async (t) => { + // These subtests should run. + await t.test('running sub-subtest 1', common.mustCall()); + await t.test('running sub-subtest 2', common.mustCall()); + + // Switch the context to only execute 'only' tests. + t.runOnly(true); + await t.test('skipped sub-subtest 1', common.mustNotCall()); + await t.test('skipped sub-subtest 2', common.mustNotCall()); + })); + + // Explicitly do not run these tests. + await t.test('skipped subtest 3', { only: false }, common.mustNotCall()); + await t.test('skipped subtest 4', { skip: true }, common.mustNotCall()); +})); + +describe.only('describe only = true, with subtests', common.mustCall(() => { + it.only('`it` subtest 1 should run', common.mustCall()); + + it('`it` subtest 2 should not run', common.mustNotCall()); +})); + +describe.only('describe only = true, with a mixture of subtests', common.mustCall(() => { + it.only('`it` subtest 1', common.mustCall()); + + it.only('`it` async subtest 1', common.mustCall(async () => {})); + + it('`it` subtest 2 only=true', { only: true }, common.mustCall()); + + it('`it` subtest 2 only=false', { only: false }, common.mustNotCall()); + + it.skip('`it` subtest 3 skip', common.mustNotCall()); + + it.todo('`it` subtest 4 todo', { only: false }, common.mustNotCall()); + + test.only('`test` subtest 1', common.mustCall()); + + test.only('`test` async subtest 1', common.mustCall(async () => {})); + + test('`test` subtest 2 only=true', { only: true }, common.mustCall()); + + test('`test` subtest 2 only=false', { only: false }, common.mustNotCall()); + + test.skip('`test` subtest 3 skip', common.mustNotCall()); + + test.todo('`test` subtest 4 todo', { only: false }, common.mustNotCall()); +})); + +describe.only('describe only = true, with subtests', common.mustCall(() => { + test.only('subtest should run', common.mustCall()); + + test('async subtest should not run', common.mustNotCall()); + + test('subtest should be skipped', { only: false }, common.mustNotCall()); +})); + + +describe('describe only = undefined, with nested only subtest', common.mustCall(() => { + test('subtest should not run', common.mustNotCall()); + describe('nested describe', common.mustCall(() => { + test('subtest should not run', common.mustNotCall()); + test.only('nested test should run', common.mustCall()); + })); +})); + + +describe('describe only = undefined, with subtests', common.mustCall(() => { + test('async subtest should not run', common.mustNotCall()); +})); + +describe('describe only = false, with subtests', { only: false }, common.mustNotCall(() => { + test('async subtest should not run', common.mustNotCall()); +})); + + +describe.only('describe only = true, with nested subtests', common.mustCall(() => { + test('async subtest should run', common.mustCall()); + describe('nested describe', common.mustCall(() => { + test('nested test should run', common.mustCall()); + })); +})); + +describe('describe only = false, with nested only subtests', { only: false }, common.mustNotCall(() => { + test('async subtest should not run', common.mustNotCall()); + describe('nested describe', common.mustNotCall(() => { + test.only('nested test should run', common.mustNotCall()); + })); +})); + +test('nested tests with run only',{only: true}, common.mustCall(async (t) => { + // Within this test, all subtests are run by default. + await t.test('running subtest - 1'); + + // The test context can be updated to run subtests with the 'only' option. + await t.runOnly(true); + await t.test('this subtest is now skipped - 2', common.mustNotCall()); + await t.test('this subtest is run - 3', { only: true }, common.mustCall(async (t) => { + await t.test('this subtest is run - 4', common.mustCall()); + await t.test('this subtest is run - 5', { only: true }, common.mustCall()); + })); + + // Switch the context back to execute all tests. + await t.runOnly(false); + await t.test('this subtest is now run - 6'); + + // Explicitly do not run these tests. + await t.test('skipped subtest - 7', { only: false }, common.mustNotCall()); + await t.test('skipped subtest - 8', { skip: true }, common.mustNotCall()); +})) diff --git a/test/fixtures/test-runner/output/only_tests.snapshot b/test/fixtures/test-runner/output/only_tests.snapshot new file mode 100644 index 0000000..25e2e9b --- /dev/null +++ b/test/fixtures/test-runner/output/only_tests.snapshot @@ -0,0 +1,203 @@ +TAP version 13 +# Subtest: only = true, skip = string +ok 1 - only = true, skip = string # SKIP skip message + --- + duration_ms: * + ... +# Subtest: only = true, skip = true +ok 2 - only = true, skip = true # SKIP + --- + duration_ms: * + ... +# Subtest: only = true, with subtests + # Subtest: running subtest 1 + ok 1 - running subtest 1 + --- + duration_ms: * + ... + # Subtest: running subtest 2 + ok 2 - running subtest 2 + --- + duration_ms: * + ... + # Subtest: running subtest 3 + ok 3 - running subtest 3 + --- + duration_ms: * + ... + # Subtest: running subtest 4 + # Subtest: running sub-subtest 1 + ok 1 - running sub-subtest 1 + --- + duration_ms: * + ... + # Subtest: running sub-subtest 2 + ok 2 - running sub-subtest 2 + --- + duration_ms: * + ... + 1..2 + ok 4 - running subtest 4 + --- + duration_ms: * + ... + # Subtest: skipped subtest 4 + ok 5 - skipped subtest 4 # SKIP + --- + duration_ms: * + ... + 1..5 +ok 3 - only = true, with subtests + --- + duration_ms: * + ... +# Subtest: describe only = true, with subtests + # Subtest: `it` subtest 1 should run + ok 1 - `it` subtest 1 should run + --- + duration_ms: * + ... + 1..1 +ok 4 - describe only = true, with subtests + --- + duration_ms: * + type: 'suite' + ... +# Subtest: describe only = true, with a mixture of subtests + # Subtest: `it` subtest 1 + ok 1 - `it` subtest 1 + --- + duration_ms: * + ... + # Subtest: `it` async subtest 1 + ok 2 - `it` async subtest 1 + --- + duration_ms: * + ... + # Subtest: `it` subtest 2 only=true + ok 3 - `it` subtest 2 only=true + --- + duration_ms: * + ... + # Subtest: `test` subtest 1 + ok 4 - `test` subtest 1 + --- + duration_ms: * + ... + # Subtest: `test` async subtest 1 + ok 5 - `test` async subtest 1 + --- + duration_ms: * + ... + # Subtest: `test` subtest 2 only=true + ok 6 - `test` subtest 2 only=true + --- + duration_ms: * + ... + 1..6 +ok 5 - describe only = true, with a mixture of subtests + --- + duration_ms: * + type: 'suite' + ... +# Subtest: describe only = true, with subtests + # Subtest: subtest should run + ok 1 - subtest should run + --- + duration_ms: * + ... + 1..1 +ok 6 - describe only = true, with subtests + --- + duration_ms: * + type: 'suite' + ... +# Subtest: describe only = undefined, with nested only subtest + # Subtest: nested describe + # Subtest: nested test should run + ok 1 - nested test should run + --- + duration_ms: * + ... + 1..1 + ok 1 - nested describe + --- + duration_ms: * + type: 'suite' + ... + 1..1 +ok 7 - describe only = undefined, with nested only subtest + --- + duration_ms: * + type: 'suite' + ... +# Subtest: describe only = true, with nested subtests + # Subtest: async subtest should run + ok 1 - async subtest should run + --- + duration_ms: * + ... + # Subtest: nested describe + # Subtest: nested test should run + ok 1 - nested test should run + --- + duration_ms: * + ... + 1..1 + ok 2 - nested describe + --- + duration_ms: * + type: 'suite' + ... + 1..2 +ok 8 - describe only = true, with nested subtests + --- + duration_ms: * + type: 'suite' + ... +# Subtest: nested tests with run only + # Subtest: running subtest - 1 + ok 1 - running subtest - 1 + --- + duration_ms: * + ... + # Subtest: this subtest is run - 3 + # Subtest: this subtest is run - 4 + ok 1 - this subtest is run - 4 + --- + duration_ms: * + ... + # Subtest: this subtest is run - 5 + ok 2 - this subtest is run - 5 + --- + duration_ms: * + ... + 1..2 + ok 2 - this subtest is run - 3 + --- + duration_ms: * + ... + # Subtest: this subtest is now run - 6 + ok 3 - this subtest is now run - 6 + --- + duration_ms: * + ... + # Subtest: skipped subtest - 8 + ok 4 - skipped subtest - 8 # SKIP + --- + duration_ms: * + ... + 1..4 +ok 9 - nested tests with run only + --- + duration_ms: * + ... +1..9 +# tests 28 +# suites 7 +# pass 24 +# fail 0 +# cancelled 0 +# skipped 4 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/output.js b/test/fixtures/test-runner/output/output.js new file mode 100644 index 0000000..f9faca0 --- /dev/null +++ b/test/fixtures/test-runner/output/output.js @@ -0,0 +1,405 @@ +// Flags: --no-warnings +'use strict'; +require('../../../common'); +const assert = require('node:assert'); +const test = require('#node:test'); +const util = require('util'); + +test('sync pass todo', (t) => { + t.todo(); +}); + +test('sync pass todo with message', (t) => { + t.todo('this is a passing todo'); +}); + +test('sync fail todo', (t) => { + t.todo(); + throw new Error('thrown from sync fail todo'); +}); + +test('sync fail todo with message', (t) => { + t.todo('this is a failing todo'); + throw new Error('thrown from sync fail todo with message'); +}); + +test('sync skip pass', (t) => { + t.skip(); +}); + +test('sync skip pass with message', (t) => { + t.skip('this is skipped'); +}); + +test('sync pass', (t) => { + t.diagnostic('this test should pass'); +}); + +test('sync throw fail', () => { + throw new Error('thrown from sync throw fail'); +}); + +test('async skip pass', async (t) => { + t.skip(); +}); + +test('async pass', async () => { + +}); + +test('async throw fail', async () => { + throw new Error('thrown from async throw fail'); +}); + +test('async skip fail', async (t) => { + t.skip(); + throw new Error('thrown from async throw fail'); +}); + +test('async assertion fail', async () => { + // Make sure the assert module is handled. + assert.strictEqual(true, false); +}); + +test('resolve pass', () => { + return Promise.resolve(); +}); + +test('reject fail', () => { + return Promise.reject(new Error('rejected from reject fail')); +}); + +test('unhandled rejection - passes but warns', () => { + Promise.reject(new Error('rejected from unhandled rejection fail')); +}); + +test('async unhandled rejection - passes but warns', async () => { + Promise.reject(new Error('rejected from async unhandled rejection fail')); +}); + +test('immediate throw - passes but warns', () => { + setImmediate(() => { + throw new Error('thrown from immediate throw fail'); + }); +}); + +test('immediate reject - passes but warns', () => { + setImmediate(() => { + Promise.reject(new Error('rejected from immediate reject fail')); + }); +}); + +test('immediate resolve pass', () => { + return new Promise((resolve) => { + setImmediate(() => { + resolve(); + }); + }); +}); + +test('subtest sync throw fail', async (t) => { + await t.test('+sync throw fail', (t) => { + t.diagnostic('this subtest should make its parent test fail'); + throw new Error('thrown from subtest sync throw fail'); + }); +}); + +test('sync throw non-error fail', async (t) => { + throw Symbol('thrown symbol from sync throw non-error fail'); +}); + +test('level 0a', { concurrency: 4 }, async (t) => { + t.test('level 1a', async (t) => { + const p1a = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 100); + }); + + return p1a; + }); + + test('level 1b', async (t) => { + const p1b = new Promise((resolve) => { + resolve(); + }); + + return p1b; + }); + + t.test('level 1c', async (t) => { + const p1c = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 200); + }); + + return p1c; + }); + + t.test('level 1d', async (t) => { + const p1c = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 150); + }); + + return p1c; + }); + + const p0a = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 300); + }); + + return p0a; +}); + +test('top level', { concurrency: 2 }, async (t) => { + t.test('+long running', async (t) => { + return new Promise((resolve, reject) => { + setTimeout(resolve, 300).unref(); + }); + }); + + t.test('+short running', async (t) => { + t.test('++short running', async (t) => {}); + }); +}); + +test('invalid subtest - pass but subtest fails', (t) => { + setImmediate(() => { + t.test('invalid subtest fail', () => { + throw new Error('this should not be thrown'); + }); + }); +}); + +test('sync skip option', { skip: true }, (t) => { + throw new Error('this should not be executed'); +}); + +test('sync skip option with message', { skip: 'this is skipped' }, (t) => { + throw new Error('this should not be executed'); +}); + +test('sync skip option is false fail', { skip: false }, (t) => { + throw new Error('this should be executed'); +}); + +// A test with no arguments provided. +test(); + +// A test with only a named function provided. +test(function functionOnly() {}); + +// A test with only an anonymous function provided. +test(() => {}); + +// A test with only a name provided. +test('test with only a name provided'); + +// A test with an empty string name. +test(''); + +// A test with only options provided. +test({ skip: true }); + +// A test with only a name and options provided. +test('test with a name and options provided', { skip: true }); + +// A test with only options and a function provided. +test({ skip: true }, function functionAndOptions() {}); + +test('callback pass', (t, done) => { + setImmediate(done); +}); + +test('callback fail', (t, done) => { + setImmediate(() => { + done(new Error('callback failure')); + }); +}); + +test('sync t is this in test', function(t) { + assert.strictEqual(this, t); +}); + +test('async t is this in test', async function(t) { + assert.strictEqual(this, t); +}); + +test('callback t is this in test', function(t, done) { + assert.strictEqual(this, t); + done(); +}); + +test('callback also returns a Promise', async (t, done) => { + throw new Error('thrown from callback also returns a Promise'); +}); + +test('callback throw', (t, done) => { + throw new Error('thrown from callback throw'); +}); + +test('callback called twice', (t, done) => { + done(); + done(); +}); + +test('callback called twice in different ticks', (t, done) => { + setImmediate(done); + done(); +}); + +test('callback called twice in future tick', (t, done) => { + setImmediate(() => { + done(); + done(); + }); +}); + +test('callback async throw', (t, done) => { + setImmediate(() => { + throw new Error('thrown from callback async throw'); + }); +}); + +test('callback async throw after done', (t, done) => { + setImmediate(() => { + throw new Error('thrown from callback async throw after done'); + }); + + done(); +}); + +test('only is set on subtests but not in only mode', async (t) => { + // All of these subtests should run. + await t.test('running subtest 1'); + t.runOnly(true); + await t.test('running subtest 2'); + await t.test('running subtest 3', { only: true }); + t.runOnly(false); + await t.test('running subtest 4'); +}); + +test('custom inspect symbol fail', () => { + const obj = { + [util.inspect.custom]() { + return 'customized'; + }, + foo: 1, + }; + + throw obj; +}); + +test('custom inspect symbol that throws fail', () => { + const obj = { + [util.inspect.custom]() { + throw new Error('bad-inspect'); + }, + foo: 1, + }; + + throw obj; +}); + +test('subtest sync throw fails', async (t) => { + await t.test('sync throw fails at first', (t) => { + throw new Error('thrown from subtest sync throw fails at first'); + }); + await t.test('sync throw fails at second', (t) => { + throw new Error('thrown from subtest sync throw fails at second'); + }); +}); + +test('timed out async test', { timeout: 5 }, async (t) => { + return new Promise((resolve) => { + setTimeout(() => { + // Empty timer so the process doesn't exit before the timeout triggers. + }, 5); + setTimeout(resolve, 30_000_000).unref(); + }); +}); + +test('timed out callback test', { timeout: 5 }, (t, done) => { + setTimeout(() => { + // Empty timer so the process doesn't exit before the timeout triggers. + }, 5); + setTimeout(done, 30_000_000).unref(); +}); + + +test('large timeout async test is ok', { timeout: 30_000_000 }, async (t) => { + return new Promise((resolve) => { + setTimeout(resolve, 10); + }); +}); + +test('large timeout callback test is ok', { timeout: 30_000_000 }, (t, done) => { + setTimeout(done, 10); +}); + +test('successful thenable', () => { + let thenCalled = false; + return { + get then() { + if (thenCalled) throw new Error(); + thenCalled = true; + return (successHandler) => successHandler(); + }, + }; +}); + +test('rejected thenable', () => { + let thenCalled = false; + return { + get then() { + if (thenCalled) throw new Error(); + thenCalled = true; + return (_, errorHandler) => errorHandler('custom error'); + }, + }; +}); + +test('unfinished test with uncaughtException', async () => { + await new Promise(() => { + setTimeout(() => { throw new Error('foo'); }); + }); +}); + +test('unfinished test with unhandledRejection', async () => { + await new Promise(() => { + setTimeout(() => Promise.reject(new Error('bar'))); + }); +}); + +// Verify that uncaught exceptions outside of any tests are handled after the +// test harness has finished bootstrapping itself. +setImmediate(() => { + throw new Error('uncaught from outside of a test'); +}); + +test('assertion errors display actual and expected properly', async () => { + // Make sure the assert module is handled. + const circular = { bar: 2 }; + circular.c = circular; + const tmpLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 1; + const boo = [1]; + const baz = { + date: new Date(0), + null: null, + number: 1, + string: 'Hello', + undefined: undefined, + } + try { + assert.deepEqual({ foo: 1, bar: 1, boo, baz }, { boo, baz, circular }); // eslint-disable-line no-restricted-properties + } catch (err) { + Error.stackTraceLimit = tmpLimit; + throw err; + } +}); diff --git a/test/fixtures/test-runner/output/output.snapshot b/test/fixtures/test-runner/output/output.snapshot new file mode 100644 index 0000000..12a208c --- /dev/null +++ b/test/fixtures/test-runner/output/output.snapshot @@ -0,0 +1,965 @@ +TAP version 13 +# Subtest: sync pass todo +ok 1 - sync pass todo # TODO + --- + duration_ms: * + ... +# Subtest: sync pass todo with message +ok 2 - sync pass todo with message # TODO this is a passing todo + --- + duration_ms: * + ... +# Subtest: sync fail todo +not ok 3 - sync fail todo # TODO + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'thrown from sync fail todo' + toString: [Function (anonymous)] + error: 'thrown from sync fail todo' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: sync fail todo with message +not ok 4 - sync fail todo with message # TODO this is a failing todo + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'thrown from sync fail todo with message' + toString: [Function (anonymous)] + error: 'thrown from sync fail todo with message' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: sync skip pass +ok 5 - sync skip pass # SKIP + --- + duration_ms: * + ... +# Subtest: sync skip pass with message +ok 6 - sync skip pass with message # SKIP this is skipped + --- + duration_ms: * + ... +# Subtest: sync pass +ok 7 - sync pass + --- + duration_ms: * + ... +# this test should pass +# Subtest: sync throw fail +not ok 8 - sync throw fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'thrown from sync throw fail' + toString: [Function (anonymous)] + error: 'thrown from sync throw fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: async skip pass +ok 9 - async skip pass # SKIP + --- + duration_ms: * + ... +# Subtest: async pass +ok 10 - async pass + --- + duration_ms: * + ... +# Subtest: async throw fail +not ok 11 - async throw fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'thrown from async throw fail' + toString: [Function (anonymous)] + error: 'thrown from async throw fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: async skip fail +not ok 12 - async skip fail # SKIP + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'thrown from async throw fail' + toString: [Function (anonymous)] + error: 'thrown from async throw fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: async assertion fail +not ok 13 - async assertion fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: |- + Expected values to be strictly equal: + + true !== false + + toString: [Function (anonymous)] + error: |- + Expected values to be strictly equal: + + true !== false + + code: 'ERR_ASSERTION' + name: 'AssertionError' + expected: false + actual: true + operator: 'strictEqual' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: resolve pass +ok 14 - resolve pass + --- + duration_ms: * + ... +# Subtest: reject fail +not ok 15 - reject fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'rejected from reject fail' + toString: [Function (anonymous)] + error: 'rejected from reject fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: unhandled rejection - passes but warns +ok 16 - unhandled rejection - passes but warns + --- + duration_ms: * + ... +# Subtest: async unhandled rejection - passes but warns +ok 17 - async unhandled rejection - passes but warns + --- + duration_ms: * + ... +# Subtest: immediate throw - passes but warns +ok 18 - immediate throw - passes but warns + --- + duration_ms: * + ... +# Subtest: immediate reject - passes but warns +ok 19 - immediate reject - passes but warns + --- + duration_ms: * + ... +# Subtest: immediate resolve pass +ok 20 - immediate resolve pass + --- + duration_ms: * + ... +# Subtest: subtest sync throw fail + # Subtest: +sync throw fail + not ok 1 - +sync throw fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):11' + failureType: 'testCodeFailure' + message: 'thrown from subtest sync throw fail' + toString: [Function (anonymous)] + error: 'thrown from subtest sync throw fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + * + * + ... + # this subtest should make its parent test fail + 1..1 +not ok 21 - subtest sync throw fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'subtestsFailed' + message: '1 subtest failed' + toString: [Function (anonymous)] + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: sync throw non-error fail +not ok 22 - sync throw non-error fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'Symbol(thrown symbol from sync throw non-error fail)' + toString: [Function (anonymous)] + error: 'Symbol(thrown symbol from sync throw non-error fail)' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + ... +# Subtest: level 0a + # Subtest: level 1a + ok 1 - level 1a + --- + duration_ms: * + ... + # Subtest: level 1b + ok 2 - level 1b + --- + duration_ms: * + ... + # Subtest: level 1c + ok 3 - level 1c + --- + duration_ms: * + ... + # Subtest: level 1d + ok 4 - level 1d + --- + duration_ms: * + ... + 1..4 +ok 23 - level 0a + --- + duration_ms: * + ... +# Subtest: top level + # Subtest: +long running + not ok 1 - +long running + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):5' + failureType: 'cancelledByParent' + message: 'test did not finish before its parent and was cancelled' + toString: [Function (anonymous)] + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + # Subtest: +short running + # Subtest: ++short running + ok 1 - ++short running + --- + duration_ms: * + ... + 1..1 + ok 2 - +short running + --- + duration_ms: * + ... + 1..2 +not ok 24 - top level + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'subtestsFailed' + message: '1 subtest failed' + toString: [Function (anonymous)] + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: invalid subtest - pass but subtest fails +ok 25 - invalid subtest - pass but subtest fails + --- + duration_ms: * + ... +# Subtest: sync skip option +ok 26 - sync skip option # SKIP + --- + duration_ms: * + ... +# Subtest: sync skip option with message +ok 27 - sync skip option with message # SKIP this is skipped + --- + duration_ms: * + ... +# Subtest: sync skip option is false fail +not ok 28 - sync skip option is false fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'this should be executed' + toString: [Function (anonymous)] + error: 'this should be executed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: +ok 29 - + --- + duration_ms: * + ... +# Subtest: functionOnly +ok 30 - functionOnly + --- + duration_ms: * + ... +# Subtest: +ok 31 - + --- + duration_ms: * + ... +# Subtest: test with only a name provided +ok 32 - test with only a name provided + --- + duration_ms: * + ... +# Subtest: +ok 33 - + --- + duration_ms: * + ... +# Subtest: +ok 34 - # SKIP + --- + duration_ms: * + ... +# Subtest: test with a name and options provided +ok 35 - test with a name and options provided # SKIP + --- + duration_ms: * + ... +# Subtest: functionAndOptions +ok 36 - functionAndOptions # SKIP + --- + duration_ms: * + ... +# Subtest: callback pass +ok 37 - callback pass + --- + duration_ms: * + ... +# Subtest: callback fail +not ok 38 - callback fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'callback failure' + toString: [Function (anonymous)] + error: 'callback failure' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + ... +# Subtest: sync t is this in test +ok 39 - sync t is this in test + --- + duration_ms: * + ... +# Subtest: async t is this in test +ok 40 - async t is this in test + --- + duration_ms: * + ... +# Subtest: callback t is this in test +ok 41 - callback t is this in test + --- + duration_ms: * + ... +# Subtest: callback also returns a Promise +not ok 42 - callback also returns a Promise + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'callbackAndPromisePresent' + message: 'passed a callback but also returned a Promise' + toString: [Function (anonymous)] + error: 'passed a callback but also returned a Promise' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: callback throw +not ok 43 - callback throw + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'thrown from callback throw' + toString: [Function (anonymous)] + error: 'thrown from callback throw' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: callback called twice +not ok 44 - callback called twice + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'multipleCallbackInvocations' + message: 'callback invoked multiple times' + toString: [Function (anonymous)] + error: 'callback invoked multiple times' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... +# Subtest: callback called twice in different ticks +ok 45 - callback called twice in different ticks + --- + duration_ms: * + ... +# Subtest: callback called twice in future tick +not ok 46 - callback called twice in future tick + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'uncaughtException' + message: 'callback invoked multiple times' + toString: [Function (anonymous)] + error: 'callback invoked multiple times' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: callback async throw +not ok 47 - callback async throw + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'uncaughtException' + message: 'thrown from callback async throw' + toString: [Function (anonymous)] + error: 'thrown from callback async throw' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + ... +# Subtest: callback async throw after done +ok 48 - callback async throw after done + --- + duration_ms: * + ... +# Subtest: only is set on subtests but not in only mode + # Subtest: running subtest 1 + ok 1 - running subtest 1 + --- + duration_ms: * + ... + # Subtest: running subtest 3 + ok 2 - running subtest 3 + --- + duration_ms: * + ... + # Subtest: running subtest 4 + ok 3 - running subtest 4 + --- + duration_ms: * + ... + 1..3 +ok 49 - only is set on subtests but not in only mode + --- + duration_ms: * + ... +# Subtest: custom inspect symbol fail +not ok 50 - custom inspect symbol fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'customized' + toString: [Function (anonymous)] + error: 'customized' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: custom inspect symbol that throws fail +not ok 51 - custom inspect symbol that throws fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: |- + { + foo: 1, + [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] + } + toString: [Function (anonymous)] + error: |- + { + foo: 1, + [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] + } + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +# Subtest: subtest sync throw fails + # Subtest: sync throw fails at first + not ok 1 - sync throw fails at first + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):11' + failureType: 'testCodeFailure' + message: 'thrown from subtest sync throw fails at first' + toString: [Function (anonymous)] + error: 'thrown from subtest sync throw fails at first' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + * + * + ... + # Subtest: sync throw fails at second + not ok 2 - sync throw fails at second + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):11' + failureType: 'testCodeFailure' + message: 'thrown from subtest sync throw fails at second' + toString: [Function (anonymous)] + error: 'thrown from subtest sync throw fails at second' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + ... + 1..2 +not ok 52 - subtest sync throw fails + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'subtestsFailed' + message: '2 subtests failed' + toString: [Function (anonymous)] + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: timed out async test +not ok 53 - timed out async test + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testTimeoutFailure' + message: 'test timed out after 5ms' + toString: [Function (anonymous)] + error: 'test timed out after 5ms' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + ... +# Subtest: timed out callback test +not ok 54 - timed out callback test + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testTimeoutFailure' + message: 'test timed out after 5ms' + toString: [Function (anonymous)] + error: 'test timed out after 5ms' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + ... +# Subtest: large timeout async test is ok +ok 55 - large timeout async test is ok + --- + duration_ms: * + ... +# Subtest: large timeout callback test is ok +ok 56 - large timeout callback test is ok + --- + duration_ms: * + ... +# Subtest: successful thenable +ok 57 - successful thenable + --- + duration_ms: * + ... +# Subtest: rejected thenable +not ok 58 - rejected thenable + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'custom error' + toString: [Function (anonymous)] + error: 'custom error' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + ... +# Subtest: unfinished test with uncaughtException +not ok 59 - unfinished test with uncaughtException + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'uncaughtException' + message: 'foo' + toString: [Function (anonymous)] + error: 'foo' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + ... +# Subtest: unfinished test with unhandledRejection +not ok 60 - unfinished test with unhandledRejection + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'unhandledRejection' + message: 'bar' + toString: [Function (anonymous)] + error: 'bar' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + ... +# Subtest: assertion errors display actual and expected properly +{ + boo: [ 1 ], + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + circular: { bar: 2, c: [Circular *1] } +} +[ 1 ] +{ + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined +} +1970-01-01T00:00:00.000Z + { bar: 2, c: [Circular *1] } +{ + foo: 1, + bar: 1, + boo: [ 1 ], + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + } +} +[ 1 ] +{ + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined +} +1970-01-01T00:00:00.000Z +not ok 61 - assertion errors display actual and expected properly + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + failureType: 'testCodeFailure' + message: |- + Expected values to be loosely deep-equal: + + { + bar: 1, + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + foo: 1 + } + + should loosely deep-equal + + { + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + circular: { + bar: 2, + c: [Circular *1] + } + } + toString: [Function (anonymous)] + error: |- + Expected values to be loosely deep-equal: + + { + bar: 1, + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + foo: 1 + } + + should loosely deep-equal + + { + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + circular: { + bar: 2, + c: [Circular *1] + } + } + code: 'ERR_ASSERTION' + name: 'AssertionError' + expected: + boo: + 0: 1 + baz: + date: 1970-01-01T00:00:00.000Z + null: ~ + number: 1 + string: 'Hello' + circular: + bar: 2 + c: + actual: + foo: 1 + bar: 1 + boo: + 0: 1 + baz: + date: 1970-01-01T00:00:00.000Z + null: ~ + number: 1 + string: 'Hello' + operator: 'deepEqual' + stack: |- + * + ... +# Subtest: invalid subtest fail +not ok 62 - invalid subtest fail + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):7' + failureType: 'parentAlreadyFinished' + message: 'test could not be started because its parent finished' + toString: [Function (anonymous)] + error: 'test could not be started because its parent finished' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... +1..62 +# Error: Test "unhandled rejection - passes but warns" at test/fixtures/test-runner/output/output.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. +# Error: Test "async unhandled rejection - passes but warns" at test/fixtures/test-runner/output/output.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. +# Error: A resource generated asynchronous activity after the test ended. This activity created the error "Error: uncaught from outside of a test" which triggered an uncaughtException event, caught by the test runner. +# Error: Test "immediate throw - passes but warns" at test/fixtures/test-runner/output/output.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. +# Error: Test "immediate reject - passes but warns" at test/fixtures/test-runner/output/output.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. +# Error: Test "callback called twice in different ticks" at test/fixtures/test-runner/output/output.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. +# Error: Test "callback async throw after done" at test/fixtures/test-runner/output/output.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. +# tests 75 +# suites 0 +# pass 34 +# fail 25 +# cancelled 3 +# skipped 9 +# todo 4 +# duration_ms * diff --git a/test/fixtures/test-runner/output/output_cli.js b/test/fixtures/test-runner/output/output_cli.js new file mode 100644 index 0000000..4c6b029 --- /dev/null +++ b/test/fixtures/test-runner/output/output_cli.js @@ -0,0 +1,8 @@ +'use strict'; +require('../../../common'); +const fixtures = require('../../../common/fixtures'); +const spawn = require('node:child_process').spawn; + +spawn(process.execPath, + ['--no-warnings', '--test', '--test-reporter', 'tap', fixtures.path('test-runner/output/output.js'), fixtures.path('test-runner/output/single.js')], + { stdio: 'inherit' }); diff --git a/test/message/test_runner_output.out b/test/fixtures/test-runner/output/output_cli.snapshot similarity index 54% rename from test/message/test_runner_output.out rename to test/fixtures/test-runner/output/output_cli.snapshot index 15d2009..0821b2c 100644 --- a/test/message/test_runner_output.out +++ b/test/fixtures/test-runner/output/output_cli.snapshot @@ -13,6 +13,8 @@ ok 2 - sync pass todo with message # TODO this is a passing todo not ok 3 - sync fail todo # TODO --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'thrown from sync fail todo' failureType: 'testCodeFailure' error: 'thrown from sync fail todo' code: 'ERR_TEST_FAILURE' @@ -29,6 +31,8 @@ not ok 3 - sync fail todo # TODO not ok 4 - sync fail todo with message # TODO this is a failing todo --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'thrown from sync fail todo with message' failureType: 'testCodeFailure' error: 'thrown from sync fail todo with message' code: 'ERR_TEST_FAILURE' @@ -61,6 +65,8 @@ ok 7 - sync pass not ok 8 - sync throw fail --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'thrown from sync throw fail' failureType: 'testCodeFailure' error: 'thrown from sync throw fail' code: 'ERR_TEST_FAILURE' @@ -87,6 +93,8 @@ ok 10 - async pass not ok 11 - async throw fail --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'thrown from async throw fail' failureType: 'testCodeFailure' error: 'thrown from async throw fail' code: 'ERR_TEST_FAILURE' @@ -103,6 +111,8 @@ not ok 11 - async throw fail not ok 12 - async skip fail # SKIP --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'thrown from async throw fail' failureType: 'testCodeFailure' error: 'thrown from async throw fail' code: 'ERR_TEST_FAILURE' @@ -119,13 +129,20 @@ not ok 12 - async skip fail # SKIP not ok 13 - async assertion fail --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: |- + Expected values to be strictly equal: + + true !== false + failureType: 'testCodeFailure' error: |- Expected values to be strictly equal: - + true !== false - + code: 'ERR_ASSERTION' + name: 'AssertionError' expected: false actual: true operator: 'strictEqual' @@ -147,6 +164,8 @@ ok 14 - resolve pass not ok 15 - reject fail --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'rejected from reject fail' failureType: 'testCodeFailure' error: 'rejected from reject fail' code: 'ERR_TEST_FAILURE' @@ -189,6 +208,8 @@ ok 20 - immediate resolve pass not ok 1 - +sync throw fail --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):11' + message: 'thrown from subtest sync throw fail' failureType: 'testCodeFailure' error: 'thrown from subtest sync throw fail' code: 'ERR_TEST_FAILURE' @@ -203,23 +224,40 @@ ok 20 - immediate resolve pass * * * + * + * ... # this subtest should make its parent test fail 1..1 not ok 21 - subtest sync throw fail --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: '1 subtest failed' failureType: 'subtestsFailed' error: '1 subtest failed' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * ... # Subtest: sync throw non-error fail not ok 22 - sync throw non-error fail --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'Symbol(thrown symbol from sync throw non-error fail)' failureType: 'testCodeFailure' error: 'Symbol(thrown symbol from sync throw non-error fail)' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * ... # Subtest: level 0a # Subtest: level 1a @@ -252,9 +290,18 @@ ok 23 - level 0a not ok 1 - +long running --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):5' + message: 'test did not finish before its parent and was cancelled' failureType: 'cancelledByParent' error: 'test did not finish before its parent and was cancelled' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * ... # Subtest: +short running # Subtest: ++short running @@ -271,9 +318,17 @@ ok 23 - level 0a not ok 24 - top level --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: '1 subtest failed' failureType: 'subtestsFailed' error: '1 subtest failed' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * ... # Subtest: invalid subtest - pass but subtest fails ok 25 - invalid subtest - pass but subtest fails @@ -294,6 +349,8 @@ ok 27 - sync skip option with message # SKIP this is skipped not ok 28 - sync skip option is false fail --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'this should be executed' failureType: 'testCodeFailure' error: 'this should be executed' code: 'ERR_TEST_FAILURE' @@ -346,36 +403,17 @@ ok 36 - functionAndOptions # SKIP --- duration_ms: * ... -# Subtest: escaped description \\ \# \\\#\\ \\n \\t \\f \\v \\b \\r -ok 37 - escaped description \\ \# \\\#\\ \\n \\t \\f \\v \\b \\r - --- - duration_ms: * - ... -# Subtest: escaped skip message -ok 38 - escaped skip message # SKIP \#skip - --- - duration_ms: * - ... -# Subtest: escaped todo message -ok 39 - escaped todo message # TODO \#todo - --- - duration_ms: * - ... -# Subtest: escaped diagnostic -ok 40 - escaped diagnostic - --- - duration_ms: * - ... -# \#diagnostic # Subtest: callback pass -ok 41 - callback pass +ok 37 - callback pass --- duration_ms: * ... # Subtest: callback fail -not ok 42 - callback fail +not ok 38 - callback fail --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'callback failure' failureType: 'testCodeFailure' error: 'callback failure' code: 'ERR_TEST_FAILURE' @@ -384,32 +422,44 @@ not ok 42 - callback fail * ... # Subtest: sync t is this in test -ok 43 - sync t is this in test +ok 39 - sync t is this in test --- duration_ms: * ... # Subtest: async t is this in test -ok 44 - async t is this in test +ok 40 - async t is this in test --- duration_ms: * ... # Subtest: callback t is this in test -ok 45 - callback t is this in test +ok 41 - callback t is this in test --- duration_ms: * ... # Subtest: callback also returns a Promise -not ok 46 - callback also returns a Promise +not ok 42 - callback also returns a Promise --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'passed a callback but also returned a Promise' failureType: 'callbackAndPromisePresent' error: 'passed a callback but also returned a Promise' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * ... # Subtest: callback throw -not ok 47 - callback throw +not ok 43 - callback throw --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'thrown from callback throw' failureType: 'testCodeFailure' error: 'thrown from callback throw' code: 'ERR_TEST_FAILURE' @@ -423,35 +473,53 @@ not ok 47 - callback throw * ... # Subtest: callback called twice -not ok 48 - callback called twice +not ok 44 - callback called twice --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'callback invoked multiple times' failureType: 'multipleCallbackInvocations' error: 'callback invoked multiple times' code: 'ERR_TEST_FAILURE' stack: |- * * + * + * + * + * + * + * + * + * ... # Subtest: callback called twice in different ticks -ok 49 - callback called twice in different ticks +ok 45 - callback called twice in different ticks --- duration_ms: * ... # Subtest: callback called twice in future tick -not ok 50 - callback called twice in future tick +not ok 46 - callback called twice in future tick --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'callback invoked multiple times' failureType: 'uncaughtException' error: 'callback invoked multiple times' code: 'ERR_TEST_FAILURE' stack: |- * + * + * + * + * ... # Subtest: callback async throw -not ok 51 - callback async throw +not ok 47 - callback async throw --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'thrown from callback async throw' failureType: 'uncaughtException' error: 'thrown from callback async throw' code: 'ERR_TEST_FAILURE' @@ -460,11 +528,11 @@ not ok 51 - callback async throw * ... # Subtest: callback async throw after done -ok 52 - callback async throw after done +ok 48 - callback async throw after done --- duration_ms: * ... -# Subtest: only is set but not in only mode +# Subtest: only is set on subtests but not in only mode # Subtest: running subtest 1 ok 1 - running subtest 1 --- @@ -475,33 +543,51 @@ ok 52 - callback async throw after done --- duration_ms: * ... + # 'only' and 'runOnly' require the --test-only command-line option. # Subtest: running subtest 3 ok 3 - running subtest 3 --- duration_ms: * ... + # 'only' and 'runOnly' require the --test-only command-line option. # Subtest: running subtest 4 ok 4 - running subtest 4 --- duration_ms: * ... 1..4 -ok 53 - only is set but not in only mode +ok 49 - only is set on subtests but not in only mode --- duration_ms: * ... # Subtest: custom inspect symbol fail -not ok 54 - custom inspect symbol fail +not ok 50 - custom inspect symbol fail --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'customized' failureType: 'testCodeFailure' error: 'customized' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * ... # Subtest: custom inspect symbol that throws fail -not ok 55 - custom inspect symbol that throws fail +not ok 51 - custom inspect symbol that throws fail --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: |- + { + foo: 1, + [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] + } failureType: 'testCodeFailure' error: |- { @@ -509,12 +595,22 @@ not ok 55 - custom inspect symbol that throws fail [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] } code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * ... # Subtest: subtest sync throw fails # Subtest: sync throw fails at first not ok 1 - sync throw fails at first --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):11' + message: 'thrown from subtest sync throw fails at first' failureType: 'testCodeFailure' error: 'thrown from subtest sync throw fails at first' code: 'ERR_TEST_FAILURE' @@ -529,11 +625,15 @@ not ok 55 - custom inspect symbol that throws fail * * * + * + * ... # Subtest: sync throw fails at second not ok 2 - sync throw fails at second --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):11' + message: 'thrown from subtest sync throw fails at second' failureType: 'testCodeFailure' error: 'thrown from subtest sync throw fails at second' code: 'ERR_TEST_FAILURE' @@ -546,60 +646,97 @@ not ok 55 - custom inspect symbol that throws fail * * * - * - * ... 1..2 -not ok 56 - subtest sync throw fails +not ok 52 - subtest sync throw fails --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: '2 subtests failed' failureType: 'subtestsFailed' error: '2 subtests failed' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * ... # Subtest: timed out async test -not ok 57 - timed out async test +not ok 53 - timed out async test --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'test timed out after 5ms' failureType: 'testTimeoutFailure' error: 'test timed out after 5ms' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * ... # Subtest: timed out callback test -not ok 58 - timed out callback test +not ok 54 - timed out callback test --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'test timed out after 5ms' failureType: 'testTimeoutFailure' error: 'test timed out after 5ms' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * ... # Subtest: large timeout async test is ok -ok 59 - large timeout async test is ok +ok 55 - large timeout async test is ok --- duration_ms: * ... # Subtest: large timeout callback test is ok -ok 60 - large timeout callback test is ok +ok 56 - large timeout callback test is ok --- duration_ms: * ... # Subtest: successful thenable -ok 61 - successful thenable +ok 57 - successful thenable --- duration_ms: * ... # Subtest: rejected thenable -not ok 62 - rejected thenable +not ok 58 - rejected thenable --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'custom error' failureType: 'testCodeFailure' error: 'custom error' code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * ... # Subtest: unfinished test with uncaughtException -not ok 63 - unfinished test with uncaughtException +not ok 59 - unfinished test with uncaughtException --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'foo' failureType: 'uncaughtException' error: 'foo' code: 'ERR_TEST_FAILURE' @@ -609,9 +746,11 @@ not ok 63 - unfinished test with uncaughtException * ... # Subtest: unfinished test with unhandledRejection -not ok 64 - unfinished test with unhandledRejection +not ok 60 - unfinished test with unhandledRejection --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: 'bar' failureType: 'unhandledRejection' error: 'bar' code: 'ERR_TEST_FAILURE' @@ -620,27 +759,140 @@ not ok 64 - unfinished test with unhandledRejection * * ... +# Subtest: assertion errors display actual and expected properly +not ok 61 - assertion errors display actual and expected properly + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):1' + message: |- + Expected values to be loosely deep-equal: + + { + bar: 1, + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + foo: 1 + } + + should loosely deep-equal + + { + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + circular: { + bar: 2, + c: [Circular *1] + } + } + failureType: 'testCodeFailure' + error: |- + Expected values to be loosely deep-equal: + + { + bar: 1, + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + foo: 1 + } + + should loosely deep-equal + + { + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + circular: { + bar: 2, + c: [Circular *1] + } + } + code: 'ERR_ASSERTION' + name: 'AssertionError' + expected: + boo: + 0: 1 + baz: + date: + number: 1 + string: 'Hello' + circular: + bar: 2 + c: + actual: + foo: 1 + bar: 1 + boo: + baz: + operator: 'deepEqual' + stack: |- + * + ... # Subtest: invalid subtest fail -not ok 65 - invalid subtest fail +not ok 62 - invalid subtest fail --- duration_ms: * + location: '/test/fixtures/test-runner/output/output.js:(LINE):7' + message: 'test could not be started because its parent finished' failureType: 'parentAlreadyFinished' error: 'test could not be started because its parent finished' code: 'ERR_TEST_FAILURE' stack: |- * + * + * + * + * + * + ... +# Error: Test "unhandled rejection - passes but warns" at test/fixtures/test-runner/output/output.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. +# Error: Test "async unhandled rejection - passes but warns" at test/fixtures/test-runner/output/output.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. +# Error: A resource generated asynchronous activity after the test ended. This activity created the error "Error: uncaught from outside of a test" which triggered an uncaughtException event, caught by the test runner. +# Error: Test "immediate throw - passes but warns" at test/fixtures/test-runner/output/output.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. +# Error: Test "immediate reject - passes but warns" at test/fixtures/test-runner/output/output.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. +# Error: Test "callback called twice in different ticks" at test/fixtures/test-runner/output/output.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. +# Error: Test "callback async throw after done" at test/fixtures/test-runner/output/output.js:(LINE):1 generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. +# Subtest: last test +ok 63 - last test + --- + duration_ms: * ... -1..65 -# Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. -# Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. -# Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. -# Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. -# Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. -# Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. -# tests 65 -# pass 27 -# fail 21 -# cancelled 2 -# skipped 10 -# todo 5 +1..63 +# tests 77 +# suites 0 +# pass 36 +# fail 25 +# cancelled 3 +# skipped 9 +# todo 4 # duration_ms * diff --git a/test/fixtures/test-runner/output/reset-color-depth.js b/test/fixtures/test-runner/output/reset-color-depth.js new file mode 100644 index 0000000..02c04b2 --- /dev/null +++ b/test/fixtures/test-runner/output/reset-color-depth.js @@ -0,0 +1,5 @@ +'use strict'; + +// Make tests OS-independent by overriding stdio getColorDepth(). +process.stdout.getColorDepth = () => 8; +process.stderr.getColorDepth = () => 8; diff --git a/test/fixtures/test-runner/output/single.js b/test/fixtures/test-runner/output/single.js new file mode 100644 index 0000000..721900a --- /dev/null +++ b/test/fixtures/test-runner/output/single.js @@ -0,0 +1,3 @@ +'use strict'; +const test = require('#node:test'); +test('last test', () => {}); diff --git a/test/fixtures/test-runner/output/skip-each-hooks.js b/test/fixtures/test-runner/output/skip-each-hooks.js new file mode 100644 index 0000000..e2da97a --- /dev/null +++ b/test/fixtures/test-runner/output/skip-each-hooks.js @@ -0,0 +1,21 @@ +// Flags: --test-reporter=spec +'use strict'; +const { after, afterEach, before, beforeEach, test } = require('#node:test'); + +before(() => { + console.log('BEFORE'); +}); + +beforeEach(() => { + console.log('BEFORE EACH'); +}); + +after(() => { + console.log('AFTER'); +}); + +afterEach(() => { + console.log('AFTER EACH'); +}); + +test('skipped test', { skip: true }); diff --git a/test/fixtures/test-runner/output/skip-each-hooks.snapshot b/test/fixtures/test-runner/output/skip-each-hooks.snapshot new file mode 100644 index 0000000..e923761 --- /dev/null +++ b/test/fixtures/test-runner/output/skip-each-hooks.snapshot @@ -0,0 +1,11 @@ +BEFORE +AFTER +﹣ skipped test (*ms) # SKIP +ℹ tests 1 +ℹ suites 0 +ℹ pass 0 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 1 +ℹ todo 0 +ℹ duration_ms * diff --git a/test/fixtures/test-runner/output/skip_pattern.js b/test/fixtures/test-runner/output/skip_pattern.js new file mode 100644 index 0000000..9363881 --- /dev/null +++ b/test/fixtures/test-runner/output/skip_pattern.js @@ -0,0 +1,20 @@ +// Flags: --test-skip-pattern=disabled --test-skip-pattern=/no/i +'use strict'; +const common = require('../../../common'); +const { + describe, + it, + test, +} = require('#node:test'); + +test('top level test disabled', common.mustNotCall()); +test('top level skipped test disabled', { skip: true }, common.mustNotCall()); +test('top level skipped test enabled', { skip: true }, common.mustNotCall()); +it('top level it enabled', common.mustCall()); +it('top level it disabled', common.mustNotCall()); +it.skip('top level skipped it disabled', common.mustNotCall()); +it.skip('top level skipped it enabled', common.mustNotCall()); +describe('top level describe', common.mustCall()); +describe.skip('top level skipped describe disabled', common.mustNotCall()); +describe.skip('top level skipped describe enabled', common.mustNotCall()); +test('this will NOt call', common.mustNotCall()); diff --git a/test/fixtures/test-runner/output/skip_pattern.snapshot b/test/fixtures/test-runner/output/skip_pattern.snapshot new file mode 100644 index 0000000..fd8fcf1 --- /dev/null +++ b/test/fixtures/test-runner/output/skip_pattern.snapshot @@ -0,0 +1,37 @@ +TAP version 13 +# Subtest: top level skipped test enabled +ok 1 - top level skipped test enabled # SKIP + --- + duration_ms: * + ... +# Subtest: top level it enabled +ok 2 - top level it enabled + --- + duration_ms: * + ... +# Subtest: top level skipped it enabled +ok 3 - top level skipped it enabled # SKIP + --- + duration_ms: * + ... +# Subtest: top level describe +ok 4 - top level describe + --- + duration_ms: * + type: 'suite' + ... +# Subtest: top level skipped describe enabled +ok 5 - top level skipped describe enabled # SKIP + --- + duration_ms: * + type: 'suite' + ... +1..5 +# tests 3 +# suites 2 +# pass 1 +# fail 0 +# cancelled 0 +# skipped 2 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/source_mapped_locations.mjs b/test/fixtures/test-runner/output/source_mapped_locations.mjs new file mode 100644 index 0000000..4434b9c --- /dev/null +++ b/test/fixtures/test-runner/output/source_mapped_locations.mjs @@ -0,0 +1,8 @@ +// Flags: --enable-source-maps +import pkg from '#node:test'; +const { test } = pkg; +import { strictEqual } from 'node:assert'; +test('fails', () => { + strictEqual(1, 2); +}); +//# sourceMappingURL=source_mapped_locations.mjs.map diff --git a/test/fixtures/test-runner/output/source_mapped_locations.mjs.map b/test/fixtures/test-runner/output/source_mapped_locations.mjs.map new file mode 100644 index 0000000..991dd95 --- /dev/null +++ b/test/fixtures/test-runner/output/source_mapped_locations.mjs.map @@ -0,0 +1 @@ +{"version":3,"file":"source_mapped_locations.mjs","sourceRoot":"","sources":["source_mapped_locations.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;IACjB,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC"} diff --git a/test/fixtures/test-runner/output/source_mapped_locations.snapshot b/test/fixtures/test-runner/output/source_mapped_locations.snapshot new file mode 100644 index 0000000..5388167 --- /dev/null +++ b/test/fixtures/test-runner/output/source_mapped_locations.snapshot @@ -0,0 +1,20 @@ +node:internal/modules/cjs/loader:1177 + const err = new Error(`Cannot find module '${request}'`); + ^ + +Error: Cannot find module '/lib/internal/source_map/source_map_cache.js' + * + * + * + * + * + * + * + * + * + * { + code: 'MODULE_NOT_FOUND', + path: '/package.json' +} + +Node.js v18.20.4 diff --git a/test/fixtures/test-runner/output/source_mapped_locations.ts b/test/fixtures/test-runner/output/source_mapped_locations.ts new file mode 100644 index 0000000..fbae935 --- /dev/null +++ b/test/fixtures/test-runner/output/source_mapped_locations.ts @@ -0,0 +1,7 @@ +// Flags: --enable-source-maps +import { test } from '#node:test'; +import { strictEqual } from 'node:assert'; + +test('fails', () => { + strictEqual(1, 2); +}); diff --git a/test/fixtures/test-runner/output/spec_reporter.js b/test/fixtures/test-runner/output/spec_reporter.js new file mode 100644 index 0000000..46e18b1 --- /dev/null +++ b/test/fixtures/test-runner/output/spec_reporter.js @@ -0,0 +1,11 @@ +'use strict'; +require('../../../common'); +const fixtures = require('../../../common/fixtures'); +const spawn = require('node:child_process').spawn; + +const child = spawn(process.execPath, + ['--no-warnings', '--test-reporter', 'spec', fixtures.path('test-runner/output/output.js')], + { stdio: 'pipe' }); +// eslint-disable-next-line no-control-regex +child.stdout.on('data', (d) => process.stdout.write(d.toString().replace(/[^\x00-\x7F]/g, '').replace(/\u001b\[\d+m/g, ''))); +child.stderr.pipe(process.stderr); diff --git a/test/fixtures/test-runner/output/spec_reporter.snapshot b/test/fixtures/test-runner/output/spec_reporter.snapshot new file mode 100644 index 0000000..ff2e184 --- /dev/null +++ b/test/fixtures/test-runner/output/spec_reporter.snapshot @@ -0,0 +1,598 @@ + sync pass todo (*ms) # TODO + sync pass todo with message (*ms) # this is a passing todo + sync fail todo (*ms) # TODO + Error: thrown from sync fail todo + * + * + * + * + * + * + * + + sync fail todo with message (*ms) # this is a failing todo + Error: thrown from sync fail todo with message + * + * + * + * + * + * + * + + sync skip pass (*ms) # SKIP + sync skip pass with message (*ms) # this is skipped + sync pass (*ms) + this test should pass + sync throw fail (*ms) + Error: thrown from sync throw fail + * + * + * + * + * + * + * + + async skip pass (*ms) # SKIP + async pass (*ms) + async throw fail (*ms) + Error: thrown from async throw fail + * + * + * + * + * + * + * + + async skip fail (*ms) # SKIP + Error: thrown from async throw fail + * + * + * + * + * + * + * + + async assertion fail (*ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + * + * + * + * + * + * + * { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: true, + expected: false, + operator: 'strictEqual' + } + + resolve pass (*ms) + reject fail (*ms) + Error: rejected from reject fail + * + * + * + * + * + * + * + + unhandled rejection - passes but warns (*ms) + async unhandled rejection - passes but warns (*ms) + immediate throw - passes but warns (*ms) + immediate reject - passes but warns (*ms) + immediate resolve pass (*ms) + subtest sync throw fail + +sync throw fail (*ms) + Error: thrown from subtest sync throw fail + * + * + * + * + * + * + * + * + * + * + * + * + + this subtest should make its parent test fail + subtest sync throw fail (*ms) + sync throw non-error fail (*ms) + Symbol(thrown symbol from sync throw non-error fail) + + level 0a + level 1a (*ms) + level 1b (*ms) + level 1c (*ms) + level 1d (*ms) + level 0a (*ms) + top level + +long running (*ms) + 'test did not finish before its parent and was cancelled' + + +short running + ++short running (*ms) + +short running (*ms) + top level (*ms) + invalid subtest - pass but subtest fails (*ms) + sync skip option (*ms) # SKIP + sync skip option with message (*ms) # this is skipped + sync skip option is false fail (*ms) + Error: this should be executed + * + * + * + * + * + * + * + + (*ms) + functionOnly (*ms) + (*ms) + test with only a name provided (*ms) + (*ms) + (*ms) # SKIP + test with a name and options provided (*ms) # SKIP + functionAndOptions (*ms) # SKIP + callback pass (*ms) + callback fail (*ms) + Error: callback failure + * + * + + sync t is this in test (*ms) + async t is this in test (*ms) + callback t is this in test (*ms) + callback also returns a Promise (*ms) + 'passed a callback but also returned a Promise' + + callback throw (*ms) + Error: thrown from callback throw + * + * + * + * + * + * + * + + callback called twice (*ms) + 'callback invoked multiple times' + + callback called twice in different ticks (*ms) + callback called twice in future tick (*ms) + Error: callback invoked multiple times + * + * + * + * + * { + failureType: 'multipleCallbackInvocations', + cause: 'callback invoked multiple times', + code: 'ERR_TEST_FAILURE', + toString: [Function (anonymous)] + } + + callback async throw (*ms) + Error: thrown from callback async throw + * + * + + callback async throw after done (*ms) + only is set on subtests but not in only mode + running subtest 1 (*ms) + running subtest 3 (*ms) + running subtest 4 (*ms) + only is set on subtests but not in only mode (*ms) + custom inspect symbol fail (*ms) + customized + + custom inspect symbol that throws fail (*ms) + { foo: 1, [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] } + + subtest sync throw fails + sync throw fails at first (*ms) + Error: thrown from subtest sync throw fails at first + * + * + * + * + * + * + * + * + * + * + * + * + + sync throw fails at second (*ms) + Error: thrown from subtest sync throw fails at second + * + * + * + * + * + * + * + * + + subtest sync throw fails (*ms) + timed out async test (*ms) + 'test timed out after *ms' + + timed out callback test (*ms) + 'test timed out after *ms' + + large timeout async test is ok (*ms) + large timeout callback test is ok (*ms) + successful thenable (*ms) + rejected thenable (*ms) + 'custom error' + + unfinished test with uncaughtException (*ms) + Error: foo + * + * + * + + unfinished test with unhandledRejection (*ms) + Error: bar + * + * + * + + assertion errors display actual and expected properly (*ms) + AssertionError [ERR_ASSERTION]: Expected values to be loosely deep-equal: + + { + bar: 1, + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + foo: 1 + } + + should loosely deep-equal + + { + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + circular: { + bar: 2, + c: [Circular *1] + } + } + * { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: [Object], + expected: [Object], + operator: 'deepEqual' + } + + invalid subtest fail (*ms) + 'test could not be started because its parent finished' + + Error: Test "unhandled rejection - passes but warns" at test/fixtures/test-runner/output/output.js:72:1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. + Error: Test "async unhandled rejection - passes but warns" at test/fixtures/test-runner/output/output.js:76:1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. + Error: A resource generated asynchronous activity after the test ended. This activity created the error "Error: uncaught from outside of a test" which triggered an uncaughtException event, caught by the test runner. + Error: Test "immediate throw - passes but warns" at test/fixtures/test-runner/output/output.js:80:1 generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. + Error: Test "immediate reject - passes but warns" at test/fixtures/test-runner/output/output.js:86:1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. + Error: Test "callback called twice in different ticks" at test/fixtures/test-runner/output/output.js:251:1 generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. + Error: Test "callback async throw after done" at test/fixtures/test-runner/output/output.js:269:1 generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. + tests 75 + suites 0 + pass 34 + fail 25 + cancelled 3 + skipped 9 + todo 4 + duration_ms * + + failing tests: + +* + sync fail todo (*ms) # TODO + Error: thrown from sync fail todo + * + * + * + * + * + * + * + +* + sync fail todo with message (*ms) # this is a failing todo + Error: thrown from sync fail todo with message + * + * + * + * + * + * + * + +* + sync throw fail (*ms) + Error: thrown from sync throw fail + * + * + * + * + * + * + * + +* + async throw fail (*ms) + Error: thrown from async throw fail + * + * + * + * + * + * + * + +* + async skip fail (*ms) # SKIP + Error: thrown from async throw fail + * + * + * + * + * + * + * + +* + async assertion fail (*ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + * + * + * + * + * + * + * { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: true, + expected: false, + operator: 'strictEqual' + } + +* + reject fail (*ms) + Error: rejected from reject fail + * + * + * + * + * + * + * + +* + +sync throw fail (*ms) + Error: thrown from subtest sync throw fail + * + * + * + * + * + * + * + * + * + * + * + * + +* + sync throw non-error fail (*ms) + Symbol(thrown symbol from sync throw non-error fail) + +* + +long running (*ms) + 'test did not finish before its parent and was cancelled' + +* + sync skip option is false fail (*ms) + Error: this should be executed + * + * + * + * + * + * + * + +* + callback fail (*ms) + Error: callback failure + * + * + +* + callback also returns a Promise (*ms) + 'passed a callback but also returned a Promise' + +* + callback throw (*ms) + Error: thrown from callback throw + * + * + * + * + * + * + * + +* + callback called twice (*ms) + 'callback invoked multiple times' + +* + callback called twice in future tick (*ms) + Error: callback invoked multiple times + * + * + * + * + * { + failureType: 'multipleCallbackInvocations', + cause: 'callback invoked multiple times', + code: 'ERR_TEST_FAILURE', + toString: [Function (anonymous)] + } + +* + callback async throw (*ms) + Error: thrown from callback async throw + * + * + +* + custom inspect symbol fail (*ms) + customized + +* + custom inspect symbol that throws fail (*ms) + { foo: 1, [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] } + +* + sync throw fails at first (*ms) + Error: thrown from subtest sync throw fails at first + * + * + * + * + * + * + * + * + * + * + * + * + +* + sync throw fails at second (*ms) + Error: thrown from subtest sync throw fails at second + * + * + * + * + * + * + * + * + +* + timed out async test (*ms) + 'test timed out after *ms' + +* + timed out callback test (*ms) + 'test timed out after *ms' + +* + rejected thenable (*ms) + 'custom error' + +* + unfinished test with uncaughtException (*ms) + Error: foo + * + * + * + +* + unfinished test with unhandledRejection (*ms) + Error: bar + * + * + * + +* + assertion errors display actual and expected properly (*ms) + AssertionError [ERR_ASSERTION]: Expected values to be loosely deep-equal: + + { + bar: 1, + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + foo: 1 + } + + should loosely deep-equal + + { + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + circular: { + bar: 2, + c: [Circular *1] + } + } + * { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: [Object], + expected: [Object], + operator: 'deepEqual' + } + +* + invalid subtest fail (*ms) + 'test could not be started because its parent finished' diff --git a/test/fixtures/test-runner/output/spec_reporter_cli.js b/test/fixtures/test-runner/output/spec_reporter_cli.js new file mode 100644 index 0000000..b0c72e5 --- /dev/null +++ b/test/fixtures/test-runner/output/spec_reporter_cli.js @@ -0,0 +1,11 @@ +'use strict'; +require('../../../common'); +const fixtures = require('../../../common/fixtures'); +const spawn = require('node:child_process').spawn; + +const child = spawn(process.execPath, + ['--no-warnings', '--test', '--test-reporter', 'spec', fixtures.path('test-runner/output/output.js')], + { stdio: 'pipe' }); +// eslint-disable-next-line no-control-regex +child.stdout.on('data', (d) => process.stdout.write(d.toString().replace(/[^\x00-\x7F]/g, '').replace(/\u001b\[\d+m/g, ''))); +child.stderr.pipe(process.stderr); diff --git a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot new file mode 100644 index 0000000..996c062 --- /dev/null +++ b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot @@ -0,0 +1,605 @@ + sync pass todo (*ms) # TODO + sync pass todo with message (*ms) # this is a passing todo + sync fail todo (*ms) # TODO + Error: thrown from sync fail todo + * + * + * + * + * + * + * + + sync fail todo with message (*ms) # this is a failing todo + Error: thrown from sync fail todo with message + * + * + * + * + * + * + * + + sync skip pass (*ms) # SKIP + sync skip pass with message (*ms) # this is skipped + sync pass (*ms) + this test should pass + sync throw fail (*ms) + Error: thrown from sync throw fail + * + * + * + * + * + * + * + + async skip pass (*ms) # SKIP + async pass (*ms) + async throw fail (*ms) + Error: thrown from async throw fail + * + * + * + * + * + * + * + + async skip fail (*ms) # SKIP + Error: thrown from async throw fail + * + * + * + * + * + * + * + + async assertion fail (*ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + * + * + * + * + * + * + * { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: true, + expected: false, + operator: 'strictEqual' + } + + resolve pass (*ms) + reject fail (*ms) + Error: rejected from reject fail + * + * + * + * + * + * + * + + unhandled rejection - passes but warns (*ms) + async unhandled rejection - passes but warns (*ms) + immediate throw - passes but warns (*ms) + immediate reject - passes but warns (*ms) + immediate resolve pass (*ms) + subtest sync throw fail + +sync throw fail (*ms) + Error: thrown from subtest sync throw fail + * + * + * + * + * + * + * + * + * + * + * + * + + this subtest should make its parent test fail + subtest sync throw fail (*ms) + + sync throw non-error fail (*ms) + Symbol(thrown symbol from sync throw non-error fail) + + level 0a + level 1a (*ms) + level 1b (*ms) + level 1c (*ms) + level 1d (*ms) + level 0a (*ms) + + top level + +long running (*ms) + 'test did not finish before its parent and was cancelled' + + +short running + ++short running (*ms) + +short running (*ms) + + top level (*ms) + + invalid subtest - pass but subtest fails (*ms) + sync skip option (*ms) # SKIP + sync skip option with message (*ms) # this is skipped + sync skip option is false fail (*ms) + Error: this should be executed + * + * + * + * + * + * + * + + (*ms) + functionOnly (*ms) + (*ms) + test with only a name provided (*ms) + (*ms) + (*ms) # SKIP + test with a name and options provided (*ms) # SKIP + functionAndOptions (*ms) # SKIP + callback pass (*ms) + callback fail (*ms) + Error: callback failure + * + * + + sync t is this in test (*ms) + async t is this in test (*ms) + callback t is this in test (*ms) + callback also returns a Promise (*ms) + 'passed a callback but also returned a Promise' + + callback throw (*ms) + Error: thrown from callback throw + * + * + * + * + * + * + * + + callback called twice (*ms) + 'callback invoked multiple times' + + callback called twice in different ticks (*ms) + callback called twice in future tick (*ms) + Error: callback invoked multiple times + * + * + * + * + * { + failureType: 'multipleCallbackInvocations', + cause: 'callback invoked multiple times', + code: 'ERR_TEST_FAILURE' + } + + callback async throw (*ms) + Error: thrown from callback async throw + * + * + + callback async throw after done (*ms) + only is set on subtests but not in only mode + running subtest 1 (*ms) + running subtest 2 (*ms) + 'only' and 'runOnly' require the --test-only command-line option. + running subtest 3 (*ms) + 'only' and 'runOnly' require the --test-only command-line option. + running subtest 4 (*ms) + only is set on subtests but not in only mode (*ms) + + custom inspect symbol fail (*ms) + customized + + custom inspect symbol that throws fail (*ms) + { foo: 1 } + + subtest sync throw fails + sync throw fails at first (*ms) + Error: thrown from subtest sync throw fails at first + * + * + * + * + * + * + * + * + * + * + * + * + + sync throw fails at second (*ms) + Error: thrown from subtest sync throw fails at second + * + * + * + * + * + * + * + * + + subtest sync throw fails (*ms) + + timed out async test (*ms) + 'test timed out after *ms' + + timed out callback test (*ms) + 'test timed out after *ms' + + large timeout async test is ok (*ms) + large timeout callback test is ok (*ms) + successful thenable (*ms) + rejected thenable (*ms) + 'custom error' + + unfinished test with uncaughtException (*ms) + Error: foo + * + * + * + + unfinished test with unhandledRejection (*ms) + Error: bar + * + * + * + + assertion errors display actual and expected properly (*ms) + AssertionError [ERR_ASSERTION]: Expected values to be loosely deep-equal: + + { + bar: 1, + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + foo: 1 + } + + should loosely deep-equal + + { + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + circular: { + bar: 2, + c: [Circular *1] + } + } + * { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: { foo: 1, bar: 1, boo: [ 1 ], baz: { date: 1970-01-01T00:00:00.000Z, null: null, number: 1, string: 'Hello', undefined: undefined } }, + expected: { boo: [ 1 ], baz: { date: 1970-01-01T00:00:00.000Z, null: null, number: 1, string: 'Hello', undefined: undefined }, circular: { bar: 2, c: [Circular *1] } }, + operator: 'deepEqual' + } + + invalid subtest fail (*ms) + 'test could not be started because its parent finished' + + Error: Test "unhandled rejection - passes but warns" at test/fixtures/test-runner/output/output.js:72:1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. + Error: Test "async unhandled rejection - passes but warns" at test/fixtures/test-runner/output/output.js:76:1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. + Error: A resource generated asynchronous activity after the test ended. This activity created the error "Error: uncaught from outside of a test" which triggered an uncaughtException event, caught by the test runner. + Error: Test "immediate throw - passes but warns" at test/fixtures/test-runner/output/output.js:80:1 generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. + Error: Test "immediate reject - passes but warns" at test/fixtures/test-runner/output/output.js:86:1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. + Error: Test "callback called twice in different ticks" at test/fixtures/test-runner/output/output.js:251:1 generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. + Error: Test "callback async throw after done" at test/fixtures/test-runner/output/output.js:269:1 generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. + tests 76 + suites 0 + pass 35 + fail 25 + cancelled 3 + skipped 9 + todo 4 + duration_ms * + + failing tests: + +* + sync fail todo (*ms) # TODO + Error: thrown from sync fail todo + * + * + * + * + * + * + * + +* + sync fail todo with message (*ms) # this is a failing todo + Error: thrown from sync fail todo with message + * + * + * + * + * + * + * + +* + sync throw fail (*ms) + Error: thrown from sync throw fail + * + * + * + * + * + * + * + +* + async throw fail (*ms) + Error: thrown from async throw fail + * + * + * + * + * + * + * + +* + async skip fail (*ms) # SKIP + Error: thrown from async throw fail + * + * + * + * + * + * + * + +* + async assertion fail (*ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + * + * + * + * + * + * + * { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: true, + expected: false, + operator: 'strictEqual' + } + +* + reject fail (*ms) + Error: rejected from reject fail + * + * + * + * + * + * + * + +* + +sync throw fail (*ms) + Error: thrown from subtest sync throw fail + * + * + * + * + * + * + * + * + * + * + * + * + +* + sync throw non-error fail (*ms) + Symbol(thrown symbol from sync throw non-error fail) + +* + +long running (*ms) + 'test did not finish before its parent and was cancelled' + +* + sync skip option is false fail (*ms) + Error: this should be executed + * + * + * + * + * + * + * + +* + callback fail (*ms) + Error: callback failure + * + * + +* + callback also returns a Promise (*ms) + 'passed a callback but also returned a Promise' + +* + callback throw (*ms) + Error: thrown from callback throw + * + * + * + * + * + * + * + +* + callback called twice (*ms) + 'callback invoked multiple times' + +* + callback called twice in future tick (*ms) + Error: callback invoked multiple times + * + * + * + * + * { + failureType: 'multipleCallbackInvocations', + cause: 'callback invoked multiple times', + code: 'ERR_TEST_FAILURE' + } + +* + callback async throw (*ms) + Error: thrown from callback async throw + * + * + +* + custom inspect symbol fail (*ms) + customized + +* + custom inspect symbol that throws fail (*ms) + { foo: 1 } + +* + sync throw fails at first (*ms) + Error: thrown from subtest sync throw fails at first + * + * + * + * + * + * + * + * + * + * + * + * + +* + sync throw fails at second (*ms) + Error: thrown from subtest sync throw fails at second + * + * + * + * + * + * + * + * + +* + timed out async test (*ms) + 'test timed out after *ms' + +* + timed out callback test (*ms) + 'test timed out after *ms' + +* + rejected thenable (*ms) + 'custom error' + +* + unfinished test with uncaughtException (*ms) + Error: foo + * + * + * + +* + unfinished test with unhandledRejection (*ms) + Error: bar + * + * + * + +* + assertion errors display actual and expected properly (*ms) + AssertionError [ERR_ASSERTION]: Expected values to be loosely deep-equal: + + { + bar: 1, + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + foo: 1 + } + + should loosely deep-equal + + { + baz: { + date: 1970-01-01T00:00:00.000Z, + null: null, + number: 1, + string: 'Hello', + undefined: undefined + }, + boo: [ + 1 + ], + circular: { + bar: 2, + c: [Circular *1] + } + } + * { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: { foo: 1, bar: 1, boo: [ 1 ], baz: { date: 1970-01-01T00:00:00.000Z, null: null, number: 1, string: 'Hello', undefined: undefined } }, + expected: { boo: [ 1 ], baz: { date: 1970-01-01T00:00:00.000Z, null: null, number: 1, string: 'Hello', undefined: undefined }, circular: { bar: 2, c: [Circular *1] } }, + operator: 'deepEqual' + } + +* + invalid subtest fail (*ms) + 'test could not be started because its parent finished' diff --git a/test/fixtures/test-runner/output/spec_reporter_successful.js b/test/fixtures/test-runner/output/spec_reporter_successful.js new file mode 100644 index 0000000..f6034a4 --- /dev/null +++ b/test/fixtures/test-runner/output/spec_reporter_successful.js @@ -0,0 +1,6 @@ +// Flags: --test-reporter=spec +'use strict'; +require('../../../common'); +const { it } = require('#node:test'); + +it('should pass', () => {}); diff --git a/test/fixtures/test-runner/output/spec_reporter_successful.snapshot b/test/fixtures/test-runner/output/spec_reporter_successful.snapshot new file mode 100644 index 0000000..a494136 --- /dev/null +++ b/test/fixtures/test-runner/output/spec_reporter_successful.snapshot @@ -0,0 +1,9 @@ +✔ should pass (*ms) +ℹ tests 1 +ℹ suites 0 +ℹ pass 1 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms * diff --git a/test/fixtures/test-runner/output/suite-skip-hooks.js b/test/fixtures/test-runner/output/suite-skip-hooks.js new file mode 100644 index 0000000..f74a726 --- /dev/null +++ b/test/fixtures/test-runner/output/suite-skip-hooks.js @@ -0,0 +1,60 @@ +// Flags: --test-reporter=spec +'use strict'; +const { + after, + afterEach, + before, + beforeEach, + describe, + it, +} = require('#node:test'); + +describe('skip all hooks in this suite', { skip: true }, () => { + before(() => { + console.log('BEFORE 1'); + }); + + beforeEach(() => { + console.log('BEFORE EACH 1'); + }); + + after(() => { + console.log('AFTER 1'); + }); + + afterEach(() => { + console.log('AFTER EACH 1'); + }); + + it('should not run'); +}); + +describe('suite runs with mixture of skipped tests', () => { + before(() => { + console.log('BEFORE 2'); + }); + + beforeEach(() => { + console.log('BEFORE EACH 2'); + }); + + after(() => { + console.log('AFTER 2'); + }); + + afterEach(() => { + console.log('AFTER EACH 2'); + }); + + it('should not run', { skip: true }); + + it('should run 1', () => { + console.log('should run 1'); + }); + + it('should not run', { skip: true }); + + it('should run 2', () => { + console.log('should run 2'); + }); +}); diff --git a/test/fixtures/test-runner/output/suite-skip-hooks.snapshot b/test/fixtures/test-runner/output/suite-skip-hooks.snapshot new file mode 100644 index 0000000..91949ea --- /dev/null +++ b/test/fixtures/test-runner/output/suite-skip-hooks.snapshot @@ -0,0 +1,23 @@ +BEFORE 2 +BEFORE EACH 2 +should run 1 +AFTER EACH 2 +BEFORE EACH 2 +should run 2 +AFTER EACH 2 +AFTER 2 +﹣ skip all hooks in this suite (*ms) # SKIP +▶ suite runs with mixture of skipped tests + ﹣ should not run (*ms) # SKIP + ✔ should run 1 (*ms) + ﹣ should not run (*ms) # SKIP + ✔ should run 2 (*ms) +✔ suite runs with mixture of skipped tests (*ms) +ℹ tests 4 +ℹ suites 2 +ℹ pass 2 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 2 +ℹ todo 0 +ℹ duration_ms * diff --git a/test/fixtures/test-runner/output/tap_escape.js b/test/fixtures/test-runner/output/tap_escape.js new file mode 100644 index 0000000..53b70d4 --- /dev/null +++ b/test/fixtures/test-runner/output/tap_escape.js @@ -0,0 +1,19 @@ +'use strict'; +require('../../../common'); +const { test } = require('#node:test'); + +// Do not include any failing tests in this file. + +// A test whose description needs to be escaped. +test('escaped description \\ # \\#\\ \n \t \f \v \b \r'); + +// A test whose skip message needs to be escaped. +test('escaped skip message', { skip: '#skip' }); + +// A test whose todo message needs to be escaped. +test('escaped todo message', { todo: '#todo' }); + +// A test with a diagnostic message that needs to be escaped. +test('escaped diagnostic', (t) => { + t.diagnostic('#diagnostic'); +}); diff --git a/test/fixtures/test-runner/output/tap_escape.snapshot b/test/fixtures/test-runner/output/tap_escape.snapshot new file mode 100644 index 0000000..722cd0c --- /dev/null +++ b/test/fixtures/test-runner/output/tap_escape.snapshot @@ -0,0 +1,31 @@ +TAP version 13 +# Subtest: escaped description \\ \# \\\#\\ \\n \\t \\f \\v \\b \\r +ok 1 - escaped description \\ \# \\\#\\ \\n \\t \\f \\v \\b \\r + --- + duration_ms: * + ... +# Subtest: escaped skip message +ok 2 - escaped skip message # SKIP \#skip + --- + duration_ms: * + ... +# Subtest: escaped todo message +ok 3 - escaped todo message # TODO \#todo + --- + duration_ms: * + ... +# Subtest: escaped diagnostic +ok 4 - escaped diagnostic + --- + duration_ms: * + ... +# \#diagnostic +1..4 +# tests 4 +# suites 0 +# pass 2 +# fail 0 +# cancelled 0 +# skipped 1 +# todo 1 +# duration_ms * diff --git a/test/fixtures/test-runner/output/test-diagnostic-warning-without-test-only-flag.js b/test/fixtures/test-runner/output/test-diagnostic-warning-without-test-only-flag.js new file mode 100644 index 0000000..812e863 --- /dev/null +++ b/test/fixtures/test-runner/output/test-diagnostic-warning-without-test-only-flag.js @@ -0,0 +1,40 @@ +'use strict'; +const { test, describe, it } = require('#node:test'); + +describe('should NOT print --test-only diagnostic warning - describe-only-false', {only: false}, () => { + it('only false in describe'); + }); + + describe('should NOT print --test-only diagnostic warning - it-only-false', () => { + it('only false in the subtest', {only: false}); + }); + + describe('should NOT print --test-only diagnostic warning - no-only', () => { + it('no only'); + }); + + test('should NOT print --test-only diagnostic warning - test-only-false', {only: false}, async (t) => { + await t.test('only false in parent test'); + }); + + test('should NOT print --test-only diagnostic warning - t.test-only-false', async (t) => { + await t.test('only false in subtest', {only: false}); + }); + + test('should NOT print --test-only diagnostic warning - no-only', async (t) => { + await t.test('no only'); + }); + + test('should print --test-only diagnostic warning - test-only-true', {only: true}, async (t) => { + await t.test('only true in parent test'); + }) + + test('should print --test-only diagnostic warning - t.test-only-true', async (t) => { + await t.test('only true in subtest', {only: true}); + }); + + test('should print --test-only diagnostic warning - 2 levels of only', async (t) => { + await t.test('only true in parent test', {only: false}, async (t) => { + await t.test('only true in subtest', {only: true}); + }); + }) diff --git a/test/fixtures/test-runner/output/test-diagnostic-warning-without-test-only-flag.snapshot b/test/fixtures/test-runner/output/test-diagnostic-warning-without-test-only-flag.snapshot new file mode 100644 index 0000000..be06318 --- /dev/null +++ b/test/fixtures/test-runner/output/test-diagnostic-warning-without-test-only-flag.snapshot @@ -0,0 +1,21 @@ +TAP version 13 +# Subtest: should print --test-only diagnostic warning - test-only-true + # Subtest: only true in parent test + ok 1 - only true in parent test + --- + duration_ms: * + ... + 1..1 +ok 1 - should print --test-only diagnostic warning - test-only-true + --- + duration_ms: * + ... +1..1 +# tests 2 +# suites 0 +# pass 2 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/test-runner-plan.js b/test/fixtures/test-runner/output/test-runner-plan.js new file mode 100644 index 0000000..8e3f576 --- /dev/null +++ b/test/fixtures/test-runner/output/test-runner-plan.js @@ -0,0 +1,79 @@ +'use strict'; +const { test } = require('#node:test'); +const { Readable } = require('node:stream'); + +test('test planning basic', (t) => { + t.plan(2); + t.assert.ok(true); + t.assert.ok(true); +}); + +test('less assertions than planned', (t) => { + t.plan(1); +}); + +test('more assertions than planned', (t) => { + t.plan(1); + t.assert.ok(true); + t.assert.ok(true); +}); + +test('subtesting', (t) => { + t.plan(1); + t.test('subtest', () => { }); +}); + +test('subtesting correctly', (t) => { + t.plan(2); + t.assert.ok(true); + t.test('subtest', (st) => { + st.plan(1); + st.assert.ok(true); + }); +}); + +test('correctly ignoring subtesting plan', (t) => { + t.plan(1); + t.test('subtest', (st) => { + st.plan(1); + st.assert.ok(true); + }); +}); + +test('failing planning by options', { plan: 1 }, () => { +}); + +test('not failing planning by options', { plan: 1 }, (t) => { + t.assert.ok(true); +}); + +test('subtest planning by options', (t) => { + t.test('subtest', { plan: 1 }, (st) => { + st.assert.ok(true); + }); +}); + +test('failing more assertions than planned', (t) => { + t.plan(2); + t.assert.ok(true); + t.assert.ok(true); + t.assert.ok(true); +}); + +test('planning with streams', (t, done) => { + function* generate() { + yield 'a'; + yield 'b'; + yield 'c'; + } + const expected = ['a', 'b', 'c']; + t.plan(expected.length); + const stream = Readable.from(generate()); + stream.on('data', (chunk) => { + t.assert.strictEqual(chunk, expected.shift()); + }); + + stream.on('end', () => { + done(); + }); +}) diff --git a/test/fixtures/test-runner/output/test-runner-plan.snapshot b/test/fixtures/test-runner/output/test-runner-plan.snapshot new file mode 100644 index 0000000..7389d65 --- /dev/null +++ b/test/fixtures/test-runner/output/test-runner-plan.snapshot @@ -0,0 +1,137 @@ +TAP version 13 +# Subtest: test planning basic +ok 1 - test planning basic + --- + duration_ms: * + ... +# Subtest: less assertions than planned +not ok 2 - less assertions than planned + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/test-runner-plan.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'plan expected 1 assertions but received 0' + toString: [Function (anonymous)] + error: 'plan expected 1 assertions but received 0' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: more assertions than planned +not ok 3 - more assertions than planned + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/test-runner-plan.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'plan expected 1 assertions but received 2' + toString: [Function (anonymous)] + error: 'plan expected 1 assertions but received 2' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: subtesting + # Subtest: subtest + ok 1 - subtest + --- + duration_ms: * + ... + 1..1 +ok 4 - subtesting + --- + duration_ms: * + ... +# Subtest: subtesting correctly + # Subtest: subtest + ok 1 - subtest + --- + duration_ms: * + ... + 1..1 +ok 5 - subtesting correctly + --- + duration_ms: * + ... +# Subtest: correctly ignoring subtesting plan + # Subtest: subtest + ok 1 - subtest + --- + duration_ms: * + ... + 1..1 +ok 6 - correctly ignoring subtesting plan + --- + duration_ms: * + ... +# Subtest: failing planning by options +not ok 7 - failing planning by options + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/test-runner-plan.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'plan expected 1 assertions but received 0' + toString: [Function (anonymous)] + error: 'plan expected 1 assertions but received 0' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: not failing planning by options +ok 8 - not failing planning by options + --- + duration_ms: * + ... +# Subtest: subtest planning by options + # Subtest: subtest + ok 1 - subtest + --- + duration_ms: * + ... + 1..1 +ok 9 - subtest planning by options + --- + duration_ms: * + ... +# Subtest: failing more assertions than planned +not ok 10 - failing more assertions than planned + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/test-runner-plan.js:(LINE):1' + failureType: 'testCodeFailure' + message: 'plan expected 2 assertions but received 3' + toString: [Function (anonymous)] + error: 'plan expected 2 assertions but received 3' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: planning with streams +ok 11 - planning with streams + --- + duration_ms: * + ... +1..11 +# tests 15 +# suites 0 +# pass 11 +# fail 4 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/timeout_in_before_each_should_not_affect_further_tests.js b/test/fixtures/test-runner/output/timeout_in_before_each_should_not_affect_further_tests.js new file mode 100644 index 0000000..6205e2c --- /dev/null +++ b/test/fixtures/test-runner/output/timeout_in_before_each_should_not_affect_further_tests.js @@ -0,0 +1,46 @@ +const {describe, test, beforeEach, afterEach} = require("node:test"); +const {setTimeout} = require("timers/promises"); + + +describe('before each timeout', () => { + let i = 0; + + beforeEach(async () => { + if (i++ === 0) { + console.log('gonna timeout'); + await setTimeout(700); + return; + } + console.log('not gonna timeout'); + }, {timeout: 500}); + + test('first describe first test', () => { + console.log('before each test first ' + i); + }); + + test('first describe second test', () => { + console.log('before each test second ' + i); + }); +}); + + +describe('after each timeout', () => { + let i = 0; + + afterEach(async function afterEach1() { + if (i++ === 0) { + console.log('gonna timeout'); + await setTimeout(700); + return; + } + console.log('not gonna timeout'); + }, {timeout: 500}); + + test('second describe first test', () => { + console.log('after each test first ' + i); + }); + + test('second describe second test', () => { + console.log('after each test second ' + i); + }); +}); diff --git a/test/fixtures/test-runner/output/timeout_in_before_each_should_not_affect_further_tests.snapshot b/test/fixtures/test-runner/output/timeout_in_before_each_should_not_affect_further_tests.snapshot new file mode 100644 index 0000000..b3579da --- /dev/null +++ b/test/fixtures/test-runner/output/timeout_in_before_each_should_not_affect_further_tests.snapshot @@ -0,0 +1,71 @@ +gonna timeout +TAP version 13 +not gonna timeout +before each test second 2 +after each test first 0 +gonna timeout +# Subtest: before each timeout + # Subtest: first describe first test + not ok 1 - first describe first test + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/timeout_in_before_each_should_not_affect_further_tests.js:(LINE):3' + failureType: 'hookFailed' + error: 'failed running beforeEach hook' + code: 'ERR_TEST_FAILURE' + stack: |- + async Promise.all (index 0) + ... + # Subtest: first describe second test + ok 2 - first describe second test + --- + duration_ms: * + ... + 1..2 +not ok 1 - before each timeout + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/timeout_in_before_each_should_not_affect_further_tests.js:(LINE):1' + failureType: 'subtestsFailed' + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + ... +after each test second 1 +not gonna timeout +# Subtest: after each timeout + # Subtest: second describe first test + not ok 1 - second describe first test + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/timeout_in_before_each_should_not_affect_further_tests.js:(LINE):3' + failureType: 'hookFailed' + error: 'failed running afterEach hook' + code: 'ERR_TEST_FAILURE' + stack: |- + async Promise.all (index 0) + ... + # Subtest: second describe second test + ok 2 - second describe second test + --- + duration_ms: * + ... + 1..2 +not ok 2 - after each timeout + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/timeout_in_before_each_should_not_affect_further_tests.js:(LINE):1' + failureType: 'subtestsFailed' + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + ... +1..2 +# tests 4 +# suites 2 +# pass 2 +# fail 2 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/unfinished-suite-async-error.js b/test/fixtures/test-runner/output/unfinished-suite-async-error.js new file mode 100644 index 0000000..f4999e6 --- /dev/null +++ b/test/fixtures/test-runner/output/unfinished-suite-async-error.js @@ -0,0 +1,14 @@ +'use strict'; +const { describe, it } = require('#node:test'); + +describe('unfinished suite with asynchronous error', () => { + it('uses callback', (t, done) => { + setImmediate(() => { + throw new Error('callback test does not complete'); + }); + }); + + it('should pass 1'); +}); + +it('should pass 2'); diff --git a/test/fixtures/test-runner/output/unfinished-suite-async-error.snapshot b/test/fixtures/test-runner/output/unfinished-suite-async-error.snapshot new file mode 100644 index 0000000..bde2676 --- /dev/null +++ b/test/fixtures/test-runner/output/unfinished-suite-async-error.snapshot @@ -0,0 +1,53 @@ +TAP version 13 +# Subtest: unfinished suite with asynchronous error + # Subtest: uses callback + not ok 1 - uses callback + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/unfinished-suite-async-error.js:(LINE):3' + failureType: 'uncaughtException' + message: 'callback test does not complete' + toString: [Function (anonymous)] + error: 'callback test does not complete' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + ... + # Subtest: should pass 1 + ok 2 - should pass 1 + --- + duration_ms: * + ... + 1..2 +not ok 1 - unfinished suite with asynchronous error + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/unfinished-suite-async-error.js:(LINE):1' + failureType: 'subtestsFailed' + message: '1 subtest failed' + toString: [Function (anonymous)] + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + ... +# Subtest: should pass 2 +ok 2 - should pass 2 + --- + duration_ms: * + ... +1..2 +# tests 3 +# suites 1 +# pass 2 +# fail 1 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/fixtures/test-runner/output/unresolved_promise.js b/test/fixtures/test-runner/output/unresolved_promise.js new file mode 100644 index 0000000..4086f19 --- /dev/null +++ b/test/fixtures/test-runner/output/unresolved_promise.js @@ -0,0 +1,7 @@ +'use strict'; +require('../../../common'); +const test = require('#node:test'); + +test('pass'); +test('never resolving promise', () => new Promise(() => {})); +test('fail', () => console.log('this should not appear')); diff --git a/test/message/test_runner_unresolved_promise.out b/test/fixtures/test-runner/output/unresolved_promise.snapshot similarity index 57% rename from test/message/test_runner_unresolved_promise.out rename to test/fixtures/test-runner/output/unresolved_promise.snapshot index b4d6cba..c46ae71 100644 --- a/test/message/test_runner_unresolved_promise.out +++ b/test/fixtures/test-runner/output/unresolved_promise.snapshot @@ -8,24 +8,37 @@ ok 1 - pass not ok 2 - never resolving promise --- duration_ms: * + location: '/test/fixtures/test-runner/output/unresolved_promise.js:(LINE):1' failureType: 'cancelledByParent' + message: 'Promise resolution is still pending but the event loop has already resolved' + toString: [Function (anonymous)] error: 'Promise resolution is still pending but the event loop has already resolved' code: 'ERR_TEST_FAILURE' stack: |- * + * + * + * ... # Subtest: fail not ok 3 - fail --- - duration_ms: 0 + duration_ms: * + location: '/test/fixtures/test-runner/output/unresolved_promise.js:(LINE):1' failureType: 'cancelledByParent' + message: 'Promise resolution is still pending but the event loop has already resolved' + toString: [Function (anonymous)] error: 'Promise resolution is still pending but the event loop has already resolved' code: 'ERR_TEST_FAILURE' stack: |- * + * + * + * ... 1..3 # tests 3 +# suites 0 # pass 1 # fail 0 # cancelled 2 diff --git a/test/fixtures/test-runner/print-arguments.js b/test/fixtures/test-runner/print-arguments.js new file mode 100644 index 0000000..433f02d --- /dev/null +++ b/test/fixtures/test-runner/print-arguments.js @@ -0,0 +1,5 @@ +const { test } = require('#node:test'); + +test('process.argv is setup', (t) => { + t.assert.deepStrictEqual(process.argv.slice(2), ['--a-custom-argument']); +}); diff --git a/test/fixtures/test-runner/protoMutation.js b/test/fixtures/test-runner/protoMutation.js index a1a8296..20071b9 100644 --- a/test/fixtures/test-runner/protoMutation.js +++ b/test/fixtures/test-runner/protoMutation.js @@ -1,3 +1,3 @@ -'use strict' +'use strict'; -Object.prototype.skip = true // eslint-disable-line no-extend-native +Object.prototype.skip = true; diff --git a/test/fixtures/test-runner/random.test.mjs b/test/fixtures/test-runner/random.test.mjs deleted file mode 100644 index 4f46876..0000000 --- a/test/fixtures/test-runner/random.test.mjs +++ /dev/null @@ -1,6 +0,0 @@ -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/fixtures/test-runner/random.test.mjs -import test from '#node:test' - -test('this should fail', () => { - throw new Error('this is a failing test') -}) diff --git a/test/fixtures/test-runner/recursive_run.js b/test/fixtures/test-runner/recursive_run.js new file mode 100644 index 0000000..28b6a2c --- /dev/null +++ b/test/fixtures/test-runner/recursive_run.js @@ -0,0 +1,7 @@ +'use strict'; + +const { test, run } = require('#node:test'); + +test('recursive run() calls', async () => { + for await (const event of run({ files: [__filename] })); +}); diff --git a/test/fixtures/test-runner/reporters.js b/test/fixtures/test-runner/reporters.js index a6c216d..b2a564a 100644 --- a/test/fixtures/test-runner/reporters.js +++ b/test/fixtures/test-runner/reporters.js @@ -1,12 +1,11 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/test/fixtures/test-runner/reporters.js -'use strict' -const test = require('#node:test') +'use strict'; +const test = require('#node:test'); test('nested', { concurrency: 4 }, async (t) => { - t.test('ok', () => {}) + t.test('ok', () => {}); t.test('failing', () => { - throw new Error('error') - }) -}) + throw new Error('error'); + }); +}); -test('top level', () => {}) +test('top level', () => {}); diff --git a/test/fixtures/test-runner/root-duration.mjs b/test/fixtures/test-runner/root-duration.mjs new file mode 100644 index 0000000..102f27b --- /dev/null +++ b/test/fixtures/test-runner/root-duration.mjs @@ -0,0 +1,8 @@ +import pkg from '#node:test'; +const { test, after } = pkg; + +after(() => {}); + +test('a test with some delay', (t, done) => { + setTimeout(done, 50); +}); diff --git a/test/fixtures/test-runner/run_inspect.js b/test/fixtures/test-runner/run_inspect.js new file mode 100644 index 0000000..ddfb52c --- /dev/null +++ b/test/fixtures/test-runner/run_inspect.js @@ -0,0 +1,40 @@ +'use strict'; + +const common = require('../../common'); +const fixtures = require('../../common/fixtures'); +const { run } = require('#node:test'); +const assert = require('node:assert'); + +const badPortError = { name: 'RangeError', code: 'ERR_SOCKET_BAD_PORT' }; +let inspectPort = 'inspectPort' in process.env ? Number(process.env.inspectPort) : undefined; +let expectedError; + +if (process.env.inspectPort === 'addTwo') { + inspectPort = common.mustCall(() => { return process.debugPort += 2; }); +} else if (process.env.inspectPort === 'string') { + inspectPort = 'string'; + expectedError = badPortError; +} else if (process.env.inspectPort === 'null') { + inspectPort = null; +} else if (process.env.inspectPort === 'bignumber') { + inspectPort = 1293812; + expectedError = badPortError; +} else if (process.env.inspectPort === 'negativenumber') { + inspectPort = -9776; + expectedError = badPortError; +} else if (process.env.inspectPort === 'bignumberfunc') { + inspectPort = common.mustCall(() => 123121); + expectedError = badPortError; +} else if (process.env.inspectPort === 'strfunc') { + inspectPort = common.mustCall(() => 'invalidPort'); + expectedError = badPortError; +} + +const stream = run({ files: [fixtures.path('test-runner/run_inspect_assert.js')], inspectPort }); +if (expectedError) { + stream.on('test:fail', common.mustCall(({ details }) => { + assert.deepStrictEqual({ name: details.error.cause.name, code: details.error.cause.code }, expectedError); + })); +} else { + stream.on('test:fail', common.mustNotCall()); +} diff --git a/test/fixtures/test-runner/run_inspect_assert.js b/test/fixtures/test-runner/run_inspect_assert.js new file mode 100644 index 0000000..d37bd9f --- /dev/null +++ b/test/fixtures/test-runner/run_inspect_assert.js @@ -0,0 +1,19 @@ +'use strict'; + +const assert = require('node:assert'); + +const { expectedPort, expectedInitialPort, expectedHost } = process.env; +const debugOptions = + require('#internal/options').getOptionValue('--inspect-port'); + +if ('expectedPort' in process.env) { + assert.strictEqual(process.debugPort, +expectedPort); +} + +if ('expectedInitialPort' in process.env) { + assert.strictEqual(debugOptions.port, +expectedInitialPort); +} + +if ('expectedHost' in process.env) { + assert.strictEqual(debugOptions.host, expectedHost); +} diff --git a/test/fixtures/test-runner/shards/a.cjs b/test/fixtures/test-runner/shards/a.cjs new file mode 100644 index 0000000..1dfa5cb --- /dev/null +++ b/test/fixtures/test-runner/shards/a.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('a.cjs this should pass'); diff --git a/test/fixtures/test-runner/shards/b.cjs b/test/fixtures/test-runner/shards/b.cjs new file mode 100644 index 0000000..c46e680 --- /dev/null +++ b/test/fixtures/test-runner/shards/b.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('b.cjs this should pass'); diff --git a/test/fixtures/test-runner/shards/c.cjs b/test/fixtures/test-runner/shards/c.cjs new file mode 100644 index 0000000..474892f --- /dev/null +++ b/test/fixtures/test-runner/shards/c.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('c.cjs this should pass'); diff --git a/test/fixtures/test-runner/shards/d.cjs b/test/fixtures/test-runner/shards/d.cjs new file mode 100644 index 0000000..86064bf --- /dev/null +++ b/test/fixtures/test-runner/shards/d.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('d.cjs this should pass'); diff --git a/test/fixtures/test-runner/shards/e.cjs b/test/fixtures/test-runner/shards/e.cjs new file mode 100644 index 0000000..1310ee8 --- /dev/null +++ b/test/fixtures/test-runner/shards/e.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('e.cjs this should pass'); diff --git a/test/fixtures/test-runner/shards/f.cjs b/test/fixtures/test-runner/shards/f.cjs new file mode 100644 index 0000000..96f6def --- /dev/null +++ b/test/fixtures/test-runner/shards/f.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('f.cjs this should pass'); diff --git a/test/fixtures/test-runner/shards/g.cjs b/test/fixtures/test-runner/shards/g.cjs new file mode 100644 index 0000000..2a845d3 --- /dev/null +++ b/test/fixtures/test-runner/shards/g.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('g.cjs this should pass'); diff --git a/test/fixtures/test-runner/shards/h.cjs b/test/fixtures/test-runner/shards/h.cjs new file mode 100644 index 0000000..09ff578 --- /dev/null +++ b/test/fixtures/test-runner/shards/h.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('h.cjs this should pass'); diff --git a/test/fixtures/test-runner/shards/i.cjs b/test/fixtures/test-runner/shards/i.cjs new file mode 100644 index 0000000..61858e1 --- /dev/null +++ b/test/fixtures/test-runner/shards/i.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('i.cjs this should pass'); diff --git a/test/fixtures/test-runner/shards/j.cjs b/test/fixtures/test-runner/shards/j.cjs new file mode 100644 index 0000000..d4bfd3f --- /dev/null +++ b/test/fixtures/test-runner/shards/j.cjs @@ -0,0 +1,4 @@ +'use strict'; +const test = require('#node:test'); + +test('j.cjs this should pass'); diff --git a/test/fixtures/test-runner/snapshots/imported-tests.js b/test/fixtures/test-runner/snapshots/imported-tests.js new file mode 100644 index 0000000..528ecb7 --- /dev/null +++ b/test/fixtures/test-runner/snapshots/imported-tests.js @@ -0,0 +1,8 @@ +'use strict'; +const { suite, test } = require('#node:test'); + +suite('imported suite', () => { + test('imported test', (t) => { + t.assert.snapshot({ foo: 1, bar: 2 }); + }); +}); diff --git a/test/fixtures/test-runner/snapshots/malformed-exports.js.snapshot b/test/fixtures/test-runner/snapshots/malformed-exports.js.snapshot new file mode 100644 index 0000000..b3479f5 --- /dev/null +++ b/test/fixtures/test-runner/snapshots/malformed-exports.js.snapshot @@ -0,0 +1 @@ +exports = null; diff --git a/test/fixtures/test-runner/snapshots/simple.js.snapshot b/test/fixtures/test-runner/snapshots/simple.js.snapshot new file mode 100644 index 0000000..d8654c3 --- /dev/null +++ b/test/fixtures/test-runner/snapshots/simple.js.snapshot @@ -0,0 +1,6 @@ +exports[`foo 1`] = ` +{ + "bar": 1, + "baz": 2 +} +`; diff --git a/test/fixtures/test-runner/snapshots/unit-2.js b/test/fixtures/test-runner/snapshots/unit-2.js new file mode 100644 index 0000000..db00d29 --- /dev/null +++ b/test/fixtures/test-runner/snapshots/unit-2.js @@ -0,0 +1,11 @@ +'use strict'; +const { snapshot, test } = require('#node:test'); +const { basename, join } = require('node:path'); + +snapshot.setResolveSnapshotPath((testFile) => { + return join(process.cwd(), `${basename(testFile)}.snapshot`); +}); + +test('has a snapshot', (t) => { + t.assert.snapshot('a snapshot from ' + __filename); +}); diff --git a/test/fixtures/test-runner/snapshots/unit.js b/test/fixtures/test-runner/snapshots/unit.js new file mode 100644 index 0000000..d023fa1 --- /dev/null +++ b/test/fixtures/test-runner/snapshots/unit.js @@ -0,0 +1,30 @@ +'use strict'; +const { snapshot, suite, test } = require('#node:test'); +const { basename, join } = require('node:path'); + +snapshot.setResolveSnapshotPath((testFile) => { + return join(process.cwd(), `${basename(testFile)}.snapshot`); +}); + +suite('suite', () => { + test('test with plan', (t) => { + t.plan(2); + t.assert.snapshot({ foo: 1, bar: 2 }); + t.assert.snapshot(5); + }); +}); + +test('test', async (t) => { + t.assert.snapshot({ baz: 9 }); +}); + +test('`${foo}`', async (t) => { + const options = { serializers: [() => { return '***'; }]}; + t.assert.snapshot('snapshotted string', options); +}); + +test('escapes in `\\${foo}`\n', async (t) => { + t.assert.snapshot('`\\${foo}`\n'); +}); + +require('./imported-tests'); diff --git a/test/fixtures/test-runner/subdir/subdir_test.js b/test/fixtures/test-runner/subdir/subdir_test.js deleted file mode 100644 index d7bb67c..0000000 --- a/test/fixtures/test-runner/subdir/subdir_test.js +++ /dev/null @@ -1 +0,0 @@ -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/fixtures/test-runner/subdir/subdir_test.js diff --git a/test/fixtures/test-runner/test/random.cjs b/test/fixtures/test-runner/test/random.cjs deleted file mode 100644 index 20bf390..0000000 --- a/test/fixtures/test-runner/test/random.cjs +++ /dev/null @@ -1,5 +0,0 @@ -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/fixtures/test-runner/test/random.cjs -'use strict' -const test = require('#node:test') - -test('this should pass') diff --git a/test/fixtures/test-runner/test_only.js b/test/fixtures/test-runner/test_only.js new file mode 100644 index 0000000..ed063a6 --- /dev/null +++ b/test/fixtures/test-runner/test_only.js @@ -0,0 +1,5 @@ +'use strict'; +const test = require('#node:test'); + +test('this should be skipped'); +test.only('this should be executed'); diff --git a/test/fixtures/test-runner/throws_sync_and_async.js b/test/fixtures/test-runner/throws_sync_and_async.js new file mode 100644 index 0000000..02974df --- /dev/null +++ b/test/fixtures/test-runner/throws_sync_and_async.js @@ -0,0 +1,10 @@ +'use strict'; +const { test } = require('#node:test'); + +test('fails and schedules more work', () => { + setTimeout(() => { + throw new Error('this should not have a chance to be thrown'); + }, 1000); + + throw new Error('fails'); +}); diff --git a/test/fixtures/test-runner/todo_exit_code.js b/test/fixtures/test-runner/todo_exit_code.js new file mode 100644 index 0000000..a5d1e3a --- /dev/null +++ b/test/fixtures/test-runner/todo_exit_code.js @@ -0,0 +1,15 @@ +const { describe, test } = require('#node:test'); + +describe('suite should pass', () => { + test.todo('should fail without harming suite', () => { + throw new Error('Fail but not badly') + }); +}); + +test.todo('should fail without effecting exit code', () => { + throw new Error('Fail but not badly') +}); + +test('empty string todo', { todo: '' }, () => { + throw new Error('Fail but not badly') +}); diff --git a/test/fixtures/test-runner/user-logs.js b/test/fixtures/test-runner/user-logs.js new file mode 100644 index 0000000..ff4089a --- /dev/null +++ b/test/fixtures/test-runner/user-logs.js @@ -0,0 +1,20 @@ +'use strict'; +const test = require('#node:test'); + +console.error('stderr', 1); + +test('a test', async () => { + console.error('stderr', 2); + await new Promise((resolve) => { + console.log('stdout', 3); + setTimeout(() => { + // This should not be sent to the TAP parser. + console.error('not ok 1 - fake test'); + resolve(); + console.log('stdout', 4); + }, 2); + }); + console.error('stderr', 5); +}); + +console.error('stderr', 6); diff --git a/test/message.js b/test/message.js deleted file mode 100755 index e58a636..0000000 --- a/test/message.js +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env node -'use strict' - -const { createReadStream, promises: fs } = require('node:fs') -const { extname, join, resolve } = require('node:path') -const { promisify } = require('node:util') -const { exec } = require('node:child_process') -const { createInterface } = require('node:readline') - -const { bin } = require('../package.json') -const binPath = resolve(__dirname, '..', bin.test) - -const MESSAGE_FOLDER = join(__dirname, './message/') -const WAIT_FOR_ELLIPSIS = Symbol('wait for ellispis') - -const TEST_RUNNER_FLAGS = ['--test', '--test-only', '--test-name-pattern', '--test-reporter', '--test-reporter-destination'] - -function readLines (file) { - return createInterface({ - input: createReadStream(file), - crlfDelay: Infinity - }) -} - -const stackTraceStartLine = /^\s+stack: \|-$/ -const errorStartLine = /^\s+Error: / -const stackTraceLine = /^\s+\*$/ -const stackTraceEndLine = /^\s+\.\.\.$/ - -const nodejs14NotEmittedWarn = /^# Warning:.*\breject/ -const nodejs14NotEmittedUnhandledRejection = /unhandledRejection/ - -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/message/testcfg.py#L53 -async function IsFailureOutput (self, output, filename) { - // Convert output lines to regexps that we can match - const patterns = [] - for await (const line of readLines(self.expected)) { - // Our implementation outputs different stack traces than the Node.js implementation. - if (stackTraceLine.test(line) && patterns[patterns.length - 1] === WAIT_FOR_ELLIPSIS) continue - - // Node.js 14 doesn't emit some warnings - if (process.version.startsWith('v14.') && nodejs14NotEmittedWarn.test(line)) continue - if (process.version.startsWith('v14.') && nodejs14NotEmittedUnhandledRejection.test(line)) { - patterns.push(WAIT_FOR_ELLIPSIS) - continue - } - - // Sometimes Node.js won't have any stack trace, but we would - if (stackTraceEndLine.test(line) && patterns[patterns.length - 1].toString().endsWith("code: 'ERR_TEST_FAILURE'$")) { - patterns.push(stackTraceStartLine, WAIT_FOR_ELLIPSIS) - } - - const pattern = line - .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') - .replace(/\\\*/g, '.*') - patterns.push(`^${pattern}$`) - - if (stackTraceStartLine.test(line) || (filename === 'test_runner_output_spec_reporter.js' && errorStartLine.test(line))) { - // Our implementation outputs different stack traces than the Node.js implementation. - patterns.push(WAIT_FOR_ELLIPSIS) - } - } - // Compare actual output with the expected - const outlines = (output.stdout + output.stderr).split('\n').filter( - (line) => line && !line.startsWith('==') && !line.startsWith('**') - ) - - let waitingForEllipsis = false - for (let i = 0; i < outlines.length; i++) { - let regex - if (patterns[i] === WAIT_FOR_ELLIPSIS) { - waitingForEllipsis = true - } else if (!(regex = new RegExp(patterns[i])).test(outlines[i].replace(/\r/, '')) && !regex.test(outlines[i].replace(/\r/, '').trimEnd())) { - if (waitingForEllipsis) { - patterns.splice(i, 0, WAIT_FOR_ELLIPSIS) - continue - } - console.log('match failed', { line: i + 1, expected: patterns[i], actual: outlines[i] }) - console.log(Array.from({ length: Math.min(patterns.length, outlines.length) }, (_, i) => ({ line: i + 1, expected: patterns[i], actual: outlines[i] })).slice(Math.max(0, i - 5), i + 5)) - return true - } else if (waitingForEllipsis && stackTraceEndLine.test(outlines[i])) { - waitingForEllipsis = false - } - } - return false -} - -const main = async () => { - const dir = await fs.opendir(MESSAGE_FOLDER) - for await (const dirent of dir) { - const ext = extname(dirent.name) - if (ext === '.js' || ext === '.mjs') { - if (typeof AbortSignal === 'undefined' && dirent.name.startsWith('test_runner_abort')) { - console.log('no AbortSignal support, skipping', dirent.name) - continue - } - const filePath = join(MESSAGE_FOLDER, dirent.name) - const expected = filePath.replace(/\.m?js$/, '.out') - const testFile = await fs.open(filePath) - const fileContent = await testFile.read({ length: 512 }) - await testFile.close() - const flagIndex = fileContent.buffer.indexOf('// Flags: ') - const flags = - flagIndex === -1 - ? [] - : fileContent.buffer - .subarray( - flagIndex + 10, - fileContent.buffer.indexOf(10, flagIndex) - ) - .toString().split(' ') - - const nodeFlags = flags.filter(flag => !TEST_RUNNER_FLAGS.find(f => flag.startsWith(f))).join(' ') - const testRunnerFlags = flags.filter(flag => TEST_RUNNER_FLAGS.find(f => flag.startsWith(f))).join(' ') - - const command = testRunnerFlags.length - ? `${process.execPath} ${nodeFlags} ${binPath} ${testRunnerFlags} ${filePath}` - : `${process.execPath} ${nodeFlags} ${filePath}` - console.log(`Running ${command}`) - let stdout, stderr - try { - const res = await promisify(exec)(command) - stdout = res.stdout.trim() - stderr = res.stderr.trim() - } catch (err) { - if (err?.stdout == null || err.stderr == null) throw err - stdout = err.stdout.trim() - stderr = err.stderr.trim() - } - if (await IsFailureOutput({ expected }, { stdout, stderr }, dirent.name)) { - throw new Error() - } - console.log('pass') - } - } -} - -main().catch(err => { - console.error(err) - process.exit(1) -}) diff --git a/test/message/test_runner_abort_suite.js b/test/message/test_runner_abort_suite.js deleted file mode 100644 index 39c3066..0000000 --- a/test/message/test_runner_abort_suite.js +++ /dev/null @@ -1,28 +0,0 @@ -// https://github.com/nodejs/node/blob/389b7e138e89a339fabe4ad628bf09cd9748f957/test/message/test_runner_abort_suite.js -// Flags: --no-warnings -'use strict' -require('../common') -const { describe, it } = require('#node:test') - -describe('describe timeout signal', { signal: AbortSignal.timeout(1) }, (t) => { - it('ok 1', async () => {}) - it('ok 2', () => {}) - it('ok 3', { signal: t.signal }, async () => {}) - it('ok 4', { signal: t.signal }, () => {}) - it('not ok 1', () => new Promise(() => {})) - it('not ok 2', (done) => {}) - it('not ok 3', { signal: t.signal }, () => new Promise(() => {})) - it('not ok 4', { signal: t.signal }, (done) => {}) - it('not ok 5', { signal: t.signal }, function (done) { - this.signal.addEventListener('abort', done) - }) -}) - -describe('describe abort signal', { signal: AbortSignal.abort() }, () => { - it('should not appear', () => {}) -}) - -// AbortSignal.timeout(1) doesn't prevent process from closing -// thus we have to keep the process open to prevent cancelation -// of the entire test tree -setTimeout(() => {}, 1000) diff --git a/test/message/test_runner_abort_suite.out b/test/message/test_runner_abort_suite.out deleted file mode 100644 index 3866997..0000000 --- a/test/message/test_runner_abort_suite.out +++ /dev/null @@ -1,99 +0,0 @@ -TAP version 13 -# Subtest: describe timeout signal - # Subtest: ok 1 - ok 1 - ok 1 - --- - duration_ms: * - ... - # Subtest: ok 2 - ok 2 - ok 2 - --- - duration_ms: * - ... - # Subtest: ok 3 - ok 3 - ok 3 - --- - duration_ms: * - ... - # Subtest: ok 4 - ok 4 - ok 4 - --- - duration_ms: * - ... - # Subtest: not ok 1 - not ok 5 - not ok 1 - --- - duration_ms: * - failureType: 'cancelledByParent' - error: 'test did not finish before its parent and was cancelled' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: not ok 2 - not ok 6 - not ok 2 - --- - duration_ms: * - failureType: 'cancelledByParent' - error: 'test did not finish before its parent and was cancelled' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: not ok 3 - not ok 7 - not ok 3 - --- - duration_ms: * - failureType: 'cancelledByParent' - error: 'test did not finish before its parent and was cancelled' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: not ok 4 - not ok 8 - not ok 4 - --- - duration_ms: * - failureType: 'cancelledByParent' - error: 'test did not finish before its parent and was cancelled' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: not ok 5 - not ok 9 - not ok 5 - --- - duration_ms: * - failureType: 'cancelledByParent' - error: 'test did not finish before its parent and was cancelled' - code: 'ERR_TEST_FAILURE' - ... - 1..9 -not ok 1 - describe timeout signal - --- - duration_ms: * - error: 'The operation was aborted due to timeout' - code: 23 - stack: |- - * - * - * - * - ... -# Subtest: describe abort signal -not ok 2 - describe abort signal - --- - duration_ms: * - error: 'This operation was aborted' - code: 20 - stack: |- - * - * - * - * - * - * - * - * - * - ... -1..2 -# tests 2 -# pass 0 -# fail 0 -# cancelled 2 -# skipped 0 -# todo 0 -# duration_ms * diff --git a/test/message/test_runner_describe_nested.js b/test/message/test_runner_describe_nested.js deleted file mode 100644 index e60ebf6..0000000 --- a/test/message/test_runner_describe_nested.js +++ /dev/null @@ -1,11 +0,0 @@ -// https://github.com/nodejs/node/blob/3e57891ee2fde0971e18fc383c25acf8f90def05/test/message/test_runner_describe_nested.js -// Flags: --no-warnings -'use strict' -require('../common') -const { describe, it } = require('#node:test') - -describe('nested - no tests', () => { - describe('nested', () => { - it('nested', () => {}) - }) -}) diff --git a/test/message/test_runner_desctibe_it.js b/test/message/test_runner_desctibe_it.js deleted file mode 100644 index d8c1bb3..0000000 --- a/test/message/test_runner_desctibe_it.js +++ /dev/null @@ -1,372 +0,0 @@ -// https://github.com/nodejs/node/blob/659dc126932f986fc33c7f1c878cb2b57a1e2fac/test/message/test_runner_desctibe_it.js -// Flags: --no-warnings -'use strict' -require('../common') -const assert = require('node:assert') -const { describe, it } = require('#node:test') -const util = require('util') - -it.todo('sync pass todo', () => { - -}) - -it('sync pass todo with message', { todo: 'this is a passing todo' }, () => { -}) - -it.todo('sync fail todo', () => { - throw new Error('thrown from sync fail todo') -}) - -it('sync fail todo with message', { todo: 'this is a failing todo' }, () => { - throw new Error('thrown from sync fail todo with message') -}) - -it.skip('sync skip pass', () => { -}) - -it('sync skip pass with message', { skip: 'this is skipped' }, () => { -}) - -it('sync pass', () => { -}) - -it('sync throw fail', () => { - throw new Error('thrown from sync throw fail') -}) - -it.skip('async skip pass', async () => { -}) - -it('async pass', async () => { - -}) - -it('async throw fail', async () => { - throw new Error('thrown from async throw fail') -}) - -it('async skip fail', async (t) => { - t.skip() - throw new Error('thrown from async throw fail') -}) - -it('async assertion fail', async () => { - // Make sure the assert module is handled. - assert.strictEqual(true, false) -}) - -it('resolve pass', () => { - return Promise.resolve() -}) - -it('reject fail', () => { - return Promise.reject(new Error('rejected from reject fail')) -}) - -it('unhandled rejection - passes but warns', () => { - Promise.reject(new Error('rejected from unhandled rejection fail')) -}) - -it('async unhandled rejection - passes but warns', async () => { - Promise.reject(new Error('rejected from async unhandled rejection fail')) -}) - -it('immediate throw - passes but warns', () => { - setImmediate(() => { - throw new Error('thrown from immediate throw fail') - }) -}) - -it('immediate reject - passes but warns', () => { - setImmediate(() => { - Promise.reject(new Error('rejected from immediate reject fail')) - }) -}) - -it('immediate resolve pass', () => { - return new Promise((resolve) => { - setImmediate(() => { - resolve() - }) - }) -}) - -describe('subtest sync throw fail', () => { - it('+sync throw fail', () => { - throw new Error('thrown from subtest sync throw fail') - }) -}) - -it('sync throw non-error fail', async () => { - throw Symbol('thrown symbol from sync throw non-error fail') -}) - -describe('level 0a', { concurrency: 4 }, () => { - it('level 1a', async () => { - const p1a = new Promise((resolve) => { - setTimeout(() => { - resolve() - }, 1000) - }) - - return p1a - }) - - it('level 1b', async () => { - const p1b = new Promise((resolve) => { - resolve() - }) - - return p1b - }) - - it('level 1c', async () => { - const p1c = new Promise((resolve) => { - setTimeout(() => { - resolve() - }, 2000) - }) - - return p1c - }) - - it('level 1d', async () => { - const p1c = new Promise((resolve) => { - setTimeout(() => { - resolve() - }, 1500) - }) - - return p1c - }) - - const p0a = new Promise((resolve) => { - setTimeout(() => { - resolve() - }, 3000) - }) - - return p0a -}) - -describe('invalid subtest - pass but subtest fails', () => { - setImmediate(() => { - it('invalid subtest fail', () => { - throw new Error('this should not be thrown') - }) - }) -}) - -it.skip('sync skip option', () => { - throw new Error('this should not be executed') -}) - -it('sync skip option with message', { skip: 'this is skipped' }, () => { - throw new Error('this should not be executed') -}) - -it('sync skip option is false fail', { skip: false }, () => { - throw new Error('this should be executed') -}) - -// A test with no arguments provided. -it() - -// A test with only a named function provided. -it(function functionOnly () {}) - -// A test with only an anonymous function provided. -it(() => {}) - -// A test with only a name provided. -it('test with only a name provided') - -// A test with an empty string name. -it('') - -// A test with only options provided. -it({ skip: true }) - -// A test with only a name and options provided. -it('test with a name and options provided', { skip: true }) - -// A test with only options and a function provided. -it({ skip: true }, function functionAndOptions () {}) - -// A test whose description needs to be escaped. -it('escaped description \\ # \\#\\') - -// A test whose skip message needs to be escaped. -it('escaped skip message', { skip: '#skip' }) - -// A test whose todo message needs to be escaped. -it('escaped todo message', { todo: '#todo' }) - -it('callback pass', (done) => { - setImmediate(done) -}) - -it('callback fail', (done) => { - setImmediate(() => { - done(new Error('callback failure')) - }) -}) - -it('sync t is this in test', function () { - assert.deepStrictEqual(this, { signal: this.signal, name: this.name }) -}) - -it('async t is this in test', async function () { - assert.deepStrictEqual(this, { signal: this.signal, name: this.name }) -}) - -it('callback t is this in test', function (done) { - assert.deepStrictEqual(this, { signal: this.signal, name: this.name }) - done() -}) - -it('callback also returns a Promise', async (done) => { - throw new Error('thrown from callback also returns a Promise') -}) - -it('callback throw', (done) => { - throw new Error('thrown from callback throw') -}) - -it('callback called twice', (done) => { - done() - done() -}) - -it('callback called twice in different ticks', (done) => { - setImmediate(done) - done() -}) - -it('callback called twice in future tick', (done) => { - setImmediate(() => { - done() - done() - }) -}) - -it('callback async throw', (done) => { - setImmediate(() => { - throw new Error('thrown from callback async throw') - }) -}) - -it('callback async throw after done', (done) => { - setImmediate(() => { - throw new Error('thrown from callback async throw after done') - }) - - done() -}) - -it('custom inspect symbol fail', () => { - const obj = { - [util.inspect.custom] () { - return 'customized' - }, - foo: 1 - } - - throw obj -}) - -it('custom inspect symbol that throws fail', () => { - const obj = { - [util.inspect.custom] () { - throw new Error('bad-inspect') - }, - foo: 1 - } - - throw obj -}) - -describe('subtest sync throw fails', () => { - it('sync throw fails at first', () => { - throw new Error('thrown from subtest sync throw fails at first') - }) - it('sync throw fails at second', () => { - throw new Error('thrown from subtest sync throw fails at second') - }) -}) - -describe('describe sync throw fails', () => { - it('should not run', () => {}) - throw new Error('thrown from describe') -}) - -describe('describe async throw fails', async () => { - it('should not run', () => {}) - throw new Error('thrown from describe') -}) - -describe('timeouts', () => { - it('timed out async test', { timeout: 5 }, async () => { - return new Promise((resolve) => { - setTimeout(resolve, 1000) - }) - }) - - it('timed out callback test', { timeout: 5 }, (done) => { - setTimeout(done, 1000) - }) - - it('large timeout async test is ok', { timeout: 30_000_000 }, async () => { - return new Promise((resolve) => { - setTimeout(resolve, 10) - }) - }) - - it('large timeout callback test is ok', { timeout: 30_000_000 }, (done) => { - setTimeout(done, 10) - }) -}) - -describe('successful thenable', () => { - it('successful thenable', () => { - let thenCalled = false - return { - get then () { - if (thenCalled) throw new Error() - thenCalled = true - return (successHandler) => successHandler() - } - } - }) - - it('rejected thenable', () => { - let thenCalled = false - return { - get then () { - if (thenCalled) throw new Error() - thenCalled = true - return (_, errorHandler) => errorHandler(new Error('custom error')) - } - } - }) - - let thenCalled = false - return { - get then () { - if (thenCalled) throw new Error() - thenCalled = true - return (successHandler) => successHandler() - } - } -}) - -describe('rejected thenable', () => { - let thenCalled = false - return { - get then () { - if (thenCalled) throw new Error() - thenCalled = true - return (_, errorHandler) => errorHandler(new Error('custom error')) - } - } -}) diff --git a/test/message/test_runner_desctibe_it.out b/test/message/test_runner_desctibe_it.out deleted file mode 100644 index 208097d..0000000 --- a/test/message/test_runner_desctibe_it.out +++ /dev/null @@ -1,647 +0,0 @@ -TAP version 13 -# Subtest: sync pass todo -ok 1 - sync pass todo # TODO - --- - duration_ms: * - ... -# Subtest: sync pass todo with message -ok 2 - sync pass todo with message # TODO this is a passing todo - --- - duration_ms: * - ... -# Subtest: sync fail todo -not ok 3 - sync fail todo # TODO - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from sync fail todo' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... -# Subtest: sync fail todo with message -not ok 4 - sync fail todo with message # TODO this is a failing todo - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from sync fail todo with message' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... -# Subtest: sync skip pass -ok 5 - sync skip pass # SKIP - --- - duration_ms: * - ... -# Subtest: sync skip pass with message -ok 6 - sync skip pass with message # SKIP this is skipped - --- - duration_ms: * - ... -# Subtest: sync pass -ok 7 - sync pass - --- - duration_ms: * - ... -# Subtest: sync throw fail -not ok 8 - sync throw fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from sync throw fail' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... -# Subtest: async skip pass -ok 9 - async skip pass # SKIP - --- - duration_ms: * - ... -# Subtest: async pass -ok 10 - async pass - --- - duration_ms: * - ... -# Subtest: async throw fail -not ok 11 - async throw fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from async throw fail' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... -# Subtest: async skip fail -not ok 12 - async skip fail - --- - duration_ms: * - failureType: 'callbackAndPromisePresent' - error: 'passed a callback but also returned a Promise' - code: 'ERR_TEST_FAILURE' - ... -# Subtest: async assertion fail -not ok 13 - async assertion fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: |- - Expected values to be strictly equal: - - true !== false - - code: 'ERR_ASSERTION' - expected: false - actual: true - operator: 'strictEqual' - stack: |- - * - * - * - * - * - * - * - ... -# Subtest: resolve pass -ok 14 - resolve pass - --- - duration_ms: * - ... -# Subtest: reject fail -not ok 15 - reject fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'rejected from reject fail' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... -# Subtest: unhandled rejection - passes but warns -ok 16 - unhandled rejection - passes but warns - --- - duration_ms: * - ... -# Subtest: async unhandled rejection - passes but warns -ok 17 - async unhandled rejection - passes but warns - --- - duration_ms: * - ... -# Subtest: immediate throw - passes but warns -ok 18 - immediate throw - passes but warns - --- - duration_ms: * - ... -# Subtest: immediate reject - passes but warns -ok 19 - immediate reject - passes but warns - --- - duration_ms: * - ... -# Subtest: immediate resolve pass -ok 20 - immediate resolve pass - --- - duration_ms: * - ... -# Subtest: subtest sync throw fail - # Subtest: +sync throw fail - not ok 1 - +sync throw fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from subtest sync throw fail' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - 1..1 -not ok 21 - subtest sync throw fail - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '1 subtest failed' - code: 'ERR_TEST_FAILURE' - stack: |- - ... -# Subtest: sync throw non-error fail -not ok 22 - sync throw non-error fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'Symbol(thrown symbol from sync throw non-error fail)' - code: 'ERR_TEST_FAILURE' - stack: |- - ... -# Subtest: level 0a - # Subtest: level 1a - ok 1 - level 1a - --- - duration_ms: * - ... - # Subtest: level 1b - ok 2 - level 1b - --- - duration_ms: * - ... - # Subtest: level 1c - ok 3 - level 1c - --- - duration_ms: * - ... - # Subtest: level 1d - ok 4 - level 1d - --- - duration_ms: * - ... - 1..4 -ok 23 - level 0a - --- - duration_ms: * - ... -# Subtest: invalid subtest - pass but subtest fails -ok 24 - invalid subtest - pass but subtest fails - --- - duration_ms: * - ... -# Subtest: sync skip option -ok 25 - sync skip option # SKIP - --- - duration_ms: * - ... -# Subtest: sync skip option with message -ok 26 - sync skip option with message # SKIP this is skipped - --- - duration_ms: * - ... -# Subtest: sync skip option is false fail -not ok 27 - sync skip option is false fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'this should be executed' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... -# Subtest: -ok 28 - - --- - duration_ms: * - ... -# Subtest: functionOnly -ok 29 - functionOnly - --- - duration_ms: * - ... -# Subtest: -ok 30 - - --- - duration_ms: * - ... -# Subtest: test with only a name provided -ok 31 - test with only a name provided - --- - duration_ms: * - ... -# Subtest: -ok 32 - - --- - duration_ms: * - ... -# Subtest: -ok 33 - # SKIP - --- - duration_ms: * - ... -# Subtest: test with a name and options provided -ok 34 - test with a name and options provided # SKIP - --- - duration_ms: * - ... -# Subtest: functionAndOptions -ok 35 - functionAndOptions # SKIP - --- - duration_ms: * - ... -# Subtest: escaped description \\ \# \\\#\\ -ok 36 - escaped description \\ \# \\\#\\ - --- - duration_ms: * - ... -# Subtest: escaped skip message -ok 37 - escaped skip message # SKIP \#skip - --- - duration_ms: * - ... -# Subtest: escaped todo message -ok 38 - escaped todo message # TODO \#todo - --- - duration_ms: * - ... -# Subtest: callback pass -ok 39 - callback pass - --- - duration_ms: * - ... -# Subtest: callback fail -not ok 40 - callback fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'callback failure' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - ... -# Subtest: sync t is this in test -ok 41 - sync t is this in test - --- - duration_ms: * - ... -# Subtest: async t is this in test -ok 42 - async t is this in test - --- - duration_ms: * - ... -# Subtest: callback t is this in test -ok 43 - callback t is this in test - --- - duration_ms: * - ... -# Subtest: callback also returns a Promise -not ok 44 - callback also returns a Promise - --- - duration_ms: * - failureType: 'callbackAndPromisePresent' - error: 'passed a callback but also returned a Promise' - code: 'ERR_TEST_FAILURE' - stack: |- - ... -# Subtest: callback throw -not ok 45 - callback throw - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from callback throw' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... -# Subtest: callback called twice -not ok 46 - callback called twice - --- - duration_ms: * - failureType: 'multipleCallbackInvocations' - error: 'callback invoked multiple times' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - ... -# Subtest: callback called twice in different ticks -ok 47 - callback called twice in different ticks - --- - duration_ms: * - ... -# Subtest: callback called twice in future tick -not ok 48 - callback called twice in future tick - --- - duration_ms: * - failureType: 'uncaughtException' - error: 'callback invoked multiple times' - code: 'ERR_TEST_FAILURE' - stack: |- - * - ... -# Subtest: callback async throw -not ok 49 - callback async throw - --- - duration_ms: * - failureType: 'uncaughtException' - error: 'thrown from callback async throw' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - ... -# Subtest: callback async throw after done -ok 50 - callback async throw after done - --- - duration_ms: * - ... -# Subtest: custom inspect symbol fail -not ok 51 - custom inspect symbol fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'customized' - code: 'ERR_TEST_FAILURE' - stack: |- - ... -# Subtest: custom inspect symbol that throws fail -not ok 52 - custom inspect symbol that throws fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: |- - { - foo: 1, - [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] - } - code: 'ERR_TEST_FAILURE' - stack: |- - ... -# Subtest: subtest sync throw fails - # Subtest: sync throw fails at first - not ok 1 - sync throw fails at first - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from subtest sync throw fails at first' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - # Subtest: sync throw fails at second - not ok 2 - sync throw fails at second - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from subtest sync throw fails at second' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - 1..2 -not ok 53 - subtest sync throw fails - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '2 subtests failed' - code: 'ERR_TEST_FAILURE' - stack: |- - ... -# Subtest: describe sync throw fails - # Subtest: should not run - not ok 1 - should not run - --- - duration_ms: * - failureType: 'cancelledByParent' - error: 'test did not finish before its parent and was cancelled' - code: 'ERR_TEST_FAILURE' - ... - 1..1 -not ok 54 - describe sync throw fails - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from describe' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... -# Subtest: describe async throw fails - # Subtest: should not run - not ok 1 - should not run - --- - duration_ms: * - failureType: 'cancelledByParent' - error: 'test did not finish before its parent and was cancelled' - code: 'ERR_TEST_FAILURE' - ... - 1..1 -not ok 55 - describe async throw fails - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from describe' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... -# Subtest: timeouts - # Subtest: timed out async test - not ok 1 - timed out async test - --- - duration_ms: * - failureType: 'testTimeoutFailure' - error: 'test timed out after 5ms' - code: 'ERR_TEST_FAILURE' - stack: |- - async Promise.all (index 0) - ... - # Subtest: timed out callback test - not ok 2 - timed out callback test - --- - duration_ms: * - failureType: 'testTimeoutFailure' - error: 'test timed out after 5ms' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: large timeout async test is ok - ok 3 - large timeout async test is ok - --- - duration_ms: * - ... - # Subtest: large timeout callback test is ok - ok 4 - large timeout callback test is ok - --- - duration_ms: * - ... - 1..4 -not ok 56 - timeouts - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '2 subtests failed' - code: 'ERR_TEST_FAILURE' - ... -# Subtest: successful thenable - # Subtest: successful thenable - ok 1 - successful thenable - --- - duration_ms: * - ... - # Subtest: rejected thenable - not ok 2 - rejected thenable - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'custom error' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - ... - 1..2 -not ok 57 - successful thenable - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '1 subtest failed' - code: 'ERR_TEST_FAILURE' - ... -# Subtest: rejected thenable -not ok 58 - rejected thenable - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'custom error' - code: 'ERR_TEST_FAILURE' - stack: |- - * - ... -# Subtest: invalid subtest fail -not ok 59 - invalid subtest fail - --- - duration_ms: * - failureType: 'parentAlreadyFinished' - error: 'test could not be started because its parent finished' - code: 'ERR_TEST_FAILURE' - stack: |- - * - ... -1..59 -# Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. -# Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. -# Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. -# Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. -# Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. -# Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. -# tests 59 -# pass 22 -# fail 23 -# cancelled 0 -# skipped 9 -# todo 5 -# duration_ms * diff --git a/test/message/test_runner_hooks.js b/test/message/test_runner_hooks.js deleted file mode 100644 index afd6e32..0000000 --- a/test/message/test_runner_hooks.js +++ /dev/null @@ -1,151 +0,0 @@ -// https://github.com/nodejs/node/blob/385d595a4f1d887f6d4221e6071571132498d57c/test/message/test_runner_hooks.js -// Flags: --no-warnings -'use strict' -const common = require('../common') -const assert = require('assert') -const { test, describe, it, before, after, beforeEach, afterEach } = require('#node:test') - -describe('describe hooks', () => { - const testArr = [] - before(function () { - testArr.push('before ' + this.name) - }) - after(function () { - testArr.push('after ' + this.name) - assert.deepStrictEqual(testArr, [ - 'before describe hooks', - 'beforeEach 1', '1', 'afterEach 1', - 'beforeEach 2', '2', 'afterEach 2', - 'beforeEach nested', - 'before nested', - 'beforeEach nested 1', 'nested 1', 'afterEach nested 1', - 'beforeEach nested 2', 'nested 2', 'afterEach nested 2', - 'after nested', - 'afterEach nested', - 'after describe hooks' - ]) - }) - beforeEach(function () { - testArr.push('beforeEach ' + this.name) - }) - afterEach(function () { - testArr.push('afterEach ' + this.name) - }) - - it('1', () => testArr.push('1')) - it('2', () => testArr.push('2')) - - describe('nested', () => { - before(function () { - testArr.push('before ' + this.name) - }) - after(function () { - testArr.push('after ' + this.name) - }) - beforeEach(function () { - testArr.push('beforeEach ' + this.name) - }) - afterEach(function () { - testArr.push('afterEach ' + this.name) - }) - it('nested 1', () => testArr.push('nested 1')) - it('nested 2', () => testArr.push('nested 2')) - }) -}) - -describe('before throws', () => { - before(() => { throw new Error('before') }) - it('1', () => {}) - it('2', () => {}) -}) - -describe('after throws', () => { - after(() => { throw new Error('after') }) - it('1', () => {}) - it('2', () => {}) -}) - -describe('beforeEach throws', () => { - beforeEach(() => { throw new Error('beforeEach') }) - it('1', () => {}) - it('2', () => {}) -}) - -describe('afterEach throws', () => { - afterEach(() => { throw new Error('afterEach') }) - it('1', () => {}) - it('2', () => {}) -}) - -describe('afterEach when test fails', () => { - afterEach(common.mustCall(2)) - it('1', () => { throw new Error('test') }) - it('2', () => {}) -}) - -describe('afterEach throws and test fails', () => { - afterEach(() => { throw new Error('afterEach') }) - it('1', () => { throw new Error('test') }) - it('2', () => {}) -}) - -test('test hooks', async (t) => { - const testArr = [] - - t.after(common.mustCall((t) => testArr.push('after ' + t.name))) - t.beforeEach((t) => testArr.push('beforeEach ' + t.name)) - t.afterEach((t) => testArr.push('afterEach ' + t.name)) - await t.test('1', () => testArr.push('1')) - await t.test('2', () => testArr.push('2')) - - await t.test('nested', async (t) => { - t.beforeEach((t) => testArr.push('nested beforeEach ' + t.name)) - t.afterEach((t) => testArr.push('nested afterEach ' + t.name)) - await t.test('nested 1', () => testArr.push('nested1')) - await t.test('nested 2', () => testArr.push('nested 2')) - }) - - assert.deepStrictEqual(testArr, [ - 'beforeEach 1', '1', 'afterEach 1', - 'beforeEach 2', '2', 'afterEach 2', - 'beforeEach nested', - 'nested beforeEach nested 1', 'nested1', 'nested afterEach nested 1', - 'nested beforeEach nested 2', 'nested 2', 'nested afterEach nested 2', - 'afterEach nested' - ]) -}) - -test('t.beforeEach throws', async (t) => { - t.after(common.mustCall()) - t.beforeEach(() => { throw new Error('beforeEach') }) - await t.test('1', () => {}) - await t.test('2', () => {}) -}) - -test('t.afterEach throws', async (t) => { - t.after(common.mustCall()) - t.afterEach(() => { throw new Error('afterEach') }) - await t.test('1', () => {}) - await t.test('2', () => {}) -}) - -test('afterEach when test fails', async (t) => { - t.after(common.mustCall()) - t.afterEach(common.mustCall(2)) - await t.test('1', () => { throw new Error('test') }) - await t.test('2', () => {}) -}) - -test('afterEach throws and test fails', async (t) => { - t.after(common.mustCall()) - t.afterEach(() => { throw new Error('afterEach') }) - await t.test('1', () => { throw new Error('test') }) - await t.test('2', () => {}) -}) - -test('t.after() is called if test body throws', (t) => { - t.after(() => { - t.diagnostic('- after() called') - }) - throw new Error('bye') -}) diff --git a/test/message/test_runner_hooks.out b/test/message/test_runner_hooks.out deleted file mode 100644 index 7c82e9f..0000000 --- a/test/message/test_runner_hooks.out +++ /dev/null @@ -1,500 +0,0 @@ -TAP version 13 -# Subtest: describe hooks - # Subtest: 1 - ok 1 - 1 - --- - duration_ms: * - ... - # Subtest: 2 - ok 2 - 2 - --- - duration_ms: * - ... - # Subtest: nested - # Subtest: nested 1 - ok 1 - nested 1 - --- - duration_ms: * - ... - # Subtest: nested 2 - ok 2 - nested 2 - --- - duration_ms: * - ... - 1..2 - ok 3 - nested - --- - duration_ms: * - ... - 1..3 -ok 1 - describe hooks - --- - duration_ms: * - ... -# Subtest: before throws - # Subtest: 1 - not ok 1 - 1 - --- - duration_ms: * - failureType: 'cancelledByParent' - error: 'test did not finish before its parent and was cancelled' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: 2 - not ok 2 - 2 - --- - duration_ms: * - failureType: 'cancelledByParent' - error: 'test did not finish before its parent and was cancelled' - code: 'ERR_TEST_FAILURE' - ... - 1..2 -not ok 2 - before throws - --- - duration_ms: * - failureType: 'hookFailed' - error: 'failed running before hook' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - ... -# Subtest: after throws - # Subtest: 1 - ok 1 - 1 - --- - duration_ms: * - ... - # Subtest: 2 - ok 2 - 2 - --- - duration_ms: * - ... - 1..2 -not ok 3 - after throws - --- - duration_ms: * - failureType: 'hookFailed' - error: 'failed running after hook' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - ... -# Subtest: beforeEach throws - # Subtest: 1 - not ok 1 - 1 - --- - duration_ms: * - failureType: 'hookFailed' - error: 'failed running beforeEach hook' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - # Subtest: 2 - not ok 2 - 2 - --- - duration_ms: * - failureType: 'hookFailed' - error: 'failed running beforeEach hook' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - 1..2 -not ok 4 - beforeEach throws - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '2 subtests failed' - code: 'ERR_TEST_FAILURE' - ... -# Subtest: afterEach throws - # Subtest: 1 - not ok 1 - 1 - --- - duration_ms: * - failureType: 'hookFailed' - error: 'failed running afterEach hook' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - # Subtest: 2 - not ok 2 - 2 - --- - duration_ms: * - failureType: 'hookFailed' - error: 'failed running afterEach hook' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - 1..2 -not ok 5 - afterEach throws - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '2 subtests failed' - code: 'ERR_TEST_FAILURE' - ... -# Subtest: afterEach when test fails - # Subtest: 1 - not ok 1 - 1 - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'test' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - # Subtest: 2 - ok 2 - 2 - --- - duration_ms: * - ... - 1..2 -not ok 6 - afterEach when test fails - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '1 subtest failed' - code: 'ERR_TEST_FAILURE' - ... -# Subtest: afterEach throws and test fails - # Subtest: 1 - not ok 1 - 1 - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'test' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - # Subtest: 2 - not ok 2 - 2 - --- - duration_ms: * - failureType: 'hookFailed' - error: 'failed running afterEach hook' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - 1..2 -not ok 7 - afterEach throws and test fails - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '2 subtests failed' - code: 'ERR_TEST_FAILURE' - ... -# Subtest: test hooks - # Subtest: 1 - ok 1 - 1 - --- - duration_ms: * - ... - # Subtest: 2 - ok 2 - 2 - --- - duration_ms: * - ... - # Subtest: nested - # Subtest: nested 1 - ok 1 - nested 1 - --- - duration_ms: * - ... - # Subtest: nested 2 - ok 2 - nested 2 - --- - duration_ms: * - ... - 1..2 - ok 3 - nested - --- - duration_ms: * - ... - 1..3 -ok 8 - test hooks - --- - duration_ms: * - ... -# Subtest: t.beforeEach throws - # Subtest: 1 - not ok 1 - 1 - --- - duration_ms: * - failureType: 'hookFailed' - error: 'failed running beforeEach hook' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - # Subtest: 2 - not ok 2 - 2 - --- - duration_ms: * - failureType: 'hookFailed' - error: 'failed running beforeEach hook' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - 1..2 -not ok 9 - t.beforeEach throws - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '2 subtests failed' - code: 'ERR_TEST_FAILURE' - ... -# Subtest: t.afterEach throws - # Subtest: 1 - not ok 1 - 1 - --- - duration_ms: * - failureType: 'hookFailed' - error: 'failed running afterEach hook' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - # Subtest: 2 - not ok 2 - 2 - --- - duration_ms: * - failureType: 'hookFailed' - error: 'failed running afterEach hook' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - 1..2 -not ok 10 - t.afterEach throws - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '2 subtests failed' - code: 'ERR_TEST_FAILURE' - ... -# Subtest: afterEach when test fails - # Subtest: 1 - not ok 1 - 1 - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'test' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - # Subtest: 2 - ok 2 - 2 - --- - duration_ms: * - ... - 1..2 -not ok 11 - afterEach when test fails - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '1 subtest failed' - code: 'ERR_TEST_FAILURE' - ... -# Subtest: afterEach throws and test fails - # Subtest: 1 - not ok 1 - 1 - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'test' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - # Subtest: 2 - not ok 2 - 2 - --- - duration_ms: * - failureType: 'hookFailed' - error: 'failed running afterEach hook' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - 1..2 -not ok 12 - afterEach throws and test fails - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '2 subtests failed' - code: 'ERR_TEST_FAILURE' - ... -# Subtest: t.after() is called if test body throws -not ok 13 - t.after() is called if test body throws - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'bye' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... -# - after() called -1..13 -# tests 13 -# pass 2 -# fail 11 -# cancelled 0 -# skipped 0 -# todo 0 -# duration_ms * diff --git a/test/message/test_runner_no_refs.js b/test/message/test_runner_no_refs.js deleted file mode 100644 index e3de920..0000000 --- a/test/message/test_runner_no_refs.js +++ /dev/null @@ -1,14 +0,0 @@ -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/message/test_runner_no_refs.js -// Flags: --no-warnings -'use strict' -require('../common') -const test = require('#node:test') - -// When run alone, the test below does not keep the event loop alive. -test('does not keep event loop alive', async (t) => { - await t.test('+does not keep event loop alive', async (t) => { - return new Promise((resolve) => { - setTimeout(resolve, 1000).unref() - }) - }) -}) diff --git a/test/message/test_runner_no_tests.js b/test/message/test_runner_no_tests.js deleted file mode 100644 index f0faabd..0000000 --- a/test/message/test_runner_no_tests.js +++ /dev/null @@ -1,8 +0,0 @@ -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/message/test_runner_no_tests.js -// Flags: --no-warnings -'use strict' -require('../common') -const test = require('#node:test') - -// No TAP output should be generated. -console.log(test.name) diff --git a/test/message/test_runner_only_tests.js b/test/message/test_runner_only_tests.js deleted file mode 100644 index 47f9c44..0000000 --- a/test/message/test_runner_only_tests.js +++ /dev/null @@ -1,49 +0,0 @@ -// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/message/test_runner_only_tests.js -// Flags: --no-warnings --test-only -'use strict' -require('../common') -const test = require('#node:test') - -// These tests should be skipped based on the 'only' option. -test('only = undefined') -test('only = undefined, skip = string', { skip: 'skip message' }) -test('only = undefined, skip = true', { skip: true }) -test('only = undefined, skip = false', { skip: false }) -test('only = false', { only: false }) -test('only = false, skip = string', { only: false, skip: 'skip message' }) -test('only = false, skip = true', { only: false, skip: true }) -test('only = false, skip = false', { only: false, skip: false }) - -// These tests should be skipped based on the 'skip' option. -test('only = true, skip = string', { only: true, skip: 'skip message' }) -test('only = true, skip = true', { only: true, skip: true }) - -// An 'only' test with subtests. -test('only = true, with subtests', { only: true }, async (t) => { - // These subtests should run. - await t.test('running subtest 1') - await t.test('running subtest 2') - - // Switch the context to only execute 'only' tests. - t.runOnly(true) - await t.test('skipped subtest 1') - await t.test('skipped subtest 2') - await t.test('running subtest 3', { only: true }) - - // Switch the context back to execute all tests. - t.runOnly(false) - await t.test('running subtest 4', async (t) => { - // These subtests should run. - await t.test('running sub-subtest 1') - await t.test('running sub-subtest 2') - - // Switch the context to only execute 'only' tests. - t.runOnly(true) - await t.test('skipped sub-subtest 1') - await t.test('skipped sub-subtest 2') - }) - - // Explicitly do not run these tests. - await t.test('skipped subtest 3', { only: false }) - await t.test('skipped subtest 4', { skip: true }) -}) diff --git a/test/message/test_runner_only_tests.out b/test/message/test_runner_only_tests.out deleted file mode 100644 index c471a52..0000000 --- a/test/message/test_runner_only_tests.out +++ /dev/null @@ -1,126 +0,0 @@ -TAP version 13 -# Subtest: only = undefined -ok 1 - only = undefined # SKIP 'only' option not set - --- - duration_ms: * - ... -# Subtest: only = undefined, skip = string -ok 2 - only = undefined, skip = string # SKIP 'only' option not set - --- - duration_ms: * - ... -# Subtest: only = undefined, skip = true -ok 3 - only = undefined, skip = true # SKIP 'only' option not set - --- - duration_ms: * - ... -# Subtest: only = undefined, skip = false -ok 4 - only = undefined, skip = false # SKIP 'only' option not set - --- - duration_ms: * - ... -# Subtest: only = false -ok 5 - only = false # SKIP 'only' option not set - --- - duration_ms: * - ... -# Subtest: only = false, skip = string -ok 6 - only = false, skip = string # SKIP 'only' option not set - --- - duration_ms: * - ... -# Subtest: only = false, skip = true -ok 7 - only = false, skip = true # SKIP 'only' option not set - --- - duration_ms: * - ... -# Subtest: only = false, skip = false -ok 8 - only = false, skip = false # SKIP 'only' option not set - --- - duration_ms: * - ... -# Subtest: only = true, skip = string -ok 9 - only = true, skip = string # SKIP skip message - --- - duration_ms: * - ... -# Subtest: only = true, skip = true -ok 10 - only = true, skip = true # SKIP - --- - duration_ms: * - ... -# Subtest: only = true, with subtests - # Subtest: running subtest 1 - ok 1 - running subtest 1 - --- - duration_ms: * - ... - # Subtest: running subtest 2 - ok 2 - running subtest 2 - --- - duration_ms: * - ... - # Subtest: skipped subtest 1 - ok 3 - skipped subtest 1 # SKIP 'only' option not set - --- - duration_ms: * - ... - # Subtest: skipped subtest 2 - ok 4 - skipped subtest 2 # SKIP 'only' option not set - --- - duration_ms: * - ... - # Subtest: running subtest 3 - ok 5 - running subtest 3 - --- - duration_ms: * - ... - # Subtest: running subtest 4 - # Subtest: running sub-subtest 1 - ok 1 - running sub-subtest 1 - --- - duration_ms: * - ... - # Subtest: running sub-subtest 2 - ok 2 - running sub-subtest 2 - --- - duration_ms: * - ... - # Subtest: skipped sub-subtest 1 - ok 3 - skipped sub-subtest 1 # SKIP 'only' option not set - --- - duration_ms: * - ... - # Subtest: skipped sub-subtest 2 - ok 4 - skipped sub-subtest 2 # SKIP 'only' option not set - --- - duration_ms: * - ... - 1..4 - ok 6 - running subtest 4 - --- - duration_ms: * - ... - # Subtest: skipped subtest 3 - ok 7 - skipped subtest 3 # SKIP 'only' option not set - --- - duration_ms: * - ... - # Subtest: skipped subtest 4 - ok 8 - skipped subtest 4 # SKIP - --- - duration_ms: * - ... - 1..8 -ok 11 - only = true, with subtests - --- - duration_ms: * - ... -1..11 -# tests 11 -# pass 1 -# fail 0 -# cancelled 0 -# skipped 10 -# todo 0 -# duration_ms * diff --git a/test/message/test_runner_output.js b/test/message/test_runner_output.js deleted file mode 100644 index 44d5141..0000000 --- a/test/message/test_runner_output.js +++ /dev/null @@ -1,385 +0,0 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/test/message/test_runner_output.js -// Flags: --no-warnings -'use strict' -require('../common') -const assert = require('node:assert') -const test = require('#node:test') -const util = require('util') - -test('sync pass todo', (t) => { - t.todo() -}) - -test('sync pass todo with message', (t) => { - t.todo('this is a passing todo') -}) - -test('sync fail todo', (t) => { - t.todo() - throw new Error('thrown from sync fail todo') -}) - -test('sync fail todo with message', (t) => { - t.todo('this is a failing todo') - throw new Error('thrown from sync fail todo with message') -}) - -test('sync skip pass', (t) => { - t.skip() -}) - -test('sync skip pass with message', (t) => { - t.skip('this is skipped') -}) - -test('sync pass', (t) => { - t.diagnostic('this test should pass') -}) - -test('sync throw fail', () => { - throw new Error('thrown from sync throw fail') -}) - -test('async skip pass', async (t) => { - t.skip() -}) - -test('async pass', async () => { - -}) - -test('async throw fail', async () => { - throw new Error('thrown from async throw fail') -}) - -test('async skip fail', async (t) => { - t.skip() - throw new Error('thrown from async throw fail') -}) - -test('async assertion fail', async () => { - // Make sure the assert module is handled. - assert.strictEqual(true, false) -}) - -test('resolve pass', () => { - return Promise.resolve() -}) - -test('reject fail', () => { - return Promise.reject(new Error('rejected from reject fail')) -}) - -test('unhandled rejection - passes but warns', () => { - Promise.reject(new Error('rejected from unhandled rejection fail')) -}) - -test('async unhandled rejection - passes but warns', async () => { - Promise.reject(new Error('rejected from async unhandled rejection fail')) -}) - -test('immediate throw - passes but warns', () => { - setImmediate(() => { - throw new Error('thrown from immediate throw fail') - }) -}) - -test('immediate reject - passes but warns', () => { - setImmediate(() => { - Promise.reject(new Error('rejected from immediate reject fail')) - }) -}) - -test('immediate resolve pass', () => { - return new Promise((resolve) => { - setImmediate(() => { - resolve() - }) - }) -}) - -test('subtest sync throw fail', async (t) => { - await t.test('+sync throw fail', (t) => { - t.diagnostic('this subtest should make its parent test fail') - throw new Error('thrown from subtest sync throw fail') - }) -}) - -test('sync throw non-error fail', async (t) => { - throw Symbol('thrown symbol from sync throw non-error fail') -}) - -test('level 0a', { concurrency: 4 }, async (t) => { - t.test('level 1a', async (t) => { - const p1a = new Promise((resolve) => { - setTimeout(() => { - resolve() - }, 100) - }) - - return p1a - }) - - t.test('level 1b', async (t) => { - const p1b = new Promise((resolve) => { - resolve() - }) - - return p1b - }) - - t.test('level 1c', async (t) => { - const p1c = new Promise((resolve) => { - setTimeout(() => { - resolve() - }, 200) - }) - - return p1c - }) - - t.test('level 1d', async (t) => { - const p1c = new Promise((resolve) => { - setTimeout(() => { - resolve() - }, 150) - }) - - return p1c - }) - - const p0a = new Promise((resolve) => { - setTimeout(() => { - resolve() - }, 300) - }) - - return p0a -}) - -test('top level', { concurrency: 2 }, async (t) => { - t.test('+long running', async (t) => { - return new Promise((resolve, reject) => { - setTimeout(resolve, 300).unref() - }) - }) - - t.test('+short running', async (t) => { - t.test('++short running', async (t) => {}) - }) -}) - -test('invalid subtest - pass but subtest fails', (t) => { - setImmediate(() => { - t.test('invalid subtest fail', () => { - throw new Error('this should not be thrown') - }) - }) -}) - -test('sync skip option', { skip: true }, (t) => { - throw new Error('this should not be executed') -}) - -test('sync skip option with message', { skip: 'this is skipped' }, (t) => { - throw new Error('this should not be executed') -}) - -test('sync skip option is false fail', { skip: false }, (t) => { - throw new Error('this should be executed') -}) - -// A test with no arguments provided. -test() - -// A test with only a named function provided. -test(function functionOnly () {}) - -// A test with only an anonymous function provided. -test(() => {}) - -// A test with only a name provided. -test('test with only a name provided') - -// A test with an empty string name. -test('') - -// A test with only options provided. -test({ skip: true }) - -// A test with only a name and options provided. -test('test with a name and options provided', { skip: true }) - -// A test with only options and a function provided. -test({ skip: true }, function functionAndOptions () {}) - -// A test whose description needs to be escaped. -test('escaped description \\ # \\#\\ \n \t \f \v \b \r') - -// A test whose skip message needs to be escaped. -test('escaped skip message', { skip: '#skip' }) - -// A test whose todo message needs to be escaped. -test('escaped todo message', { todo: '#todo' }) - -// A test with a diagnostic message that needs to be escaped. -test('escaped diagnostic', (t) => { - t.diagnostic('#diagnostic') -}) - -test('callback pass', (t, done) => { - setImmediate(done) -}) - -test('callback fail', (t, done) => { - setImmediate(() => { - done(new Error('callback failure')) - }) -}) - -test('sync t is this in test', function (t) { - assert.strictEqual(this, t) -}) - -test('async t is this in test', async function (t) { - assert.strictEqual(this, t) -}) - -test('callback t is this in test', function (t, done) { - assert.strictEqual(this, t) - done() -}) - -test('callback also returns a Promise', async (t, done) => { - throw new Error('thrown from callback also returns a Promise') -}) - -test('callback throw', (t, done) => { - throw new Error('thrown from callback throw') -}) - -test('callback called twice', (t, done) => { - done() - done() -}) - -test('callback called twice in different ticks', (t, done) => { - setImmediate(done) - done() -}) - -test('callback called twice in future tick', (t, done) => { - setImmediate(() => { - done() - done() - }) -}) - -test('callback async throw', (t, done) => { - setImmediate(() => { - throw new Error('thrown from callback async throw') - }) -}) - -test('callback async throw after done', (t, done) => { - setImmediate(() => { - throw new Error('thrown from callback async throw after done') - }) - - done() -}) - -test('only is set but not in only mode', { only: true }, async (t) => { - // All of these subtests should run. - await t.test('running subtest 1') - t.runOnly(true) - await t.test('running subtest 2') - await t.test('running subtest 3', { only: true }) - t.runOnly(false) - await t.test('running subtest 4') -}) - -test('custom inspect symbol fail', () => { - const obj = { - [util.inspect.custom] () { - return 'customized' - }, - foo: 1 - } - - throw obj -}) - -test('custom inspect symbol that throws fail', () => { - const obj = { - [util.inspect.custom] () { - throw new Error('bad-inspect') - }, - foo: 1 - } - - throw obj -}) - -test('subtest sync throw fails', async (t) => { - await t.test('sync throw fails at first', (t) => { - throw new Error('thrown from subtest sync throw fails at first') - }) - await t.test('sync throw fails at second', (t) => { - throw new Error('thrown from subtest sync throw fails at second') - }) -}) - -test('timed out async test', { timeout: 5 }, async (t) => { - return new Promise((resolve) => { - setTimeout(resolve, 100) - }) -}) - -test('timed out callback test', { timeout: 5 }, (t, done) => { - setTimeout(done, 100) -}) - -test('large timeout async test is ok', { timeout: 30_000_000 }, async (t) => { - return new Promise((resolve) => { - setTimeout(resolve, 10) - }) -}) - -test('large timeout callback test is ok', { timeout: 30_000_000 }, (t, done) => { - setTimeout(done, 10) -}) - -test('successful thenable', () => { - let thenCalled = false - return { - get then () { - if (thenCalled) throw new Error() - thenCalled = true - return (successHandler) => successHandler() - } - } -}) - -test('rejected thenable', () => { - let thenCalled = false - return { - get then () { - if (thenCalled) throw new Error() - thenCalled = true - return (_, errorHandler) => errorHandler('custom error') - } - } -}) - -test('unfinished test with uncaughtException', async () => { - await new Promise(() => { - setTimeout(() => { throw new Error('foo') }) - }) -}) - -test('unfinished test with unhandledRejection', async () => { - await new Promise(() => { - setTimeout(() => Promise.reject(new Error('bar'))) - }) -}) diff --git a/test/message/test_runner_output_cli.js b/test/message/test_runner_output_cli.js deleted file mode 100644 index ab37ddf..0000000 --- a/test/message/test_runner_output_cli.js +++ /dev/null @@ -1,8 +0,0 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/test/message/test_runner_output_cli.js -// Flags: --no-warnings -'use strict' -require('../common') -const spawn = require('node:child_process').spawn -spawn(process.execPath, - ['--no-warnings', '--test', '--test-reporter', 'tap', 'test/message/test_runner_output.js'], - { stdio: 'inherit' }) diff --git a/test/message/test_runner_output_cli.out b/test/message/test_runner_output_cli.out deleted file mode 100644 index 74b3ef7..0000000 --- a/test/message/test_runner_output_cli.out +++ /dev/null @@ -1,656 +0,0 @@ -TAP version 13 -# Subtest: *test_runner_output.js - # Subtest: sync pass todo - ok 1 - sync pass todo # TODO - --- - duration_ms: * - ... - # Subtest: sync pass todo with message - ok 2 - sync pass todo with message # TODO this is a passing todo - --- - duration_ms: * - ... - # Subtest: sync fail todo - not ok 3 - sync fail todo # TODO - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from sync fail todo' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... - # Subtest: sync fail todo with message - not ok 4 - sync fail todo with message # TODO this is a failing todo - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from sync fail todo with message' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... - # Subtest: sync skip pass - ok 5 - sync skip pass # SKIP - --- - duration_ms: * - ... - # Subtest: sync skip pass with message - ok 6 - sync skip pass with message # SKIP this is skipped - --- - duration_ms: * - ... - # Subtest: sync pass - ok 7 - sync pass - --- - duration_ms: * - ... - # this test should pass - # Subtest: sync throw fail - not ok 8 - sync throw fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from sync throw fail' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... - # Subtest: async skip pass - ok 9 - async skip pass # SKIP - --- - duration_ms: * - ... - # Subtest: async pass - ok 10 - async pass - --- - duration_ms: * - ... - # Subtest: async throw fail - not ok 11 - async throw fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from async throw fail' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... - # Subtest: async skip fail - not ok 12 - async skip fail # SKIP - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from async throw fail' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... - # Subtest: async assertion fail - not ok 13 - async assertion fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: |- - Expected values to be strictly equal: - - true !== false - - code: 'ERR_ASSERTION' - expected: false - actual: true - operator: 'strictEqual' - stack: |- - * - * - * - * - * - * - * - ... - # Subtest: resolve pass - ok 14 - resolve pass - --- - duration_ms: * - ... - # Subtest: reject fail - not ok 15 - reject fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'rejected from reject fail' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... - # Subtest: unhandled rejection - passes but warns - ok 16 - unhandled rejection - passes but warns - --- - duration_ms: * - ... - # Subtest: async unhandled rejection - passes but warns - ok 17 - async unhandled rejection - passes but warns - --- - duration_ms: * - ... - # Subtest: immediate throw - passes but warns - ok 18 - immediate throw - passes but warns - --- - duration_ms: * - ... - # Subtest: immediate reject - passes but warns - ok 19 - immediate reject - passes but warns - --- - duration_ms: * - ... - # Subtest: immediate resolve pass - ok 20 - immediate resolve pass - --- - duration_ms: * - ... - # Subtest: subtest sync throw fail - # Subtest: +sync throw fail - not ok 1 - +sync throw fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from subtest sync throw fail' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - # this subtest should make its parent test fail - 1..1 - not ok 21 - subtest sync throw fail - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '1 subtest failed' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: sync throw non-error fail - not ok 22 - sync throw non-error fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'Symbol(thrown symbol from sync throw non-error fail)' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: level 0a - # Subtest: level 1a - ok 1 - level 1a - --- - duration_ms: * - ... - # Subtest: level 1b - ok 2 - level 1b - --- - duration_ms: * - ... - # Subtest: level 1c - ok 3 - level 1c - --- - duration_ms: * - ... - # Subtest: level 1d - ok 4 - level 1d - --- - duration_ms: * - ... - 1..4 - ok 23 - level 0a - --- - duration_ms: * - ... - # Subtest: top level - # Subtest: +long running - not ok 1 - +long running - --- - duration_ms: * - failureType: 'cancelledByParent' - error: 'test did not finish before its parent and was cancelled' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: +short running - # Subtest: ++short running - ok 1 - ++short running - --- - duration_ms: * - ... - 1..1 - ok 2 - +short running - --- - duration_ms: * - ... - 1..2 - not ok 24 - top level - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '1 subtest failed' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: invalid subtest - pass but subtest fails - ok 25 - invalid subtest - pass but subtest fails - --- - duration_ms: * - ... - # Subtest: sync skip option - ok 26 - sync skip option # SKIP - --- - duration_ms: * - ... - # Subtest: sync skip option with message - ok 27 - sync skip option with message # SKIP this is skipped - --- - duration_ms: * - ... - # Subtest: sync skip option is false fail - not ok 28 - sync skip option is false fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'this should be executed' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... - # Subtest: - ok 29 - - --- - duration_ms: * - ... - # Subtest: functionOnly - ok 30 - functionOnly - --- - duration_ms: * - ... - # Subtest: - ok 31 - - --- - duration_ms: * - ... - # Subtest: test with only a name provided - ok 32 - test with only a name provided - --- - duration_ms: * - ... - # Subtest: - ok 33 - - --- - duration_ms: * - ... - # Subtest: - ok 34 - # SKIP - --- - duration_ms: * - ... - # Subtest: test with a name and options provided - ok 35 - test with a name and options provided # SKIP - --- - duration_ms: * - ... - # Subtest: functionAndOptions - ok 36 - functionAndOptions # SKIP - --- - duration_ms: * - ... - # Subtest: escaped description \\ \# \\\#\\ \\n \\t \\f \\v \\b \\r - ok 37 - escaped description \\ \# \\\#\\ \\n \\t \\f \\v \\b \\r - --- - duration_ms: * - ... - # Subtest: escaped skip message - ok 38 - escaped skip message # SKIP \#skip - --- - duration_ms: * - ... - # Subtest: escaped todo message - ok 39 - escaped todo message # TODO \#todo - --- - duration_ms: * - ... - # Subtest: escaped diagnostic - ok 40 - escaped diagnostic - --- - duration_ms: * - ... - # \#diagnostic - # Subtest: callback pass - ok 41 - callback pass - --- - duration_ms: * - ... - # Subtest: callback fail - not ok 42 - callback fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'callback failure' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - ... - # Subtest: sync t is this in test - ok 43 - sync t is this in test - --- - duration_ms: * - ... - # Subtest: async t is this in test - ok 44 - async t is this in test - --- - duration_ms: * - ... - # Subtest: callback t is this in test - ok 45 - callback t is this in test - --- - duration_ms: * - ... - # Subtest: callback also returns a Promise - not ok 46 - callback also returns a Promise - --- - duration_ms: * - failureType: 'callbackAndPromisePresent' - error: 'passed a callback but also returned a Promise' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: callback throw - not ok 47 - callback throw - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from callback throw' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - ... - # Subtest: callback called twice - not ok 48 - callback called twice - --- - duration_ms: * - failureType: 'multipleCallbackInvocations' - error: 'callback invoked multiple times' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - ... - # Subtest: callback called twice in different ticks - ok 49 - callback called twice in different ticks - --- - duration_ms: * - ... - # Subtest: callback called twice in future tick - not ok 50 - callback called twice in future tick - --- - duration_ms: * - failureType: 'uncaughtException' - error: 'callback invoked multiple times' - code: 'ERR_TEST_FAILURE' - stack: |- - * - ... - # Subtest: callback async throw - not ok 51 - callback async throw - --- - duration_ms: * - failureType: 'uncaughtException' - error: 'thrown from callback async throw' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - ... - # Subtest: callback async throw after done - ok 52 - callback async throw after done - --- - duration_ms: * - ... - # Subtest: only is set but not in only mode - # Subtest: running subtest 1 - ok 1 - running subtest 1 - --- - duration_ms: * - ... - # Subtest: running subtest 2 - ok 2 - running subtest 2 - --- - duration_ms: * - ... - # Subtest: running subtest 3 - ok 3 - running subtest 3 - --- - duration_ms: * - ... - # Subtest: running subtest 4 - ok 4 - running subtest 4 - --- - duration_ms: * - ... - 1..4 - ok 53 - only is set but not in only mode - --- - duration_ms: * - ... - # Subtest: custom inspect symbol fail - not ok 54 - custom inspect symbol fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'customized' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: custom inspect symbol that throws fail - not ok 55 - custom inspect symbol that throws fail - --- - duration_ms: * - failureType: 'testCodeFailure' - error: |- - { - foo: 1, - [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] - } - code: 'ERR_TEST_FAILURE' - ... - # Subtest: subtest sync throw fails - # Subtest: sync throw fails at first - not ok 1 - sync throw fails at first - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from subtest sync throw fails at first' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - # Subtest: sync throw fails at second - not ok 2 - sync throw fails at second - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'thrown from subtest sync throw fails at second' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - * - * - * - * - * - * - * - ... - 1..2 - not ok 56 - subtest sync throw fails - --- - duration_ms: * - failureType: 'subtestsFailed' - error: '2 subtests failed' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: timed out async test - not ok 57 - timed out async test - --- - duration_ms: * - failureType: 'testTimeoutFailure' - error: 'test timed out after 5ms' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: timed out callback test - not ok 58 - timed out callback test - --- - duration_ms: * - failureType: 'testTimeoutFailure' - error: 'test timed out after 5ms' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: large timeout async test is ok - ok 59 - large timeout async test is ok - --- - duration_ms: * - ... - # Subtest: large timeout callback test is ok - ok 60 - large timeout callback test is ok - --- - duration_ms: * - ... - # Subtest: successful thenable - ok 61 - successful thenable - --- - duration_ms: * - ... - # Subtest: rejected thenable - not ok 62 - rejected thenable - --- - duration_ms: * - failureType: 'testCodeFailure' - error: 'custom error' - code: 'ERR_TEST_FAILURE' - ... - # Subtest: unfinished test with uncaughtException - not ok 63 - unfinished test with uncaughtException - --- - duration_ms: * - failureType: 'uncaughtException' - error: 'foo' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - ... - # Subtest: unfinished test with unhandledRejection - not ok 64 - unfinished test with unhandledRejection - --- - duration_ms: * - failureType: 'unhandledRejection' - error: 'bar' - code: 'ERR_TEST_FAILURE' - stack: |- - * - * - * - ... - # Subtest: invalid subtest fail - not ok 65 - invalid subtest fail - --- - duration_ms: * - failureType: 'parentAlreadyFinished' - error: 'test could not be started because its parent finished' - code: 'ERR_TEST_FAILURE' - stack: |- - * - ... - 1..65 - # Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. - # Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. - # Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. - # Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. - # Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. - # Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. -not ok 1 - *test_runner_output.js - --- - duration_ms: * - failureType: 'subtestsFailed' - exitCode: 1 - error: 'test failed' - code: * - ... -1..1 -# tests 1 -# pass 0 -# fail 1 -# cancelled 0 -# skipped 0 -# todo 0 -# duration_ms * diff --git a/test/message/test_runner_output_dot_reporter.js b/test/message/test_runner_output_dot_reporter.js deleted file mode 100644 index 3ece578..0000000 --- a/test/message/test_runner_output_dot_reporter.js +++ /dev/null @@ -1,7 +0,0 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/test/message/test_runner_output_dot_reporter.js -// Flags: --no-warnings -'use strict' -require('../common') -const spawn = require('node:child_process').spawn -spawn(process.execPath, - ['--no-warnings', '--test-reporter', 'dot', 'test/message/test_runner_output.js'], { stdio: 'inherit' }) diff --git a/test/message/test_runner_output_dot_reporter.out b/test/message/test_runner_output_dot_reporter.out deleted file mode 100644 index 823ecfb..0000000 --- a/test/message/test_runner_output_dot_reporter.out +++ /dev/null @@ -1,4 +0,0 @@ -..XX...X..XXX.X..... -XXX.....X..X...X.... -.........X...XXX.XX. -.....XXXXXXX...XXXX diff --git a/test/message/test_runner_output_spec_reporter.js b/test/message/test_runner_output_spec_reporter.js deleted file mode 100644 index 9373622..0000000 --- a/test/message/test_runner_output_spec_reporter.js +++ /dev/null @@ -1,11 +0,0 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/test/message/test_runner_output_spec_reporter.js -// Flags: --no-warnings -'use strict' -require('../common') -const spawn = require('node:child_process').spawn -const child = spawn(process.execPath, - ['--no-warnings', '--test-reporter', 'spec', 'test/message/test_runner_output.js'], - { stdio: 'pipe' }) -// eslint-disable-next-line no-control-regex -child.stdout.on('data', (d) => process.stdout.write(d.toString().replace(/[^\x00-\x7F]/g, '').replace(/\u001b\[\d+m/g, ''))) -child.stderr.pipe(process.stderr) diff --git a/test/message/test_runner_output_spec_reporter.out b/test/message/test_runner_output_spec_reporter.out deleted file mode 100644 index 956a63d..0000000 --- a/test/message/test_runner_output_spec_reporter.out +++ /dev/null @@ -1,260 +0,0 @@ -sync pass todo (*ms) - sync pass todo with message (*ms) - sync fail todo (*ms) - Error: thrown from sync fail todo - * - * - * - * - * - * - * - * - sync fail todo with message (*ms) - Error: thrown from sync fail todo with message - * - * - * - * - * - * - * - * - sync skip pass (*ms) - sync skip pass with message (*ms) - sync pass (*ms) - this test should pass - sync throw fail (*ms) - Error: thrown from sync throw fail - * - * - * - * - * - * - * - * - async skip pass (*ms) - async pass (*ms) - async throw fail (*ms) - Error: thrown from async throw fail - * - * - * - * - * - * - * - * - async skip fail (*ms) - Error: thrown from async throw fail - * - * - * - * - * - * - * - * - async assertion fail (*ms) - AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: - - true !== false - - * - * - * - * - * - * - * - * { - generatedMessage: true, - code: 'ERR_ASSERTION', - actual: true, - expected: false, - operator: 'strictEqual' - } - resolve pass (*ms) - reject fail (*ms) - Error: rejected from reject fail - * - * - * - * - * - * - * - * - unhandled rejection - passes but warns (*ms) - async unhandled rejection - passes but warns (*ms) - immediate throw - passes but warns (*ms) - immediate reject - passes but warns (*ms) - immediate resolve pass (*ms) - subtest sync throw fail - +sync throw fail (*ms) - Error: thrown from subtest sync throw fail - * - * - * - * - * - * - * - * - * - * - this subtest should make its parent test fail - subtest sync throw fail (*ms) - sync throw non-error fail (*ms) - Symbol(thrown symbol from sync throw non-error fail) - level 0a - level 1a (*ms) - level 1b (*ms) - level 1c (*ms) - level 1d (*ms) - level 0a (*ms) - top level - +long running (*ms) - 'test did not finish before its parent and was cancelled' - +short running - ++short running (*ms) - +short running (*ms) - top level (*ms) - invalid subtest - pass but subtest fails (*ms) - sync skip option (*ms) - sync skip option with message (*ms) - sync skip option is false fail (*ms) - Error: this should be executed - * - * - * - * - * - * - * - * - (*ms) - functionOnly (*ms) - (*ms) - test with only a name provided (*ms) - (*ms) - (*ms) - test with a name and options provided (*ms) - functionAndOptions (*ms) - escaped description \ # * -  (*ms) - escaped skip message (*ms) - escaped todo message (*ms) - escaped diagnostic (*ms) - #diagnostic - callback pass (*ms) - callback fail (*ms) - Error: callback failure - * - * - sync t is this in test (*ms) - async t is this in test (*ms) - callback t is this in test (*ms) - callback also returns a Promise (*ms) - 'passed a callback but also returned a Promise' - callback throw (*ms) - Error: thrown from callback throw - * - * - * - * - * - * - * - * - callback called twice (*ms) - 'callback invoked multiple times' - callback called twice in different ticks (*ms) - callback called twice in future tick (*ms) - Error: callback invoked multiple times - * - * - * - * - * - * - failureType: 'multipleCallbackInvocations', - cause: 'callback invoked multiple times', - code: 'ERR_TEST_FAILURE' - } - callback async throw (*ms) - Error: thrown from callback async throw - * - * - callback async throw after done (*ms) - only is set but not in only mode - running subtest 1 (*ms) - running subtest 2 (*ms) - running subtest 3 (*ms) - running subtest 4 (*ms) - only is set but not in only mode (*ms) - custom inspect symbol fail (*ms) - customized - custom inspect symbol that throws fail (*ms) - { foo: 1, [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] } - subtest sync throw fails - sync throw fails at first (*ms) - Error: thrown from subtest sync throw fails at first - * - * - * - * - * - * - * - * - * - * - sync throw fails at second (*ms) - Error: thrown from subtest sync throw fails at second - * - * - * - * - * - * - * - * - * - * - subtest sync throw fails (*ms) - timed out async test (*ms) - 'test timed out after *ms' - timed out callback test (*ms) - 'test timed out after *ms' - large timeout async test is ok (*ms) - large timeout callback test is ok (*ms) - successful thenable (*ms) - rejected thenable (*ms) - 'custom error' - unfinished test with uncaughtException (*ms) - Error: foo - * - * - * - unfinished test with unhandledRejection (*ms) - Error: bar - * - * - * - invalid subtest fail (*ms) - 'test could not be started because its parent finished' - Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. - Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. - Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. - Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. - Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. - Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. - tests 65 - pass 27 - fail 21 - cancelled 2 - skipped 10 - todo 5 - duration_ms * diff --git a/test/message/test_runner_test_name_pattern.js b/test/message/test_runner_test_name_pattern.js deleted file mode 100644 index 5f7c25b..0000000 --- a/test/message/test_runner_test_name_pattern.js +++ /dev/null @@ -1,48 +0,0 @@ -// https://github.com/nodejs/node/blob/a69a30016cf3395b0bd775c1340ab6ecbac58296/test/message/test_runner_test_name_pattern.js -// Flags: --no-warnings --test-name-pattern=enabled --test-name-pattern=/pattern/i -'use strict' -const common = require('../common') -const { - after, - afterEach, - before, - beforeEach, - describe, - it, - test -} = require('#node:test') - -test('top level test disabled', common.mustNotCall()) -test('top level skipped test disabled', { skip: true }, common.mustNotCall()) -test('top level skipped test enabled', { skip: true }, common.mustNotCall()) -it('top level it enabled', common.mustCall()) -it('top level it disabled', common.mustNotCall()) -it.skip('top level skipped it disabled', common.mustNotCall()) -it.skip('top level skipped it enabled', common.mustNotCall()) -describe('top level describe disabled', common.mustNotCall()) -describe.skip('top level skipped describe disabled', common.mustNotCall()) -describe.skip('top level skipped describe enabled', common.mustNotCall()) -test('top level runs because name includes PaTtErN', common.mustCall()) - -test('top level test enabled', common.mustCall(async (t) => { - t.beforeEach(common.mustCall()) - t.afterEach(common.mustCall()) - await t.test( - 'nested test runs because name includes PATTERN', - common.mustCall() - ) -})) - -describe('top level describe enabled', () => { - before(common.mustCall()) - beforeEach(common.mustCall(4)) - afterEach(common.mustCall(4)) - after(common.mustCall()) - - it('nested it disabled', common.mustNotCall()) - it('nested it enabled', common.mustCall()) - describe('nested describe disabled', common.mustNotCall()) - describe('nested describe enabled', common.mustCall(() => { - it('is enabled', common.mustCall()) - })) -}) diff --git a/test/message/test_runner_test_name_pattern.out b/test/message/test_runner_test_name_pattern.out deleted file mode 100644 index be548ad..0000000 --- a/test/message/test_runner_test_name_pattern.out +++ /dev/null @@ -1,107 +0,0 @@ -TAP version 13 -# Subtest: top level test disabled -ok 1 - top level test disabled # SKIP test name does not match pattern - --- - duration_ms: * - ... -# Subtest: top level skipped test disabled -ok 2 - top level skipped test disabled # SKIP test name does not match pattern - --- - duration_ms: * - ... -# Subtest: top level skipped test enabled -ok 3 - top level skipped test enabled # SKIP - --- - duration_ms: * - ... -# Subtest: top level it enabled -ok 4 - top level it enabled - --- - duration_ms: * - ... -# Subtest: top level it disabled -ok 5 - top level it disabled # SKIP test name does not match pattern - --- - duration_ms: * - ... -# Subtest: top level skipped it disabled -ok 6 - top level skipped it disabled # SKIP test name does not match pattern - --- - duration_ms: * - ... -# Subtest: top level skipped it enabled -ok 7 - top level skipped it enabled # SKIP - --- - duration_ms: * - ... -# Subtest: top level describe disabled -ok 8 - top level describe disabled # SKIP test name does not match pattern - --- - duration_ms: * - ... -# Subtest: top level skipped describe disabled -ok 9 - top level skipped describe disabled # SKIP test name does not match pattern - --- - duration_ms: * - ... -# Subtest: top level skipped describe enabled -ok 10 - top level skipped describe enabled # SKIP - --- - duration_ms: * - ... -# Subtest: top level runs because name includes PaTtErN -ok 11 - top level runs because name includes PaTtErN - --- - duration_ms: * - ... -# Subtest: top level test enabled - # Subtest: nested test runs because name includes PATTERN - ok 1 - nested test runs because name includes PATTERN - --- - duration_ms: * - ... - 1..1 -ok 12 - top level test enabled - --- - duration_ms: * - ... -# Subtest: top level describe enabled - # Subtest: nested it disabled - ok 1 - nested it disabled # SKIP test name does not match pattern - --- - duration_ms: * - ... - # Subtest: nested it enabled - ok 2 - nested it enabled - --- - duration_ms: * - ... - # Subtest: nested describe disabled - ok 3 - nested describe disabled # SKIP test name does not match pattern - --- - duration_ms: * - ... - # Subtest: nested describe enabled - # Subtest: is enabled - ok 1 - is enabled - --- - duration_ms: * - ... - 1..1 - ok 4 - nested describe enabled - --- - duration_ms: * - ... - 1..4 -ok 13 - top level describe enabled - --- - duration_ms: * - ... -1..13 -# tests 13 -# pass 4 -# fail 0 -# cancelled 0 -# skipped 9 -# todo 0 -# duration_ms * diff --git a/test/message/test_runner_test_name_pattern_with_only.js b/test/message/test_runner_test_name_pattern_with_only.js deleted file mode 100644 index 4f2d6fb..0000000 --- a/test/message/test_runner_test_name_pattern_with_only.js +++ /dev/null @@ -1,14 +0,0 @@ -// https://github.com/nodejs/node/blob/87170c3f9271da947a7b33d0696ec4cf8aab6eb6/test/message/test_runner_test_name_pattern_with_only.js -// Flags: --no-warnings --test-only --test-name-pattern=enabled -'use strict' -const common = require('../common') -const { test } = require('#node:test') - -test('enabled and only', { only: true }, common.mustCall(async (t) => { - await t.test('enabled', common.mustCall()) - await t.test('disabled', common.mustNotCall()) -})) - -test('enabled but not only', common.mustNotCall()) -test('only does not match pattern', { only: true }, common.mustNotCall()) -test('not only and does not match pattern', common.mustNotCall()) diff --git a/test/message/test_runner_test_name_pattern_with_only.out b/test/message/test_runner_test_name_pattern_with_only.out deleted file mode 100644 index 2e10064..0000000 --- a/test/message/test_runner_test_name_pattern_with_only.out +++ /dev/null @@ -1,40 +0,0 @@ -TAP version 13 -# Subtest: enabled and only - # Subtest: enabled - ok 1 - enabled - --- - duration_ms: * - ... - # Subtest: disabled - ok 2 - disabled # SKIP test name does not match pattern - --- - duration_ms: * - ... - 1..2 -ok 1 - enabled and only - --- - duration_ms: * - ... -# Subtest: enabled but not only -ok 2 - enabled but not only # SKIP 'only' option not set - --- - duration_ms: * - ... -# Subtest: only does not match pattern -ok 3 - only does not match pattern # SKIP test name does not match pattern - --- - duration_ms: * - ... -# Subtest: not only and does not match pattern -ok 4 - not only and does not match pattern # SKIP 'only' option not set - --- - duration_ms: * - ... -1..4 -# tests 4 -# pass 1 -# fail 0 -# cancelled 0 -# skipped 3 -# todo 0 -# duration_ms * diff --git a/test/message/test_runner_unresolved_promise.js b/test/message/test_runner_unresolved_promise.js deleted file mode 100644 index 842ec5b..0000000 --- a/test/message/test_runner_unresolved_promise.js +++ /dev/null @@ -1,9 +0,0 @@ -// https://github.com/nodejs/node/blob/5ec2d7bc5deed26ac640feff279800e39dacc9c0/test/message/test_runner_unresolved_promise.js -// Flags: --no-warnings -'use strict' -require('../common') -const test = require('#node:test') - -test('pass') -test('never resolving promise', () => new Promise(() => {})) -test('fail', () => console.log('this should not appear')) diff --git a/test/node-core-test.js b/test/node-core-test.js deleted file mode 100644 index 53a274c..0000000 --- a/test/node-core-test.js +++ /dev/null @@ -1,81 +0,0 @@ -const { spawn, spawnSync } = require('node:child_process') -const { join, resolve } = require('node:path') -const assert = require('node:assert') - -const test = require('#node:test') - -const { bin } = require('../package.json') - -const binPath = resolve(__dirname, '..', bin.test) -const nodeDashDashTestPath = resolve(__dirname, '..', bin['node--test']) -const fixturesDir = join(__dirname, 'fixtures', 'node-core-test') - -test('should execute the tests when run as node--test --test', () => { - const args = [nodeDashDashTestPath, '--test', fixturesDir] - const child = spawnSync(process.execPath, args) - - assert.strictEqual(child.status, 0) - assert.strictEqual(child.signal, null) - assert.match(child.stdout.toString(), /\nok 1 - \S+esm\.test\.mjs\r?\n/) - assert.strictEqual(child.stderr.toString(), '') -}) - -test('should execute the tests when run as node-core-test --test', () => { - const args = [nodeDashDashTestPath, '--test', fixturesDir] - const child = spawnSync(process.execPath, args) - - assert.strictEqual(child.status, 0) - assert.strictEqual(child.signal, null) - assert.match(child.stdout.toString(), /\nok 1 - \S+esm\.test\.mjs\r?\n/) - assert.strictEqual(child.stderr.toString(), '') -}) - -test('should execute the tests when run as node--test', () => { - const args = [nodeDashDashTestPath, fixturesDir] - const child = spawnSync(process.execPath, args) - - assert.strictEqual(child.status, 0) - assert.strictEqual(child.signal, null) - assert.match(child.stdout.toString(), /\nok 1 - \S+esm\.test\.mjs\r?\n/) - assert.strictEqual(child.stderr.toString(), '') -}) - -test('should not execute the tests when run as node-core-test (without the explicit --test flag)', () => { - const args = [binPath, fixturesDir] - const child = spawnSync(process.execPath, args) - - assert.strictEqual(child.status, 1) - assert.strictEqual(child.signal, null) - assert.strictEqual(child.stdout.toString(), '') - assert.match(child.stderr.toString(), /Error: Cannot find module/) -}) - -test('should handle unfinished TLA "entry point"', () => new Promise((resolve, reject) => { - // We need to pass `--test-only` here otherwise the script will implicitly add `--test`. - const cp = spawn(process.execPath, [binPath, join(fixturesDir, 'unfinished-tla.mjs')], { stdio: 'inherit' }) - cp.on('error', reject) - cp.on('exit', (code) => { - assert.strictEqual(code, 13) - resolve() - }) -})) - -test('should handle exitCode changes as node', () => new Promise((resolve, reject) => { - // We need to pass `--test-only` here otherwise the script will implicitly add `--test`. - const cp = spawn(process.execPath, [binPath, join(fixturesDir, 'finished-tla-with-explicit-exitCode-modification.mjs')], { stdio: 'inherit' }) - cp.on('error', reject) - cp.on('exit', (code) => { - assert.strictEqual(code, 13) - resolve() - }) -})) - -test('should handle process.exit calls', () => new Promise((resolve, reject) => { - // We need to pass `--test-only` here otherwise the script will implicitly add `--test`. - const cp = spawn(process.execPath, [binPath, join(fixturesDir, 'finished-tla-with-explicit-process.exit-call.mjs')], { stdio: 'inherit' }) - cp.on('error', reject) - cp.on('exit', (code) => { - assert.strictEqual(code, 0) - resolve() - }) -})) diff --git a/test/parallel.mjs b/test/parallel.mjs deleted file mode 100755 index e0a5050..0000000 --- a/test/parallel.mjs +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env node - -import { once } from 'node:events' -import { spawn } from 'node:child_process' -import fs from 'node:fs/promises' -import process from 'node:process' -import { fileURLToPath } from 'node:url' - -const PARALLEL_DIR = new URL('./parallel/', import.meta.url) -const dir = await fs.opendir(PARALLEL_DIR) - -for await (const { name } of dir) { - if (!name.endsWith('.js') && !name.endsWith('.mjs')) continue - const cp = spawn( - process.execPath, - [fileURLToPath(new URL(name, PARALLEL_DIR))], - { stdio: 'inherit' } - ) - const [code] = await once(cp, 'exit') - if (code) process.exit(code) -} diff --git a/test/parallel/test-runner-aliases.js b/test/parallel/test-runner-aliases.js new file mode 100644 index 0000000..4e45aee --- /dev/null +++ b/test/parallel/test-runner-aliases.js @@ -0,0 +1,8 @@ +'use strict'; +require('../common'); +const { strictEqual } = require('node:assert'); +const test = require('#node:test'); + +strictEqual(test.te3st, test); +strictEqual(test.it, test); +strictEqual(test.describe, test.suite); diff --git a/test/parallel/test-runner-assert.js b/test/parallel/test-runner-assert.js new file mode 100644 index 0000000..6dd39ab --- /dev/null +++ b/test/parallel/test-runner-assert.js @@ -0,0 +1,19 @@ +'use strict'; +require('../common'); +const assert = require('node:assert'); +const test = require('#node:test'); + +const uncopiedKeys = [ + 'AssertionError', + 'CallTracker', + 'strict', +]; +test('only methods from node:assert are on t.assert', (t) => { + const expectedKeys = Object.keys(assert).filter((key) => !uncopiedKeys.includes(key)).sort(); + assert.deepStrictEqual(Object.keys(t.assert).sort(), expectedKeys); +}); + +/* The node:test polyfill does not support this */ +test.skip('t.assert.ok correctly parses the stacktrace', (t) => { + t.assert.throws(() => t.assert.ok(1 === 2), /t\.assert\.ok\(1 === 2\)/); +}); diff --git a/test/parallel/test-runner-cli.js b/test/parallel/test-runner-cli.js index 3487b1e..e78215f 100644 --- a/test/parallel/test-runner-cli.js +++ b/test/parallel/test-runner-cli.js @@ -1,168 +1,391 @@ -// https://github.com/nodejs/node/blob/f8ce9117b19702487eb600493d941f7876e00e01/test/parallel/test-runner-cli.js -'use strict' -require('../common') -const assert = require('assert') -const { spawnSync } = require('child_process') -const { join } = require('path') -const fixtures = require('../common/fixtures') -const testFixtures = fixtures.path('test-runner') - -{ - // File not found. - const args = ['--test', 'a-random-file-that-does-not-exist.js'] - const child = spawnSync(process.execPath, args) - - assert.strictEqual(child.status, 1) - assert.strictEqual(child.signal, null) - assert.strictEqual(child.stdout.toString(), '') - assert.match(child.stderr.toString(), /^Could not find/) -} - -{ - // Default behavior. node_modules is ignored. Files that don't match the - // pattern are ignored except in test/ directories. - const args = ['--test', testFixtures] - const child = spawnSync(process.execPath, args) - - assert.strictEqual(child.status, 1) - assert.strictEqual(child.signal, null) - assert.strictEqual(child.stderr.toString(), '') - const stdout = child.stdout.toString() - assert.match(stdout, /ok 1 - .+index\.test\.js/) - assert.match(stdout, /not ok 2 - .+random\.test\.mjs/) - assert.match(stdout, /not ok 1 - this should fail/) - assert.match(stdout, /ok 3 - .+subdir.+subdir_test\.js/) - assert.match(stdout, /ok 4 - .+random\.cjs/) -} - -{ - // Same but with a prototype mutation in require scripts. - const args = ['--require', join(testFixtures, 'protoMutation.js'), '--test', testFixtures] - const child = spawnSync(process.execPath, args) - - const stdout = child.stdout.toString() - assert.match(stdout, /ok 1 - .+index\.test\.js/) - assert.match(stdout, /not ok 2 - .+random\.test\.mjs/) - assert.match(stdout, /not ok 1 - this should fail/) - assert.match(stdout, /ok 3 - .+subdir.+subdir_test\.js/) - assert.match(stdout, /ok 4 - .+random\.cjs/) - assert.strictEqual(child.status, 1) - assert.strictEqual(child.signal, null) - assert.strictEqual(child.stderr.toString(), '') -} - -{ - // User specified files that don't match the pattern are still run. - const args = ['--test', testFixtures, join(testFixtures, 'index.js')] - const child = spawnSync(process.execPath, args) - - assert.strictEqual(child.status, 1) - assert.strictEqual(child.signal, null) - assert.strictEqual(child.stderr.toString(), '') - const stdout = child.stdout.toString() - assert.match(stdout, /not ok 1 - .+index\.js/) - assert.match(stdout, /ok 2 - .+index\.test\.js/) - assert.match(stdout, /not ok 3 - .+random\.test\.mjs/) - assert.match(stdout, /not ok 1 - this should fail/) - assert.match(stdout, /ok 4 - .+subdir.+subdir_test\.js/) -} - -{ - // Searches node_modules if specified. - const args = ['--test', join(testFixtures, 'node_modules')] - const child = spawnSync(process.execPath, args) - - assert.strictEqual(child.status, 1) - assert.strictEqual(child.signal, null) - assert.strictEqual(child.stderr.toString(), '') - const stdout = child.stdout.toString() - assert.match(stdout, /not ok 1 - .+test-nm\.js/) -} - -{ - // The current directory is used by default. - const args = ['--test'] - const options = { cwd: testFixtures } - const child = spawnSync(process.execPath, args, options) - - assert.strictEqual(child.status, 1) - assert.strictEqual(child.signal, null) - assert.strictEqual(child.stderr.toString(), '') - const stdout = child.stdout.toString() - assert.match(stdout, /ok 1 - .+index\.test\.js/) - assert.match(stdout, /not ok 2 - .+random\.test\.mjs/) - assert.match(stdout, /not ok 1 - this should fail/) - assert.match(stdout, /ok 3 - .+subdir.+subdir_test\.js/) - assert.match(stdout, /ok 4 - .+random\.cjs/) -} - -// Our lib does not support Node.js specific flags, skipping. -// { -// // Flags that cannot be combined with --test. -// const flags = [ -// ['--check', '--test'], -// ['--interactive', '--test'], -// ['--eval', 'console.log("should not print")', '--test'], -// ['--print', 'console.log("should not print")', '--test'] -// ] - -// flags.forEach((args) => { -// const child = spawnSync(process.execPath, args) - -// assert.notStrictEqual(child.status, 0) -// assert.strictEqual(child.signal, null) -// assert.strictEqual(child.stdout.toString(), '') -// const stderr = child.stderr.toString() -// assert.match(stderr, /--test/) -// }) -// } - -{ - // Test combined stream outputs - const args = [ - '--test', - 'test/fixtures/test-runner/index.test.js', - 'test/fixtures/test-runner/nested.js', - 'test/fixtures/test-runner/invalid-tap.js' - ] - const child = spawnSync(process.execPath, args) - - assert.strictEqual(child.status, 1) - assert.strictEqual(child.signal, null) - assert.strictEqual(child.stderr.toString(), '') - const stdout = child.stdout.toString() - assert.match(stdout, /# Subtest: .+index\.test\.js/) - assert.match(stdout, / {4}# Subtest: this should pass/) - assert.match(stdout, / {4}ok 1 - this should pass/) - assert.match(stdout, / {6}---/) - assert.match(stdout, / {6}duration_ms: .*/) - assert.match(stdout, / {6}\.\.\./) - assert.match(stdout, / {4}1\.\.1/) - - assert.match(stdout, /ok 1 - .+index\.test\.js/) - - assert.match(stdout, /# Subtest: .+invalid-tap\.js/) - assert.match(stdout, / {4}# invalid tap output/) - assert.match(stdout, /ok 2 - .+invalid-tap\.js/) - - assert.match(stdout, /# Subtest: .+nested\.js/) - assert.match(stdout, / {4}# Subtest: level 0a/) - assert.match(stdout, / {8}# Subtest: level 1a/) - assert.match(stdout, / {8}ok 1 - level 1a/) - assert.match(stdout, / {8}# Subtest: level 1b/) - assert.match(stdout, / {8}not ok 2 - level 1b/) - assert.match(stdout, / {10}code: 'ERR_TEST_FAILURE'/) - assert.match(stdout, / {10}stack: |-'/) - assert.match(stdout, / {12}TestContext\. .*/) - assert.match(stdout, / {8}# Subtest: level 1c/) - assert.match(stdout, / {8}ok 3 - level 1c # SKIP aaa/) - assert.match(stdout, / {8}# Subtest: level 1d/) - assert.match(stdout, / {8}ok 4 - level 1d/) - assert.match(stdout, / {4}not ok 1 - level 0a/) - assert.match(stdout, / {6}error: '1 subtest failed'/) - assert.match(stdout, / {4}# Subtest: level 0b/) - assert.match(stdout, / {4}not ok 2 - level 0b/) - assert.match(stdout, / {6}error: 'level 0b error'/) - assert.match(stdout, /not ok 3 - .+nested\.js/) - assert.match(stdout, /# tests 3/) -} +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const { join } = require('path'); +const fixtures = require('../common/fixtures'); +const testFixtures = fixtures.path('test-runner'); + +for (const isolation of ['none', 'process']) { + { + // File not found. + const args = [ + common.testRunnerPath, + '--test', + `--experimental-test-isolation=${isolation}`, + 'a-random-file-that-does-not-exist.js', + ]; + const child = spawnSync(process.execPath, args); + + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + assert.strictEqual(child.stdout.toString(), ''); + assert.match(child.stderr.toString(), /^Could not find/); + } + + { + // Default behavior. node_modules is ignored. Files that don't match the + // pattern are ignored except in test/ directories. + const args = [common.testRunnerPath, '--test', '--test-reporter=tap', + `--experimental-test-isolation=${isolation}`]; + const child = spawnSync(process.execPath, args, { cwd: join(testFixtures, 'default-behavior') }); + + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + + assert.match(stdout, /ok 1 - this should pass/); + assert.match(stdout, /not ok 2 - this should fail/); + assert.match(stdout, /ok 3 - subdir.+subdir_test\.js/); + assert.match(stdout, /ok 4 - this should pass/); + assert.match(stdout, /ok 5 - this should be skipped/); + assert.match(stdout, /ok 6 - this should be executed/); + } + + { + // Should match files with "-test.(c|m)js" suffix. + const args = [common.testRunnerPath, '--test', '--test-reporter=tap', + `--experimental-test-isolation=${isolation}`]; + const child = spawnSync(process.execPath, args, { cwd: join(testFixtures, 'matching-patterns') }); + + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + + assert.match(stdout, /ok 1 - this should pass/); + assert.match(stdout, /ok 2 - this should pass/); + assert.match(stdout, /ok 3 - this should pass/); + // Doesn't match the TypeScript files + assert.doesNotMatch(stdout, /ok 4 - this should pass/); + } + + { + // User specified files that don't match the pattern are still run. + const args = [ + common.testRunnerPath, + '--test', + '--test-reporter=tap', + `--experimental-test-isolation=${isolation}`, + join(testFixtures, 'index.js'), + ]; + const child = spawnSync(process.execPath, args, { cwd: testFixtures }); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + assert.match(stdout, /not ok 1 - .+index\.js/); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + } + + { + // Searches node_modules if specified. + const args = [ + common.testRunnerPath, + '--test', + '--test-reporter=tap', + `--experimental-test-isolation=${isolation}`, + join(testFixtures, 'default-behavior/node_modules/*.js'), + ]; + const child = spawnSync(process.execPath, args); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + assert.match(stdout, /not ok 1 - .+test-nm\.js/); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + } + + { + // The current directory is used by default. + const args = [common.testRunnerPath, '--test', `--experimental-test-isolation=${isolation}`]; + const options = { cwd: join(testFixtures, 'default-behavior') }; + const child = spawnSync(process.execPath, args, options); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + assert.match(stdout, /this should pass/); + assert.match(stdout, /this should fail/); + assert.match(stdout, /subdir.+subdir_test\.js/); + assert.match(stdout, /this should pass/); + assert.match(stdout, /this should be skipped/); + assert.match(stdout, /this should be executed/); + + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + } + + { + // Test combined stream outputs + const args = [ + common.testRunnerPath, + '--test', + '--test-reporter=tap', + `--experimental-test-isolation=${isolation}`, + 'test/fixtures/test-runner/default-behavior/index.test.js', + 'test/fixtures/test-runner/nested.js', + 'test/fixtures/test-runner/invalid-tap.js', + ]; + const child = spawnSync(process.execPath, args); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + assert.match(stdout, /# Subtest: this should pass/); + assert.match(stdout, /ok 1 - this should pass/); + assert.match(stdout, / {2}---/); + assert.match(stdout, / {2}duration_ms: .*/); + assert.match(stdout, / {2}\.\.\./); + + assert.match(stdout, /# Subtest: .+invalid-tap\.js/); + assert.match(stdout, /invalid tap output/); + assert.match(stdout, /ok 2 - .+invalid-tap\.js/); + + assert.match(stdout, /# Subtest: level 0a/); + assert.match(stdout, / {4}# Subtest: level 1a/); + assert.match(stdout, / {4}ok 1 - level 1a/); + assert.match(stdout, / {4}# Subtest: level 1b/); + assert.match(stdout, / {4}not ok 2 - level 1b/); + assert.match(stdout, / {6}code: 'ERR_TEST_FAILURE'/); + assert.match(stdout, / {6}stack: |-'/); + assert.match(stdout, / {8}TestContext\. .*/); + assert.match(stdout, / {4}# Subtest: level 1c/); + assert.match(stdout, / {4}ok 3 - level 1c # SKIP aaa/); + assert.match(stdout, / {4}# Subtest: level 1d/); + assert.match(stdout, / {4}ok 4 - level 1d/); + assert.match(stdout, /not ok 3 - level 0a/); + assert.match(stdout, / {2}error: '1 subtest failed'/); + assert.match(stdout, /# Subtest: level 0b/); + assert.match(stdout, /not ok 4 - level 0b/); + assert.match(stdout, / {2}error: 'level 0b error'/); + assert.match(stdout, /# tests 8/); + assert.match(stdout, /# suites 0/); + assert.match(stdout, /# pass 4/); + assert.match(stdout, /# fail 3/); + assert.match(stdout, /# cancelled 0/); + assert.match(stdout, /# skipped 1/); + assert.match(stdout, /# todo 0/); + + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + } + + { + // Flags that cannot be combined with --test. + const flags = [ + ['--check', '--test'], + ['--interactive', '--test'], + ['--eval', 'console.log("should not print")', '--test'], + ['--print', 'console.log("should not print")', '--test'], + ]; + + for (const args of flags) { + const child = spawnSync(process.execPath, args); + + assert.strictEqual(child.stdout.toString(), ''); + const stderr = child.stderr.toString(); + assert.match(stderr, /--test/); + assert.notStrictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + } + } + + { + // Test user logging in tests. + const args = [ + common.testRunnerPath, + '--test', + '--test-reporter=tap', + 'test/fixtures/test-runner/user-logs.js', + ]; + const child = spawnSync(process.execPath, args); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + assert.match(stdout, /# stderr 1/); + assert.match(stdout, /# stderr 2/); + assert.match(stdout, /# stdout 3/); + assert.match(stdout, /# stderr 6/); + assert.match(stdout, /# not ok 1 - fake test/); + assert.match(stdout, /# stderr 5/); + assert.match(stdout, /# stdout 4/); + assert.match(stdout, /# Subtest: a test/); + assert.match(stdout, /ok 1 - a test/); + assert.match(stdout, /# tests 1/); + assert.match(stdout, /# pass 1/); + + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + } + + { + // Use test with --loader and --require. + // This case is common since vscode uses --require to load the debugger. + const args = [common.testRunnerPath, '--no-warnings', + '--experimental-loader', 'data:text/javascript,', + '--require', fixtures.path('empty.js'), + '--test', join(testFixtures, 'default-behavior', 'index.test.js')]; + const child = spawnSync(process.execPath, args); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + assert.match(stdout, /this should pass/); + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + } + + { + // --test-shard option validation + const args = [common.testRunnerPath, '--test', '--test-shard=1', join(testFixtures, 'index.js')]; + const child = spawnSync(process.execPath, args, { cwd: testFixtures }); + + assert.match(child.stderr.toString(), /The argument '--test-shard' must be in the form of \/\. Received '1'/); + const stdout = child.stdout.toString(); + assert.strictEqual(stdout, ''); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + } + + { + // --test-shard option validation + const args = [common.testRunnerPath, '--test', '--test-shard=1/2/3', join(testFixtures, 'index.js')]; + const child = spawnSync(process.execPath, args, { cwd: testFixtures }); + + assert.match(child.stderr.toString(), /The argument '--test-shard' must be in the form of \/\. Received '1\/2\/3'/); + const stdout = child.stdout.toString(); + assert.strictEqual(stdout, ''); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + } + + { + // --test-shard option validation + const args = [common.testRunnerPath, '--test', '--test-shard=0/3', join(testFixtures, 'index.js')]; + const child = spawnSync(process.execPath, args, { cwd: testFixtures }); + + assert.match(child.stderr.toString(), /The value of "options\.shard\.index" is out of range\. It must be >= 1 && <= 3\. Received 0/); + const stdout = child.stdout.toString(); + assert.strictEqual(stdout, ''); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + } + + { + // --test-shard option validation + const args = ['--test', '--test-shard=0xf/20abcd', join(testFixtures, 'index.js')]; + const child = spawnSync(process.execPath, args, { cwd: testFixtures }); + + assert.match(child.stderr.toString(), /The argument '--test-shard' must be in the form of \/\. Received '0xf\/20abcd'/); + const stdout = child.stdout.toString(); + assert.strictEqual(stdout, ''); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + } + + { + // --test-shard option validation + const args = ['--test', '--test-shard=hello', join(testFixtures, 'index.js')]; + const child = spawnSync(process.execPath, args, { cwd: testFixtures }); + + assert.match(child.stderr.toString(), /The argument '--test-shard' must be in the form of \/\. Received 'hello'/); + const stdout = child.stdout.toString(); + assert.strictEqual(stdout, ''); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + } + + { + // --test-shard option, first shard + const args = [ + common.testRunnerPath, + '--test', + '--test-reporter=tap', + '--test-shard=1/2', + join(testFixtures, 'shards/*.cjs'), + ]; + const child = spawnSync(process.execPath, args); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + assert.match(stdout, /# Subtest: a\.cjs this should pass/); + assert.match(stdout, /ok 1 - a\.cjs this should pass/); + + assert.match(stdout, /# Subtest: c\.cjs this should pass/); + assert.match(stdout, /ok 2 - c\.cjs this should pass/); + + assert.match(stdout, /# Subtest: e\.cjs this should pass/); + assert.match(stdout, /ok 3 - e\.cjs this should pass/); + + assert.match(stdout, /# Subtest: g\.cjs this should pass/); + assert.match(stdout, /ok 4 - g\.cjs this should pass/); + + assert.match(stdout, /# Subtest: i\.cjs this should pass/); + assert.match(stdout, /ok 5 - i\.cjs this should pass/); + + assert.match(stdout, /# tests 5/); + assert.match(stdout, /# pass 5/); + assert.match(stdout, /# fail 0/); + assert.match(stdout, /# skipped 0/); + + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + } + + { + // --test-shard option, last shard + const args = [ + common.testRunnerPath, + '--test', + '--test-reporter=tap', + '--test-shard=2/2', + join(testFixtures, 'shards/*.cjs'), + ]; + const child = spawnSync(process.execPath, args); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + assert.match(stdout, /# Subtest: b\.cjs this should pass/); + assert.match(stdout, /ok 1 - b\.cjs this should pass/); + + assert.match(stdout, /# Subtest: d\.cjs this should pass/); + assert.match(stdout, /ok 2 - d\.cjs this should pass/); + + assert.match(stdout, /# Subtest: f\.cjs this should pass/); + assert.match(stdout, /ok 3 - f\.cjs this should pass/); + + assert.match(stdout, /# Subtest: h\.cjs this should pass/); + assert.match(stdout, /ok 4 - h\.cjs this should pass/); + + assert.match(stdout, /# Subtest: j\.cjs this should pass/); + assert.match(stdout, /ok 5 - j\.cjs this should pass/); + + assert.match(stdout, /# tests 5/); + assert.match(stdout, /# pass 5/); + assert.match(stdout, /# fail 0/); + assert.match(stdout, /# skipped 0/); + + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + } + + { + // Should not match files like latest.js + const args = ['--test', '--test-reporter=tap']; + const child = spawnSync(process.execPath, args, { cwd: join(testFixtures, 'issue-54726') }); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + + assert.match(stdout, /tests 0/); + assert.match(stdout, /suites 0/); + assert.match(stdout, /pass 0/); + assert.match(stdout, /fail 0/); + assert.match(stdout, /cancelled 0/); + assert.match(stdout, /skipped 0/); + assert.match(stdout, /todo 0/); + + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + } +} \ No newline at end of file diff --git a/test/parallel/test-runner-concurrency.js b/test/parallel/test-runner-concurrency.js index 763be40..aaba2b4 100644 --- a/test/parallel/test-runner-concurrency.js +++ b/test/parallel/test-runner-concurrency.js @@ -1,66 +1,98 @@ -// https://github.com/nodejs/node/blob/a3e110820ff98702e1761831e7beaf0f5f1f75e7/test/parallel/test-runner-concurrency.js +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const { describe, it, test } = require('#node:test'); +const assert = require('node:assert'); +const fs = require('node:fs/promises'); +const os = require('node:os'); +const timers = require('node:timers/promises'); -'use strict' -const common = require('../common') -const { describe, it, test } = require('#node:test') -const assert = require('assert') +tmpdir.refresh(); -describe('Concurrency option (boolean) = true ', { concurrency: true }, () => { - let isFirstTestOver = false +describe('Concurrency option (boolean) = true', { concurrency: true }, () => { + let isFirstTestOver = false; it('should start the first test', () => new Promise((resolve) => { - setImmediate(() => { isFirstTestOver = true; resolve() }) - })) + setImmediate(() => { isFirstTestOver = true; resolve(); }); + })); it('should start before the previous test ends', () => { // Should work even on single core CPUs - assert.strictEqual(isFirstTestOver, false) - }) -}) + assert.strictEqual(isFirstTestOver, false); + }); +}); describe( - 'Concurrency option (boolean) = false ', + 'Concurrency option (boolean) = false', { concurrency: false }, () => { - let isFirstTestOver = false + let isFirstTestOver = false; it('should start the first test', () => new Promise((resolve) => { - setImmediate(() => { isFirstTestOver = true; resolve() }) - })) + setImmediate(() => { isFirstTestOver = true; resolve(); }); + })); it('should start after the previous test ends', () => { - assert.strictEqual(isFirstTestOver, true) - }) + assert.strictEqual(isFirstTestOver, true); + }); } -) +); + +// Despite the docs saying so at some point, setting concurrency to true should +// not limit concurrency to the number of available CPU cores. +describe('concurrency: true implies Infinity', { concurrency: true }, () => { + // The factor 5 is intentionally chosen to be higher than the default libuv + // thread pool size. + const nTests = 5 * os.availableParallelism(); + let nStarted = 0; + for (let i = 0; i < nTests; i++) { + it(`should run test ${i} concurrently`, async () => { + assert.strictEqual(nStarted++, i); + await timers.setImmediate(); + assert.strictEqual(nStarted, nTests); + }); + } +}); { // Make sure tests run in order when root concurrency is 1 (default) - const tree = [] + const tree = []; const expectedTestTree = common.mustCall(() => { assert.deepStrictEqual(tree, [ 'suite 1', 'nested', 'suite 2', '1', '2', 'nested 1', 'nested 2', - 'test', 'test 1', 'test 2' - ]) - }) + 'test', 'test 1', 'test 2', + ]); + }); describe('suite 1', () => { - tree.push('suite 1') - it('1', () => tree.push('1')) - it('2', () => tree.push('2')) + tree.push('suite 1'); + it('1', () => tree.push('1')); + it('2', () => tree.push('2')); describe('nested', () => { - tree.push('nested') - it('nested 1', () => tree.push('nested 1')) - it('nested 2', () => tree.push('nested 2')) - }) - }) + tree.push('nested'); + it('nested 1', () => tree.push('nested 1')); + it('nested 2', () => tree.push('nested 2')); + }); + }); test('test', async (t) => { - tree.push('test') - await t.test('test1', () => tree.push('test 1')) - await t.test('test 2', () => tree.push('test 2')) - }) + tree.push('test'); + await t.test('test1', () => tree.push('test 1')); + await t.test('test 2', () => tree.push('test 2')); + }); describe('suite 2', () => { - tree.push('suite 2') - it('should run after other suites', expectedTestTree) - }) + tree.push('suite 2'); + it('should run after other suites', expectedTestTree); + }); } + +test('--test multiple files', { skip: os.availableParallelism() < 3 }, async () => { + await fs.writeFile(tmpdir.resolve('test-runner-concurrency'), ''); + const { code, stderr } = await common.spawnPromisified(process.execPath, [ + '--test', + fixtures.path('test-runner', 'concurrency', 'a.mjs'), + fixtures.path('test-runner', 'concurrency', 'b.mjs'), + ]); + assert.strictEqual(stderr, ''); + assert.strictEqual(code, 0); +}); diff --git a/test/parallel/test-runner-enable-source-maps-issue.js b/test/parallel/test-runner-enable-source-maps-issue.js new file mode 100644 index 0000000..d68bb7c --- /dev/null +++ b/test/parallel/test-runner-enable-source-maps-issue.js @@ -0,0 +1,16 @@ +'use strict'; +require('../common'); +const assert = require('node:assert'); +const { spawnSync } = require('node:child_process'); +const { test } = require('#node:test'); +const fixtures = require('../common/fixtures'); + +test('ensures --enable-source-maps does not throw an error', () => { + const fixture = fixtures.path('test-runner', 'coverage', 'stdin.test.js'); + const args = ['--enable-source-maps', fixture]; + + const result = spawnSync(process.execPath, args); + + assert.strictEqual(result.stderr.toString(), ''); + assert.strictEqual(result.status, 0); +}); diff --git a/test/parallel/test-runner-exit-code.js b/test/parallel/test-runner-exit-code.js index 8ac1a17..7c7aa3d 100644 --- a/test/parallel/test-runner-exit-code.js +++ b/test/parallel/test-runner-exit-code.js @@ -1,57 +1,72 @@ -// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/test/parallel/test-runner-exit-code.js -'use strict' -const common = require('../common') -const fixtures = require('../common/fixtures') -const assert = require('assert') -const { spawnSync, spawn } = require('child_process') -const { once } = require('events') -const finished = require('util').promisify(require('stream').finished) - -async function runAndKill (file) { +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { spawnSync, spawn } = require('child_process'); +const { once } = require('events'); +const { finished } = require('stream/promises'); + +async function runAndKill(file) { if (common.isWindows) { - common.printSkipMessage(`signals are not supported in windows, skipping ${file}`) - return + common.printSkipMessage(`signals are not supported in windows, skipping ${file}`); + return; } - let stdout = '' - const child = spawn(process.execPath, ['--test', file]) - child.stdout.setEncoding('utf8') + let stdout = ''; + const child = spawn(process.execPath, ['--test', '--test-reporter=tap', file]); + child.stdout.setEncoding('utf8'); child.stdout.on('data', (chunk) => { - if (!stdout.length) child.kill('SIGINT') - stdout += chunk - }) - const [code, signal] = await once(child, 'exit') - await finished(child.stdout) - assert.strictEqual(stdout, 'TAP version 13\n') - assert.strictEqual(signal, null) - assert.strictEqual(code, 1) + if (!stdout.length) child.kill('SIGINT'); + stdout += chunk; + }); + const [code, signal] = await once(child, 'exit'); + await finished(child.stdout); + assert(stdout.startsWith('TAP version 13\n')); + assert.strictEqual(signal, null); + assert.strictEqual(code, 1); } if (process.argv[2] === 'child') { - const test = require('#node:test') + const test = require('#node:test'); if (process.argv[3] === 'pass') { test('passing test', () => { - assert.strictEqual(true, true) - }) + assert.strictEqual(true, true); + }); } else if (process.argv[3] === 'fail') { - assert.strictEqual(process.argv[3], 'fail') + assert.strictEqual(process.argv[3], 'fail'); test('failing test', () => { - assert.strictEqual(true, false) - }) - } else assert.fail('unreachable') + assert.strictEqual(true, false); + }); + } else assert.fail('unreachable'); } else { - let child = spawnSync(process.execPath, [__filename, 'child', 'pass']) - assert.strictEqual(child.status, 0) - assert.strictEqual(child.signal, null) + let child = spawnSync(process.execPath, [__filename, 'child', 'pass']); + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + + child = spawnSync(process.execPath, [ + '--test', + fixtures.path('test-runner', 'default-behavior', 'subdir', 'subdir_test.js'), + ]); + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + - child = spawnSync(process.execPath, ['--test', fixtures.path('test-runner', 'subdir', 'subdir_test.js')]) - assert.strictEqual(child.status, 0) - assert.strictEqual(child.signal, null) + child = spawnSync(process.execPath, [ + '--test', + fixtures.path('test-runner', 'todo_exit_code.js'), + ]); + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + const stdout = child.stdout.toString(); + assert.match(stdout, /tests 3/); + assert.match(stdout, /pass 0/); + assert.match(stdout, /fail 0/); + assert.match(stdout, /todo 3/); - child = spawnSync(process.execPath, [__filename, 'child', 'fail']) - assert.strictEqual(child.status, 1) - assert.strictEqual(child.signal, null) + child = spawnSync(process.execPath, [__filename, 'child', 'fail']); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); - runAndKill(fixtures.path('test-runner', 'never_ending_sync.js')).then(common.mustCall()) - runAndKill(fixtures.path('test-runner', 'never_ending_async.js')).then(common.mustCall()) + runAndKill(fixtures.path('test-runner', 'never_ending_sync.js')).then(common.mustCall()); + runAndKill(fixtures.path('test-runner', 'never_ending_async.js')).then(common.mustCall()); } diff --git a/test/parallel/test-runner-extraneous-async-activity.js b/test/parallel/test-runner-extraneous-async-activity.js index da826c4..0a131b8 100644 --- a/test/parallel/test-runner-extraneous-async-activity.js +++ b/test/parallel/test-runner-extraneous-async-activity.js @@ -1,32 +1,73 @@ -// https://github.com/nodejs/node/blob/06603c44a5b0e92b1a3591ace467ce9770bf9658/test/parallel/test-runner-extraneous-async-activity.js -'use strict' -require('../common') -const fixtures = require('../common/fixtures') -const assert = require('assert') -const { spawnSync } = require('child_process') +'use strict'; +require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const common = require('../common'); { const child = spawnSync(process.execPath, [ + common.testRunnerPath, '--test', - fixtures.path('test-runner', 'extraneous_set_immediate_async.mjs') - ]) - const stdout = child.stdout.toString() - assert.match(stdout, /^# pass 0$/m) - assert.match(stdout, /^# fail 1$/m) - assert.match(stdout, /^# cancelled 0$/m) - assert.strictEqual(child.status, 1) - assert.strictEqual(child.signal, null) + fixtures.path('test-runner', 'extraneous_set_immediate_async.mjs'), + ]); + const stdout = child.stdout.toString(); + assert.match(stdout, /Error: Test "extraneous async activity test" at .+extraneous_set_immediate_async\.mjs:\d+:1 generated asynchronous activity after the test ended/m); + assert.match(stdout, /pass 1/m); + assert.match(stdout, /fail 1$/m); + assert.match(stdout, /cancelled 0$/m); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); } { const child = spawnSync(process.execPath, [ + common.testRunnerPath, '--test', - fixtures.path('test-runner', 'extraneous_set_timeout_async.mjs') - ]) - const stdout = child.stdout.toString() - assert.match(stdout, /^# pass 0$/m) - assert.match(stdout, /^# fail 1$/m) - assert.match(stdout, /^# cancelled 0$/m) - assert.strictEqual(child.status, 1) - assert.strictEqual(child.signal, null) + fixtures.path('test-runner', 'extraneous_set_timeout_async.mjs'), + ]); + const stdout = child.stdout.toString(); + assert.match(stdout, /Error: Test "extraneous async activity test" at .+extraneous_set_timeout_async\.mjs:\d+:1 generated asynchronous activity after the test ended/m); + assert.match(stdout, /pass 1$/m); + assert.match(stdout, /fail 1$/m); + assert.match(stdout, /cancelled 0$/m); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); +} + +{ + const child = spawnSync(process.execPath, [ + common.testRunnerPath, + '--test', + fixtures.path('test-runner', 'async-error-in-test-hook.mjs'), + ]); + const stdout = child.stdout.toString(); + assert.match(stdout, /Error: Test hook "before" at .+async-error-in-test-hook\.mjs:\d+:1 generated asynchronous activity after the test ended/m); + assert.match(stdout, /Error: Test hook "beforeEach" at .+async-error-in-test-hook\.mjs:\d+:1 generated asynchronous activity after the test ended/m); + assert.match(stdout, /Error: Test hook "after" at .+async-error-in-test-hook\.mjs:\d+:1 generated asynchronous activity after the test ended/m); + assert.match(stdout, /Error: Test hook "afterEach" at .+async-error-in-test-hook\.mjs:\d+:1 generated asynchronous activity after the test ended/m); + assert.match(stdout, /pass 1$/m); + assert.match(stdout, /fail 1$/m); + assert.match(stdout, /cancelled 0$/m); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); +} + +{ + const child = spawnSync(process.execPath, [ + common.testRunnerPath, + '--test', + '--experimental-test-isolation=none', + fixtures.path('test-runner', 'async-error-in-test-hook.mjs'), + ]); + const stdout = child.stdout.toString(); + assert.match(stdout, /Error: Test hook "before" at .+async-error-in-test-hook\.mjs:\d+:1 generated asynchronous activity after the test ended/m); + assert.match(stdout, /Error: Test hook "beforeEach" at .+async-error-in-test-hook\.mjs:\d+:1 generated asynchronous activity after the test ended/m); + assert.match(stdout, /Error: Test hook "after" at .+async-error-in-test-hook\.mjs:\d+:1 generated asynchronous activity after the test ended/m); + assert.match(stdout, /Error: Test hook "afterEach" at .+async-error-in-test-hook\.mjs:\d+:1 generated asynchronous activity after the test ended/m); + assert.match(stdout, /pass 1$/m); + assert.match(stdout, /fail 0$/m); + assert.match(stdout, /cancelled 0$/m); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); } diff --git a/test/parallel/test-runner-filetest-location.js b/test/parallel/test-runner-filetest-location.js new file mode 100644 index 0000000..e30a2c6 --- /dev/null +++ b/test/parallel/test-runner-filetest-location.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const { strictEqual } = require('node:assert'); +const { relative } = require('node:path'); +const { run } = require('#node:test'); +const fixture = fixtures.path('test-runner', 'never_ending_sync.js'); +const relativePath = relative(process.cwd(), fixture); +const stream = run({ + files: [relativePath], + timeout: common.platformTimeout(100), +}); + +stream.on('test:fail', common.mustCall((result) => { + strictEqual(result.name, relativePath); + strictEqual(result.details.error.failureType, 'testTimeoutFailure'); + strictEqual(result.line, 1); + strictEqual(result.column, 1); + strictEqual(result.file, fixture); +})); diff --git a/test/parallel/test-runner-filter-warning.js b/test/parallel/test-runner-filter-warning.js new file mode 100644 index 0000000..d07a0ec --- /dev/null +++ b/test/parallel/test-runner-filter-warning.js @@ -0,0 +1,11 @@ +// Flags: --test-only +'use strict'; +const common = require('../common'); +const { test } = require('#node:test'); +const { defaultMaxListeners } = require('node:events'); + +process.on('warning', common.mustNotCall()); + +for (let i = 0; i < defaultMaxListeners + 1; ++i) { + test(`test ${i + 1}`); +} diff --git a/test/parallel/test-runner-force-exit-failure.js b/test/parallel/test-runner-force-exit-failure.js new file mode 100644 index 0000000..03c2875 --- /dev/null +++ b/test/parallel/test-runner-force-exit-failure.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); +const { match, doesNotMatch, strictEqual } = require('node:assert'); +const { spawnSync } = require('node:child_process'); +const fixtures = require('../common/fixtures'); +const fixture = fixtures.path('test-runner/throws_sync_and_async.js'); + +for (const isolation of ['none', 'process']) { + const args = [ + fixture, + '--test', + '--test-reporter=spec', + '--test-force-exit', + `--experimental-test-isolation=${isolation}`, + ]; + const r = spawnSync(process.execPath, args); + + strictEqual(r.status, 1); + strictEqual(r.signal, null); + strictEqual(r.stderr.toString(), ''); + + const stdout = r.stdout.toString(); + match(stdout, /Error: fails/); + doesNotMatch(stdout, /this should not have a chance to be thrown/); +} diff --git a/test/parallel/test-runner-force-exit-flush.js b/test/parallel/test-runner-force-exit-flush.js new file mode 100644 index 0000000..5f50296 --- /dev/null +++ b/test/parallel/test-runner-force-exit-flush.js @@ -0,0 +1,49 @@ +'use strict'; +require('../common'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const { match, strictEqual } = require('node:assert'); +const { spawnSync } = require('node:child_process'); +const { readFileSync } = require('node:fs'); +const { test } = require('#node:test'); + +function runWithReporter(reporter) { + const destination = tmpdir.resolve(`${reporter}.out`); + const args = [ + fixtures.path('test-runner', 'reporters.js'), + '--test-force-exit', + `--test-reporter=${reporter}`, + `--test-reporter-destination=${destination}`, + ]; + const child = spawnSync(process.execPath, args); + strictEqual(child.stdout.toString(), ''); + strictEqual(child.stderr.toString(), ''); + strictEqual(child.status, 1); + return destination; +} + +tmpdir.refresh(); + +test('junit reporter', () => { + const output = readFileSync(runWithReporter('junit'), 'utf8'); + match(output, //); + match(output, //); + match(output, //); + match(output, /