velocious 1.0.643 → 1.0.645
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -10
- package/build/authorization/ability.js +36 -13
- package/build/background-jobs/adapter.js +9 -0
- package/build/background-jobs/main.js +35 -10
- package/build/background-jobs/store.js +217 -38
- package/build/background-jobs/types.js +15 -0
- package/build/configuration-types.js +1 -1
- package/build/configuration.js +45 -9
- package/build/database/drivers/base-column.js +8 -0
- package/build/database/drivers/mssql/sql/update.js +1 -3
- package/build/database/drivers/mysql/sql/update.js +1 -3
- package/build/database/drivers/pgsql/column.js +22 -0
- package/build/database/drivers/pgsql/sql/update.js +1 -3
- package/build/database/drivers/sqlite/sql/alter-table.js +11 -1
- package/build/database/drivers/sqlite/sql/update.js +1 -3
- package/build/database/query/index.js +2 -2
- package/build/database/query/model-class-query.js +26 -10
- package/build/database/query/preloader/has-many.js +4 -1
- package/build/database/query/query-data.js +2 -1
- package/build/database/query/update-base.js +14 -0
- package/build/database/query/with-count.js +3 -1
- package/build/database/record/acts-as-list.js +37 -13
- package/build/database/record/attachments/store.js +336 -26
- package/build/database/record/auditing.js +6 -3
- package/build/database/record/index.js +197 -42
- package/build/database/record/instance-relationships/belongs-to.js +2 -1
- package/build/database/record/validators/uniqueness.js +8 -5
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
- package/build/frontend-model-controller.js +136 -53
- package/build/frontend-model-resource/base-resource.js +87 -37
- package/build/frontend-model-resource/velocious-attachment-resource.js +98 -31
- package/build/frontend-models/base.js +307 -59
- package/build/frontend-models/preloader.js +6 -3
- package/build/frontend-models/query.js +15 -6
- package/build/frontend-models/resource-definition.js +21 -2
- package/build/frontend-models/use-destroyed-event.js +1 -1
- package/build/frontend-models/use-model-class-event.js +2 -2
- package/build/frontend-models/use-updated-event.js +1 -1
- package/build/frontend-models/websocket-channel.js +256 -93
- package/build/frontend-models/websocket-publishers.js +218 -18
- package/build/http-server/client/websocket-session.js +13 -1
- package/build/http-server/websocket-channel.js +15 -2
- package/build/src/authorization/ability.d.ts +9 -3
- package/build/src/authorization/ability.d.ts.map +1 -1
- package/build/src/authorization/ability.js +33 -14
- package/build/src/background-jobs/adapter.d.ts +6 -0
- package/build/src/background-jobs/adapter.d.ts.map +1 -1
- package/build/src/background-jobs/adapter.js +9 -1
- package/build/src/background-jobs/main.d.ts +6 -0
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +34 -11
- package/build/src/background-jobs/store.d.ts +66 -12
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +191 -35
- package/build/src/background-jobs/types.d.ts +55 -0
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +16 -1
- package/build/src/configuration-types.d.ts +3 -3
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +2 -2
- package/build/src/configuration.d.ts +18 -8
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +38 -7
- package/build/src/database/drivers/base-column.d.ts +5 -0
- package/build/src/database/drivers/base-column.d.ts.map +1 -1
- package/build/src/database/drivers/base-column.js +8 -1
- package/build/src/database/drivers/mssql/sql/update.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/sql/update.js +2 -4
- package/build/src/database/drivers/mysql/sql/update.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/sql/update.js +2 -4
- package/build/src/database/drivers/pgsql/column.d.ts +5 -0
- package/build/src/database/drivers/pgsql/column.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/column.js +19 -1
- package/build/src/database/drivers/pgsql/sql/update.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/sql/update.js +2 -4
- package/build/src/database/drivers/sqlite/sql/alter-table.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/sql/alter-table.js +6 -2
- package/build/src/database/drivers/sqlite/sql/update.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/sql/update.js +2 -4
- package/build/src/database/query/index.d.ts +4 -4
- package/build/src/database/query/index.d.ts.map +1 -1
- package/build/src/database/query/index.js +3 -3
- package/build/src/database/query/model-class-query.d.ts +8 -2
- package/build/src/database/query/model-class-query.d.ts.map +1 -1
- package/build/src/database/query/model-class-query.js +25 -11
- package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-many.js +4 -2
- package/build/src/database/query/query-data.d.ts.map +1 -1
- package/build/src/database/query/query-data.js +3 -2
- package/build/src/database/query/update-base.d.ts +7 -0
- package/build/src/database/query/update-base.d.ts.map +1 -1
- package/build/src/database/query/update-base.js +13 -1
- package/build/src/database/query/with-count.d.ts.map +1 -1
- package/build/src/database/query/with-count.js +3 -2
- package/build/src/database/record/acts-as-list.d.ts.map +1 -1
- package/build/src/database/record/acts-as-list.js +35 -14
- package/build/src/database/record/attachments/store.d.ts +67 -0
- package/build/src/database/record/attachments/store.d.ts.map +1 -1
- package/build/src/database/record/attachments/store.js +301 -25
- package/build/src/database/record/auditing.d.ts.map +1 -1
- package/build/src/database/record/auditing.js +7 -4
- package/build/src/database/record/index.d.ts +78 -26
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +171 -42
- package/build/src/database/record/instance-relationships/belongs-to.d.ts.map +1 -1
- package/build/src/database/record/instance-relationships/belongs-to.js +3 -2
- package/build/src/database/record/validators/uniqueness.d.ts.map +1 -1
- package/build/src/database/record/validators/uniqueness.js +8 -6
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +4 -4
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -12
- package/build/src/frontend-model-controller.d.ts +29 -8
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +122 -51
- package/build/src/frontend-model-resource/base-resource.d.ts +51 -24
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +83 -37
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +45 -26
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/velocious-attachment-resource.js +89 -31
- package/build/src/frontend-models/base.d.ts +53 -20
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +275 -59
- package/build/src/frontend-models/preloader.d.ts.map +1 -1
- package/build/src/frontend-models/preloader.js +6 -4
- package/build/src/frontend-models/query.d.ts +2 -2
- package/build/src/frontend-models/query.d.ts.map +1 -1
- package/build/src/frontend-models/query.js +14 -7
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +20 -3
- package/build/src/frontend-models/use-destroyed-event.d.ts +1 -1
- package/build/src/frontend-models/use-destroyed-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-destroyed-event.js +2 -2
- package/build/src/frontend-models/use-model-class-event.d.ts +2 -2
- package/build/src/frontend-models/use-model-class-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-model-class-event.js +3 -3
- package/build/src/frontend-models/use-updated-event.d.ts +1 -1
- package/build/src/frontend-models/use-updated-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-updated-event.js +2 -2
- package/build/src/frontend-models/websocket-channel.d.ts +60 -41
- package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-channel.js +225 -85
- package/build/src/frontend-models/websocket-publishers.d.ts +9 -0
- package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-publishers.js +190 -19
- package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
- package/build/src/http-server/client/websocket-session.js +11 -2
- package/build/src/http-server/websocket-channel.d.ts +20 -8
- package/build/src/http-server/websocket-channel.d.ts.map +1 -1
- package/build/src/http-server/websocket-channel.js +15 -3
- package/build/src/sync/assets/cache.d.ts +288 -0
- package/build/src/sync/assets/cache.d.ts.map +1 -0
- package/build/src/sync/assets/cache.js +883 -0
- package/build/src/sync/assets/types.d.ts +199 -0
- package/build/src/sync/assets/types.d.ts.map +1 -0
- package/build/src/sync/assets/types.js +56 -0
- package/build/src/sync/sync-api-client.d.ts.map +1 -1
- package/build/src/sync/sync-api-client.js +4 -3
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +3 -2
- package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
- package/build/src/sync/sync-envelope-replay-service.js +6 -5
- package/build/src/sync/sync-publisher.d.ts.map +1 -1
- package/build/src/sync/sync-publisher.js +4 -3
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts +2 -2
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts.map +1 -1
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +3 -3
- package/build/src/testing/test-profiler.d.ts +4 -4
- package/build/src/testing/test-profiler.d.ts.map +1 -1
- package/build/src/testing/test-profiler.js +3 -3
- package/build/src/testing/test-runner.d.ts +279 -117
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +503 -933
- package/build/src/testing/test.d.ts +11 -72
- package/build/src/testing/test.d.ts.map +1 -1
- package/build/src/testing/test.js +131 -284
- package/build/src/testing/velocious-attempt-executor.d.ts +54 -0
- package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -0
- package/build/src/testing/velocious-attempt-executor.js +408 -0
- package/build/src/testing/velocious-runner-reporter.d.ts +67 -0
- package/build/src/testing/velocious-runner-reporter.d.ts.map +1 -0
- package/build/src/testing/velocious-runner-reporter.js +240 -0
- package/build/src/testing/velocious-suite-hook-executor.d.ts +19 -0
- package/build/src/testing/velocious-suite-hook-executor.d.ts.map +1 -0
- package/build/src/testing/velocious-suite-hook-executor.js +41 -0
- package/build/src/testing/velocious-test-arguments.d.ts +39 -0
- package/build/src/testing/velocious-test-arguments.d.ts.map +1 -0
- package/build/src/testing/velocious-test-arguments.js +55 -0
- package/build/src/utils/model-primary-key.d.ts +73 -0
- package/build/src/utils/model-primary-key.d.ts.map +1 -0
- package/build/src/utils/model-primary-key.js +144 -0
- package/build/src/utils/sha256-bytes-hex.d.ts +7 -0
- package/build/src/utils/sha256-bytes-hex.d.ts.map +1 -0
- package/build/src/utils/sha256-bytes-hex.js +117 -0
- package/build/src/utils/sha256-hex.d.ts +2 -4
- package/build/src/utils/sha256-hex.d.ts.map +1 -1
- package/build/src/utils/sha256-hex.js +6 -120
- package/build/sync/assets/cache.js +998 -0
- package/build/sync/assets/types.js +62 -0
- package/build/sync/sync-api-client.js +3 -2
- package/build/sync/sync-client.js +2 -1
- package/build/sync/sync-envelope-replay-service.js +5 -4
- package/build/sync/sync-publisher.js +3 -2
- package/build/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
- package/build/testing/test-profiler.js +2 -2
- package/build/testing/test-runner.js +496 -990
- package/build/testing/test.js +164 -306
- package/build/testing/velocious-attempt-executor.js +441 -0
- package/build/testing/velocious-runner-reporter.js +264 -0
- package/build/testing/velocious-suite-hook-executor.js +43 -0
- package/build/testing/velocious-test-arguments.js +64 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/model-primary-key.js +171 -0
- package/build/utils/sha256-bytes-hex.js +132 -0
- package/build/utils/sha256-hex.js +7 -135
- package/package.json +5 -4
- package/src/authorization/ability.js +36 -13
- package/src/background-jobs/adapter.js +9 -0
- package/src/background-jobs/main.js +35 -10
- package/src/background-jobs/store.js +217 -38
- package/src/background-jobs/types.js +15 -0
- package/src/configuration-types.js +1 -1
- package/src/configuration.js +45 -9
- package/src/database/drivers/base-column.js +8 -0
- package/src/database/drivers/mssql/sql/update.js +1 -3
- package/src/database/drivers/mysql/sql/update.js +1 -3
- package/src/database/drivers/pgsql/column.js +22 -0
- package/src/database/drivers/pgsql/sql/update.js +1 -3
- package/src/database/drivers/sqlite/sql/alter-table.js +11 -1
- package/src/database/drivers/sqlite/sql/update.js +1 -3
- package/src/database/query/index.js +2 -2
- package/src/database/query/model-class-query.js +26 -10
- package/src/database/query/preloader/has-many.js +4 -1
- package/src/database/query/query-data.js +2 -1
- package/src/database/query/update-base.js +14 -0
- package/src/database/query/with-count.js +3 -1
- package/src/database/record/acts-as-list.js +37 -13
- package/src/database/record/attachments/store.js +336 -26
- package/src/database/record/auditing.js +6 -3
- package/src/database/record/index.js +197 -42
- package/src/database/record/instance-relationships/belongs-to.js +2 -1
- package/src/database/record/validators/uniqueness.js +8 -5
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
- package/src/frontend-model-controller.js +136 -53
- package/src/frontend-model-resource/base-resource.js +87 -37
- package/src/frontend-model-resource/velocious-attachment-resource.js +98 -31
- package/src/frontend-models/base.js +307 -59
- package/src/frontend-models/preloader.js +6 -3
- package/src/frontend-models/query.js +15 -6
- package/src/frontend-models/resource-definition.js +21 -2
- package/src/frontend-models/use-destroyed-event.js +1 -1
- package/src/frontend-models/use-model-class-event.js +2 -2
- package/src/frontend-models/use-updated-event.js +1 -1
- package/src/frontend-models/websocket-channel.js +256 -93
- package/src/frontend-models/websocket-publishers.js +218 -18
- package/src/http-server/client/websocket-session.js +13 -1
- package/src/http-server/websocket-channel.js +15 -2
- package/src/sync/assets/cache.js +998 -0
- package/src/sync/assets/types.js +62 -0
- package/src/sync/sync-api-client.js +3 -2
- package/src/sync/sync-client.js +2 -1
- package/src/sync/sync-envelope-replay-service.js +5 -4
- package/src/sync/sync-publisher.js +3 -2
- package/src/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
- package/src/testing/test-profiler.js +2 -2
- package/src/testing/test-runner.js +496 -990
- package/src/testing/test.js +164 -306
- package/src/testing/velocious-attempt-executor.js +441 -0
- package/src/testing/velocious-runner-reporter.js +264 -0
- package/src/testing/velocious-suite-hook-executor.js +43 -0
- package/src/testing/velocious-test-arguments.js +64 -0
- package/src/utils/model-primary-key.js +171 -0
- package/src/utils/sha256-bytes-hex.js +132 -0
- package/src/utils/sha256-hex.js +7 -135
- package/build/src/testing/testing-package-adapter.d.ts +0 -9
- package/build/src/testing/testing-package-adapter.d.ts.map +0 -1
- package/build/src/testing/testing-package-adapter.js +0 -69
- package/build/testing/testing-package-adapter.js +0 -76
- package/src/testing/testing-package-adapter.js +0 -76
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {addTrackedStackToError} from "../utils/with-tracked-stack.js"
|
|
4
3
|
import fs from "node:fs/promises"
|
|
5
4
|
import path from "path"
|
|
6
|
-
import {format} from "node:util"
|
|
7
5
|
import {AsyncLocalStorage} from "node:async_hooks"
|
|
6
|
+
import {createTestContext, defaultTestContext} from "@velocious/testing"
|
|
7
|
+
import {TestRunner as PackageTestRunner} from "@velocious/testing/runner"
|
|
8
8
|
import Application from "../../src/application.js"
|
|
9
|
-
import BacktraceCleaner from "../utils/backtrace-cleaner-node.js"
|
|
10
|
-
import {TestDatabaseAccessRevokedError} from "../environment-handlers/base.js"
|
|
11
9
|
import RequestClient from "./request-client.js"
|
|
12
10
|
import picocolors from "picocolors"
|
|
13
11
|
import restArgsError from "../utils/rest-args-error.js"
|
|
14
|
-
import {testConfig
|
|
15
|
-
import {pathToFileURL} from "url"
|
|
16
|
-
import {clearDeliveries} from "../mailer.js"
|
|
12
|
+
import {testConfig} from "./test.js"
|
|
13
|
+
import {fileURLToPath, pathToFileURL} from "url"
|
|
17
14
|
import SharedTransactionBroker from "./shared-transaction-broker.js"
|
|
18
15
|
import { SHARED_TRANSACTION_BROKER_ENV } from "./shared-transaction-proxy-driver.js"
|
|
19
|
-
import
|
|
16
|
+
import VelociousAttemptExecutor from "./velocious-attempt-executor.js"
|
|
17
|
+
import VelociousRunnerReporter, {AbortRemainingTestsError} from "./velocious-runner-reporter.js"
|
|
18
|
+
import VelociousSuiteHookExecutor from "./velocious-suite-hook-executor.js"
|
|
19
|
+
import VelociousTestArguments from "./velocious-test-arguments.js"
|
|
20
|
+
|
|
21
|
+
/** @typedef {typeof defaultTestContext} PackageTestContext */
|
|
22
|
+
/** @typedef {(typeof defaultTestContext.registry.suites)[number]} PackageSuiteDeclaration */
|
|
23
|
+
/** @typedef {PackageSuiteDeclaration["tests"][number]} PackageTestDeclaration */
|
|
24
|
+
/** @typedef {PackageSuiteDeclaration["hooks"]["beforeAll"][number]} PackageHookDeclaration */
|
|
25
|
+
/** @typedef {PackageSuiteDeclaration | PackageTestDeclaration | PackageHookDeclaration} PackageRegistration */
|
|
26
|
+
/** @typedef {{hadRetries: boolean, options: PackageTestDeclaration["options"], retries: number | undefined}} PackageRetryOptionRestoration */
|
|
20
27
|
|
|
21
|
-
/**
|
|
22
|
-
* ConsoleMethodName type.
|
|
23
|
-
* @typedef {"log" | "info" | "warn" | "error" | "debug"} ConsoleMethodName */
|
|
24
28
|
/**
|
|
25
29
|
* AttemptConsoleOutput type.
|
|
26
30
|
* @typedef {object} AttemptConsoleOutput
|
|
@@ -62,6 +66,7 @@ import {synchronizeTestingPackageTests} from "./testing-package-adapter.js"
|
|
|
62
66
|
* @property {number} [line] - Source line number.
|
|
63
67
|
* @property {string} [ownerFilePath] - Deterministic importing test file.
|
|
64
68
|
* @property {(arg: TestArgs) => (void|Promise<void>)} function - Test callback to execute.
|
|
69
|
+
* @property {PackageTestDeclaration} [declaration] - Package declaration.
|
|
65
70
|
*/
|
|
66
71
|
/**
|
|
67
72
|
* FailedTestDetail type.
|
|
@@ -73,13 +78,6 @@ import {synchronizeTestingPackageTests} from "./testing-package-adapter.js"
|
|
|
73
78
|
* @property {string} [consoleOutput] - Captured console output while test ran.
|
|
74
79
|
* @property {string} [consoleLogPath] - Saved console log path.
|
|
75
80
|
*/
|
|
76
|
-
/**
|
|
77
|
-
* ActiveAfterAllScopeEntry type.
|
|
78
|
-
* @typedef {object} ActiveAfterAllScopeEntry
|
|
79
|
-
* @property {TestsArgument} tests - Scope test tree.
|
|
80
|
-
* @property {boolean} afterAllsRun - Whether cleanup hooks have run.
|
|
81
|
-
* @property {string} [profileScopeId] - Opaque profile scope identifier.
|
|
82
|
-
*/
|
|
83
81
|
/**
|
|
84
82
|
* Defines this typedef.
|
|
85
83
|
* @typedef {(args: {configuration: import("../configuration.js").default, testArgs: TestArgs, testData: TestData}) => (void|Promise<void>)} AfterBeforeEachCallbackType
|
|
@@ -120,9 +118,8 @@ import {synchronizeTestingPackageTests} from "./testing-package-adapter.js"
|
|
|
120
118
|
* @property {Record<string, TestsArgument>} subs - Optional child nodes. Each item is another `Node`, allowing recursion.
|
|
121
119
|
*/
|
|
122
120
|
/**
|
|
123
|
-
* Marks the error thrown by
|
|
124
|
-
* lifecycle
|
|
125
|
-
* ordinary test failure (the promise already settled).
|
|
121
|
+
* Marks the error thrown by the attempt timeout so the runner can distinguish
|
|
122
|
+
* detached lifecycle cleanup from an ordinary test failure.
|
|
126
123
|
* @typedef {Error & {velociousTestTimeout?: true}} TestTimeoutError
|
|
127
124
|
*/
|
|
128
125
|
/**
|
|
@@ -145,99 +142,6 @@ import {synchronizeTestingPackageTests} from "./testing-package-adapter.js"
|
|
|
145
142
|
* @property {import("../database/pool/base.js").TestSharedConnectionRegistration | undefined} sharedRegistration - Physical-key shared registration once published.
|
|
146
143
|
*/
|
|
147
144
|
|
|
148
|
-
/**
|
|
149
|
-
* Runs run with timeout.
|
|
150
|
-
*
|
|
151
|
-
* On timeout the wrapped `promise` is NOT cancelled — it keeps running detached.
|
|
152
|
-
* The rejected error is tagged with `velociousTestTimeout` so the runner knows
|
|
153
|
-
* the lifecycle (and its afterEach database cleanup) is still in flight and can
|
|
154
|
-
* wait for it to settle before the next test reuses the shared connection.
|
|
155
|
-
* @param {Promise<ReturnType<typeof JSON.parse>> | ReturnType<typeof JSON.parse>} promise - Promise or value.
|
|
156
|
-
* @param {number} timeoutMs - Timeout in milliseconds.
|
|
157
|
-
* @param {string} testDescription - Test description.
|
|
158
|
-
* @returns {Promise<ReturnType<typeof JSON.parse>>} - Resolves or rejects based on timeout or promise result.
|
|
159
|
-
*/
|
|
160
|
-
function runWithTimeout(promise, timeoutMs, testDescription) {
|
|
161
|
-
const timeoutSeconds = (timeoutMs / 1000).toFixed(3).replace(/\.?0+$/, "")
|
|
162
|
-
/** @type {TestTimeoutError} */
|
|
163
|
-
const timeoutError = new Error(`Timed out after ${timeoutSeconds}s: ${testDescription}`)
|
|
164
|
-
timeoutError.velociousTestTimeout = true
|
|
165
|
-
|
|
166
|
-
return new Promise((resolve, reject) => {
|
|
167
|
-
const timeout = setTimeout(() => reject(timeoutError), timeoutMs)
|
|
168
|
-
|
|
169
|
-
Promise.resolve(promise).then((result) => {
|
|
170
|
-
clearTimeout(timeout)
|
|
171
|
-
resolve(result)
|
|
172
|
-
}).catch((error) => {
|
|
173
|
-
clearTimeout(timeout)
|
|
174
|
-
reject(error)
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Waits for an abandoned (timed-out) test lifecycle to settle, bounded by a
|
|
181
|
-
* grace period, so its afterEach database cleanup runs on the shared connection
|
|
182
|
-
* before the next test reuses it. Returns the fulfillment/rejection outcome if
|
|
183
|
-
* the lifecycle settles, or a pending outcome once the grace elapses.
|
|
184
|
-
*
|
|
185
|
-
* The grace timer is kept ref'd so it cannot let Node exit with an unsettled
|
|
186
|
-
* top-level await when the timed-out lifecycle has no ref'd handles of its own
|
|
187
|
-
* (for example a stalled mocked async API). Once the caller continues past this
|
|
188
|
-
* await, the timer has already resolved and no longer anchors the event loop.
|
|
189
|
-
* @param {Promise<ReturnType<typeof JSON.parse>>} lifecycle - The abandoned per-test lifecycle promise.
|
|
190
|
-
* @param {number} graceMs - Maximum time to wait for the lifecycle to settle.
|
|
191
|
-
* @returns {Promise<{settled: false} | {settled: true, status: "fulfilled"} | {settled: true, status: "rejected", reason: unknown}>} - Settlement outcome.
|
|
192
|
-
*/
|
|
193
|
-
function awaitSettledOrGrace(lifecycle, graceMs) {
|
|
194
|
-
return new Promise((resolve) => {
|
|
195
|
-
let settled = false
|
|
196
|
-
const graceTimer = setTimeout(() => {
|
|
197
|
-
if (settled) return
|
|
198
|
-
|
|
199
|
-
settled = true
|
|
200
|
-
resolve({settled: false})
|
|
201
|
-
}, graceMs)
|
|
202
|
-
|
|
203
|
-
Promise.resolve(lifecycle).then(
|
|
204
|
-
() => {
|
|
205
|
-
if (settled) return
|
|
206
|
-
|
|
207
|
-
settled = true
|
|
208
|
-
clearTimeout(graceTimer)
|
|
209
|
-
resolve({settled: true, status: "fulfilled"})
|
|
210
|
-
},
|
|
211
|
-
(reason) => {
|
|
212
|
-
if (settled) return
|
|
213
|
-
|
|
214
|
-
settled = true
|
|
215
|
-
clearTimeout(graceTimer)
|
|
216
|
-
resolve({settled: true, status: "rejected", reason})
|
|
217
|
-
}
|
|
218
|
-
)
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Checks whether a late lifecycle stopped only because its test access was revoked.
|
|
224
|
-
* @param {unknown} error - Lifecycle rejection.
|
|
225
|
-
* @returns {boolean} - Whether every contained error is expected revocation.
|
|
226
|
-
*/
|
|
227
|
-
function isTestDatabaseAccessRevocation(error) {
|
|
228
|
-
if (error instanceof TestDatabaseAccessRevokedError) return true
|
|
229
|
-
if (error instanceof AggregateError) {
|
|
230
|
-
return error.errors.length > 0 && error.errors.every((nestedError) => isTestDatabaseAccessRevocation(nestedError))
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return false
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Captured console methods.
|
|
238
|
-
* @type {ConsoleMethodName[]} */
|
|
239
|
-
const CAPTURED_CONSOLE_METHODS = ["log", "info", "warn", "error", "debug"]
|
|
240
|
-
|
|
241
145
|
/**
|
|
242
146
|
* Runs to file slug.
|
|
243
147
|
* @param {string} value - Value to sanitize.
|
|
@@ -252,10 +156,8 @@ function toFileSlug(value) {
|
|
|
252
156
|
}
|
|
253
157
|
|
|
254
158
|
export default class TestRunner {
|
|
255
|
-
/**
|
|
256
|
-
|
|
257
|
-
* @type {ActiveAfterAllScopeEntry[]} */
|
|
258
|
-
_activeAfterAllScopes
|
|
159
|
+
/** @type {PackageTestContext} */
|
|
160
|
+
_context
|
|
259
161
|
|
|
260
162
|
/**
|
|
261
163
|
* Narrows the runtime value to the documented type.
|
|
@@ -266,6 +168,7 @@ export default class TestRunner {
|
|
|
266
168
|
* Runs constructor.
|
|
267
169
|
* @param {object} args - Options object.
|
|
268
170
|
* @param {import("../configuration.js").default} args.configuration - Configuration instance.
|
|
171
|
+
* @param {PackageTestContext} [args.context] - Declaration context.
|
|
269
172
|
* @param {string[] | string} [args.excludeTags] - Tags to exclude.
|
|
270
173
|
* @param {string[] | string} [args.includeTags] - Tags to include.
|
|
271
174
|
* @param {Array<string>} args.testFiles - Test files.
|
|
@@ -273,18 +176,17 @@ export default class TestRunner {
|
|
|
273
176
|
* @param {RegExp[]} [args.examplePatterns] - Example patterns.
|
|
274
177
|
* @param {import("./test-profiler.js").default} [args.profiler] - Opt-in profiler.
|
|
275
178
|
*/
|
|
276
|
-
constructor({configuration, excludeTags, includeTags, testFiles, lineFilters, examplePatterns, profiler, ...restArgs}) {
|
|
179
|
+
constructor({configuration, context = defaultTestContext, excludeTags, includeTags, testFiles, lineFilters, examplePatterns, profiler, ...restArgs}) {
|
|
277
180
|
restArgsError(restArgs)
|
|
278
181
|
|
|
279
182
|
if (!configuration) throw new Error("configuration is required")
|
|
280
183
|
|
|
281
184
|
this._configuration = configuration
|
|
185
|
+
this._context = context
|
|
282
186
|
this._sharedTransactionCoordinatorOwnerStorage = new AsyncLocalStorage()
|
|
283
187
|
this._testDatabaseAccessScopeStorage = new AsyncLocalStorage()
|
|
284
188
|
this._excludeTags = this.normalizeTags(excludeTags)
|
|
285
|
-
this._excludeTagSet = new Set(this._excludeTags)
|
|
286
189
|
this._includeTags = this.normalizeTags(includeTags)
|
|
287
|
-
this._includeTagSet = new Set(this._includeTags)
|
|
288
190
|
this._testFiles = testFiles
|
|
289
191
|
this._lineFilters = lineFilters || {}
|
|
290
192
|
this._examplePatterns = examplePatterns || []
|
|
@@ -294,14 +196,49 @@ export default class TestRunner {
|
|
|
294
196
|
this._failedTests = 0
|
|
295
197
|
this._successfulTests = 0
|
|
296
198
|
this._testsCount = 0
|
|
297
|
-
this._activeAfterAllScopes = []
|
|
298
199
|
this._failedTestDetails = []
|
|
299
200
|
/** @type {{fullDescription: string, filePath: string, line: number} | null} */
|
|
300
201
|
this._lastTestContext = null
|
|
301
202
|
/** @type {Array<{fullDescription: string, filePath: string, line: number, durationMs: number}>} */
|
|
302
203
|
this._testDurations = []
|
|
204
|
+
/** @type {WeakMap<PackageTestDeclaration, {testArgs: TestArgs, testData: TestData}>} */
|
|
205
|
+
this._testCompatibility = new WeakMap()
|
|
206
|
+
/** @type {WeakSet<PackageTestDeclaration>} */
|
|
207
|
+
this._injectedTests = new WeakSet()
|
|
208
|
+
/** @type {WeakSet<PackageTestDeclaration>} */
|
|
209
|
+
this._completedTests = new WeakSet()
|
|
210
|
+
/** @type {WeakMap<PackageTestDeclaration, {descriptions: string[], testDescription: string, fullDescription: string, ownerFilePath: string | undefined, suites: PackageSuiteDeclaration[]}>} */
|
|
211
|
+
this._testMetadata = new WeakMap()
|
|
212
|
+
/** @type {WeakMap<PackageHookDeclaration, {declarationIndex: number, declarationScopeId: string | undefined, ownerFilePath: string | undefined}>} */
|
|
213
|
+
this._hookMetadata = new WeakMap()
|
|
214
|
+
/** @type {WeakMap<PackageTestDeclaration, Map<number, {abortRemainingTests: boolean, error: ReturnType<typeof JSON.parse>, failed: boolean}>>} */
|
|
215
|
+
this._attemptOutcomes = new WeakMap()
|
|
216
|
+
/** @type {Array<{suite: PackageSuiteDeclaration, phase: "beforeAll" | "afterAll", error: ReturnType<typeof JSON.parse>}>} */
|
|
217
|
+
this._suiteHookFailures = []
|
|
218
|
+
/** @type {Map<string, PackageTestDeclaration[]>} */
|
|
219
|
+
this._testsByFullName = new Map()
|
|
220
|
+
/** @type {WeakMap<PackageRegistration, string>} */
|
|
221
|
+
this._declarationOwners = new WeakMap()
|
|
222
|
+
/** @type {PackageTestRunner | undefined} */
|
|
223
|
+
this._packageRunner = undefined
|
|
224
|
+
/** @type {import("@velocious/testing/runner").TestRunResult | undefined} */
|
|
225
|
+
this._packageResult = undefined
|
|
226
|
+
/** @type {Map<string, TestData> | undefined} */
|
|
227
|
+
this._legacyFixtureDataByFullName = undefined
|
|
228
|
+
/** @type {{filePath?: string, line?: number}} */
|
|
229
|
+
this._legacyFixtureLocation = {}
|
|
230
|
+
this._attemptExecutor = new VelociousAttemptExecutor({testRunner: this})
|
|
231
|
+
this._runnerReporter = new VelociousRunnerReporter({testRunner: this})
|
|
232
|
+
this._suiteHookExecutor = new VelociousSuiteHookExecutor({testRunner: this})
|
|
233
|
+
this._testArguments = new VelociousTestArguments({testRunner: this})
|
|
303
234
|
}
|
|
304
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Gets the package declaration context.
|
|
238
|
+
* @returns {PackageTestContext} - Package declaration context.
|
|
239
|
+
*/
|
|
240
|
+
getTestContext() { return this._context }
|
|
241
|
+
|
|
305
242
|
/**
|
|
306
243
|
* Runs get configuration.
|
|
307
244
|
* @returns {import("../configuration.js").default} - The configuration.
|
|
@@ -343,24 +280,6 @@ export default class TestRunner {
|
|
|
343
280
|
return await this._profiler.runSpan(metadata, callback)
|
|
344
281
|
}
|
|
345
282
|
|
|
346
|
-
/**
|
|
347
|
-
* Adds declaration metadata to hooks only for an active profile.
|
|
348
|
-
* @template {AfterBeforeEachCallbackObjectType | BeforeAfterAllCallbackObjectType} T
|
|
349
|
-
* @param {T[]} hooks - Hooks declared in one scope.
|
|
350
|
-
* @param {string | undefined} declarationScopeId - Profile scope identifier.
|
|
351
|
-
* @param {string | undefined} ownerFilePath - Scope owner file.
|
|
352
|
-
* @returns {T[]} - Profile-aware hook entries.
|
|
353
|
-
*/
|
|
354
|
-
profileHookEntries(hooks, declarationScopeId, ownerFilePath) {
|
|
355
|
-
if (!this._profiler) return hooks
|
|
356
|
-
|
|
357
|
-
return hooks.map((hook, declarationIndex) => Object.assign({}, hook, {
|
|
358
|
-
declarationIndex: hook.declarationIndex ?? declarationIndex,
|
|
359
|
-
declarationScopeId: hook.declarationScopeId ?? declarationScopeId,
|
|
360
|
-
ownerFilePath: hook.ownerFilePath ?? ownerFilePath
|
|
361
|
-
}))
|
|
362
|
-
}
|
|
363
|
-
|
|
364
283
|
/**
|
|
365
284
|
* Runs normalize tags.
|
|
366
285
|
* @param {string[] | string | undefined} tags - Tags.
|
|
@@ -666,107 +585,6 @@ export default class TestRunner {
|
|
|
666
585
|
return new Set([...this._excludeTags, ...configTags])
|
|
667
586
|
}
|
|
668
587
|
|
|
669
|
-
/**
|
|
670
|
-
* Runs has matching tag.
|
|
671
|
-
* @param {string[] | string | undefined} testTags - Test tags.
|
|
672
|
-
* @param {Set<string>} tagSet - Tag set.
|
|
673
|
-
* @returns {boolean} - Whether any tags match.
|
|
674
|
-
*/
|
|
675
|
-
hasMatchingTag(testTags, tagSet) {
|
|
676
|
-
if (!tagSet.size) return false
|
|
677
|
-
|
|
678
|
-
const normalized = this.normalizeTags(testTags)
|
|
679
|
-
|
|
680
|
-
for (const tag of normalized) {
|
|
681
|
-
if (tagSet.has(tag)) return true
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
return false
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/**
|
|
688
|
-
* Runs has runnable tests.
|
|
689
|
-
* @param {TestsArgument} tests - Tests.
|
|
690
|
-
* @param {string[]} [descriptions] - Description stack.
|
|
691
|
-
* @param {boolean} [lineMatchedInScope] - Whether line matched in scope.
|
|
692
|
-
* @returns {boolean} - Whether any tests in this scope will run.
|
|
693
|
-
*/
|
|
694
|
-
hasRunnableTests(tests, descriptions = [], lineMatchedInScope = false) {
|
|
695
|
-
for (const testDescription in tests.tests) {
|
|
696
|
-
const testData = tests.tests[testDescription]
|
|
697
|
-
const testArgs = /** @type {TestArgs} */ (Object.assign({}, testData.args))
|
|
698
|
-
const includeByLine = lineMatchedInScope || this.matchesLineFilter(testData)
|
|
699
|
-
|
|
700
|
-
if (this._onlyFocussed && !testArgs.focus) continue
|
|
701
|
-
if (this.shouldSkipTest(testArgs, testData, testDescription, descriptions, includeByLine)) continue
|
|
702
|
-
|
|
703
|
-
return true
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
for (const subDescription in tests.subs) {
|
|
707
|
-
const subTest = tests.subs[subDescription]
|
|
708
|
-
const scopeLineMatch = lineMatchedInScope || this.matchesLineFilter(subTest)
|
|
709
|
-
const nextDescriptions = descriptions.concat([subDescription])
|
|
710
|
-
|
|
711
|
-
if (this._onlyFocussed && !subTest.anyTestsFocussed) continue
|
|
712
|
-
if (this.hasRunnableTests(subTest, nextDescriptions, scopeLineMatch)) return true
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
return false
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* Runs should skip test.
|
|
720
|
-
* @param {TestArgs} testArgs - Test args.
|
|
721
|
-
* @param {TestData} testData - Test data.
|
|
722
|
-
* @param {string} testDescription - Test description.
|
|
723
|
-
* @param {string[]} descriptions - Description stack.
|
|
724
|
-
* @param {boolean} lineMatchedInScope - Whether line matched in scope.
|
|
725
|
-
* @returns {boolean} - Whether the test should be skipped.
|
|
726
|
-
*/
|
|
727
|
-
shouldSkipTest(testArgs, testData, testDescription, descriptions, lineMatchedInScope) {
|
|
728
|
-
if (this.hasTag(testArgs, "browser-only") && !this.isBrowserTestMode()) return true
|
|
729
|
-
if (this.hasMatchingTag(testArgs.tags, this.getExcludeTagSet())) return true
|
|
730
|
-
|
|
731
|
-
if (this._includeTagSet.size > 0 && !testArgs.focus) {
|
|
732
|
-
if (!this.hasMatchingTag(testArgs.tags, this._includeTagSet)) return true
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
if (this.getExamplePatterns().length > 0) {
|
|
736
|
-
const fullDescription = this.buildFullDescription(descriptions, testDescription)
|
|
737
|
-
const matches = this.getExamplePatterns().some((pattern) => {
|
|
738
|
-
pattern.lastIndex = 0
|
|
739
|
-
return pattern.test(fullDescription)
|
|
740
|
-
})
|
|
741
|
-
|
|
742
|
-
if (!matches) return true
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const lineFilters = this.getLineFilters()
|
|
746
|
-
|
|
747
|
-
if (Object.keys(lineFilters).length > 0) {
|
|
748
|
-
if (!lineMatchedInScope && !this.matchesLineFilter(testData)) return true
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
return false
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
/**
|
|
755
|
-
* Runs matches line filter.
|
|
756
|
-
* @param {TestData | TestsArgument} entry - Test entry.
|
|
757
|
-
* @returns {boolean} - Whether line filter matches entry.
|
|
758
|
-
*/
|
|
759
|
-
matchesLineFilter(entry) {
|
|
760
|
-
if (!entry || !entry.filePath || !entry.line) return false
|
|
761
|
-
|
|
762
|
-
const filePath = path.resolve(entry.filePath)
|
|
763
|
-
const lines = this.getLineFilters()[filePath]
|
|
764
|
-
|
|
765
|
-
if (!lines || lines.length === 0) return false
|
|
766
|
-
|
|
767
|
-
return lines.includes(entry.line)
|
|
768
|
-
}
|
|
769
|
-
|
|
770
588
|
/**
|
|
771
589
|
* Runs build full description.
|
|
772
590
|
* @param {string[]} descriptions - Description stack.
|
|
@@ -1128,61 +946,48 @@ export default class TestRunner {
|
|
|
1128
946
|
|
|
1129
947
|
if (!this._profiler) {
|
|
1130
948
|
await environmentHandler.importTestFiles(this.getTestFiles())
|
|
1131
|
-
synchronizeTestingPackageTests(tests)
|
|
1132
949
|
return
|
|
1133
950
|
}
|
|
1134
951
|
|
|
1135
952
|
for (const testFile of this.getTestFiles()) {
|
|
1136
|
-
const existingRegistrations = this.testRegistrationObjects(
|
|
953
|
+
const existingRegistrations = this.testRegistrationObjects()
|
|
1137
954
|
|
|
1138
955
|
await this._profiler.measurePhase("imports", async () => {
|
|
1139
956
|
await environmentHandler.importTestFiles([testFile])
|
|
1140
957
|
}, {filePath: testFile})
|
|
1141
|
-
|
|
1142
|
-
this.assignTestRegistrationOwnership(tests, existingRegistrations, testFile)
|
|
958
|
+
this.assignTestRegistrationOwnership(existingRegistrations, testFile)
|
|
1143
959
|
}
|
|
1144
960
|
}
|
|
1145
961
|
|
|
1146
962
|
/**
|
|
1147
|
-
* Collects
|
|
1148
|
-
* @param {
|
|
1149
|
-
* @
|
|
1150
|
-
* @returns {Set<object>} - Registration identities.
|
|
963
|
+
* Collects package declaration objects by identity.
|
|
964
|
+
* @param {Set<PackageRegistration>} [registrations] - Accumulated identities.
|
|
965
|
+
* @returns {Set<PackageRegistration>} - Registration identities.
|
|
1151
966
|
*/
|
|
1152
|
-
testRegistrationObjects(
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
967
|
+
testRegistrationObjects(registrations = new Set()) {
|
|
968
|
+
const visit = (/** @type {PackageSuiteDeclaration} */ suite) => {
|
|
969
|
+
registrations.add(suite)
|
|
970
|
+
for (const hook of [...suite.hooks.beforeAll, ...suite.hooks.beforeEach, ...suite.hooks.afterEach, ...suite.hooks.afterAll]) {
|
|
971
|
+
registrations.add(hook)
|
|
972
|
+
}
|
|
973
|
+
for (const testDeclaration of suite.tests) registrations.add(testDeclaration)
|
|
974
|
+
for (const childSuite of suite.suites) visit(childSuite)
|
|
1157
975
|
}
|
|
1158
976
|
|
|
1159
|
-
for (const
|
|
1160
|
-
for (const childScope of Object.values(scope.subs)) this.testRegistrationObjects(childScope, registrations)
|
|
977
|
+
for (const suite of this.getTestContext().registry.suites) visit(suite)
|
|
1161
978
|
|
|
1162
979
|
return registrations
|
|
1163
980
|
}
|
|
1164
981
|
|
|
1165
982
|
/**
|
|
1166
|
-
* Assigns deterministic ownership to
|
|
1167
|
-
*
|
|
1168
|
-
* @param {TestsArgument} scope - Test scope.
|
|
1169
|
-
* @param {Set<object>} previousRegistrations - Identities present before import.
|
|
983
|
+
* Assigns deterministic ownership to package declarations added by one entry file.
|
|
984
|
+
* @param {Set<PackageRegistration>} previousRegistrations - Identities present before import.
|
|
1170
985
|
* @param {string} ownerFilePath - Importing entry file.
|
|
1171
986
|
* @returns {void}
|
|
1172
987
|
*/
|
|
1173
|
-
assignTestRegistrationOwnership(
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
for (const hook of [...scope.beforeAlls, ...scope.beforeEaches, ...scope.afterEaches, ...scope.afterAlls]) {
|
|
1177
|
-
if (!previousRegistrations.has(hook)) hook.ownerFilePath ??= ownerFilePath
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
for (const testData of Object.values(scope.tests)) {
|
|
1181
|
-
if (!previousRegistrations.has(testData)) testData.ownerFilePath ??= ownerFilePath
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
for (const childScope of Object.values(scope.subs)) {
|
|
1185
|
-
this.assignTestRegistrationOwnership(childScope, previousRegistrations, ownerFilePath)
|
|
988
|
+
assignTestRegistrationOwnership(previousRegistrations, ownerFilePath) {
|
|
989
|
+
for (const registration of this.testRegistrationObjects()) {
|
|
990
|
+
if (!previousRegistrations.has(registration)) this._declarationOwners.set(registration, ownerFilePath)
|
|
1186
991
|
}
|
|
1187
992
|
}
|
|
1188
993
|
|
|
@@ -1190,7 +995,7 @@ export default class TestRunner {
|
|
|
1190
995
|
* Runs is failed.
|
|
1191
996
|
* @returns {boolean} - Whether failed.
|
|
1192
997
|
*/
|
|
1193
|
-
isFailed() { return this._failedTests !== undefined && this._failedTests > 0 }
|
|
998
|
+
isFailed() { return this._failedTests !== undefined && (this._failedTests > 0 || this._packageResult?.status === "failed") }
|
|
1194
999
|
|
|
1195
1000
|
/**
|
|
1196
1001
|
* Runs get failed tests.
|
|
@@ -1279,7 +1084,7 @@ export default class TestRunner {
|
|
|
1279
1084
|
* @returns {number} - The executed tests count.
|
|
1280
1085
|
*/
|
|
1281
1086
|
getExecutedTestsCount() {
|
|
1282
|
-
return this._testDurations.length
|
|
1087
|
+
return this._packageResult?.tests.length ?? this._testDurations.length
|
|
1283
1088
|
}
|
|
1284
1089
|
|
|
1285
1090
|
/**
|
|
@@ -1305,17 +1110,79 @@ export default class TestRunner {
|
|
|
1305
1110
|
this._abortRemainingTests = false
|
|
1306
1111
|
this._failedTestDetails = []
|
|
1307
1112
|
this._testDurations = []
|
|
1113
|
+
this._testCompatibility = new WeakMap()
|
|
1114
|
+
this._injectedTests = new WeakSet()
|
|
1115
|
+
this._completedTests = new WeakSet()
|
|
1116
|
+
this._testMetadata = new WeakMap()
|
|
1117
|
+
this._hookMetadata = new WeakMap()
|
|
1118
|
+
this._attemptOutcomes = new WeakMap()
|
|
1119
|
+
this._suiteHookFailures = []
|
|
1120
|
+
this._testsByFullName = new Map()
|
|
1121
|
+
this._packageResult = undefined
|
|
1122
|
+
const context = this.getTestContext()
|
|
1123
|
+
/** @type {string | undefined} */
|
|
1124
|
+
let ownerFilePath
|
|
1125
|
+
|
|
1126
|
+
context.reset({config: true})
|
|
1127
|
+
context.setDeclarationLocator(() => this.captureTestDeclarationLocation(ownerFilePath))
|
|
1308
1128
|
const testingConfigPath = this.getConfiguration().getTesting()
|
|
1309
1129
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
await this.
|
|
1313
|
-
|
|
1130
|
+
await context.describe("", {databaseCleaning: {transaction: true}}, async () => {
|
|
1131
|
+
if (testingConfigPath) {
|
|
1132
|
+
await this.runProfileSpan({phase: "testing config/global setup"}, async () => {
|
|
1133
|
+
await this.getConfiguration().getEnvironmentHandler().importTestingConfigPath()
|
|
1134
|
+
})
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (!this._profiler) {
|
|
1138
|
+
await this.importTestFiles()
|
|
1139
|
+
} else {
|
|
1140
|
+
for (const testFile of this.getTestFiles()) {
|
|
1141
|
+
ownerFilePath = testFile
|
|
1142
|
+
const existingRegistrations = this.testRegistrationObjects()
|
|
1143
|
+
|
|
1144
|
+
await this._profiler.measurePhase("imports", async () => {
|
|
1145
|
+
await this.getConfiguration().getEnvironmentHandler().importTestFiles([testFile])
|
|
1146
|
+
}, {filePath: testFile})
|
|
1147
|
+
this.assignTestRegistrationOwnership(existingRegistrations, testFile)
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
})
|
|
1151
|
+
ownerFilePath = undefined
|
|
1152
|
+
this.analyzeDeclarations()
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Captures a test source location without attributing package/facade frames.
|
|
1157
|
+
* @param {string | undefined} ownerFilePath - Importing entry file fallback.
|
|
1158
|
+
* @returns {{filePath?: string, line?: number}} - Declaration location.
|
|
1159
|
+
*/
|
|
1160
|
+
captureTestDeclarationLocation(ownerFilePath) {
|
|
1161
|
+
const stack = new Error().stack?.split("\n") || []
|
|
1162
|
+
|
|
1163
|
+
for (const stackLine of stack) {
|
|
1164
|
+
const match = stackLine.match(/(?:\(|\s)(file:\/\/.*?|\/[^"]*?):(\d+):(\d+)\)?$/u)
|
|
1165
|
+
if (!match) continue
|
|
1166
|
+
|
|
1167
|
+
let filePath = match[1]
|
|
1168
|
+
if (filePath.startsWith("file://")) {
|
|
1169
|
+
try {
|
|
1170
|
+
filePath = fileURLToPath(filePath)
|
|
1171
|
+
} catch {
|
|
1172
|
+
continue
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
const resolvedFilePath = path.resolve(filePath)
|
|
1176
|
+
const portablePath = resolvedFilePath.replaceAll(path.sep, "/")
|
|
1177
|
+
|
|
1178
|
+
if (portablePath.endsWith("/src/testing/test-runner.js")) continue
|
|
1179
|
+
if (portablePath.endsWith("/src/testing/test.js")) continue
|
|
1180
|
+
if (portablePath.includes("/node_modules/@velocious/testing/")) continue
|
|
1181
|
+
|
|
1182
|
+
return {filePath: resolvedFilePath, line: Number(match[2])}
|
|
1314
1183
|
}
|
|
1315
1184
|
|
|
1316
|
-
|
|
1317
|
-
await this.analyzeTests(tests)
|
|
1318
|
-
this._onlyFocussed = this.anyTestsFocussed
|
|
1185
|
+
return ownerFilePath ? {filePath: ownerFilePath} : {}
|
|
1319
1186
|
}
|
|
1320
1187
|
|
|
1321
1188
|
/**
|
|
@@ -1440,13 +1307,7 @@ export default class TestRunner {
|
|
|
1440
1307
|
process.on("uncaughtException", onUncaughtException)
|
|
1441
1308
|
|
|
1442
1309
|
try {
|
|
1443
|
-
await this.
|
|
1444
|
-
afterEaches: [],
|
|
1445
|
-
beforeEaches: [],
|
|
1446
|
-
tests,
|
|
1447
|
-
descriptions: [],
|
|
1448
|
-
indentLevel: 0
|
|
1449
|
-
})
|
|
1310
|
+
await this.runPackageTests()
|
|
1450
1311
|
|
|
1451
1312
|
// A rejection scheduled by the final test (a detached rejected promise,
|
|
1452
1313
|
// or an afterCommit callback rejecting as the suite drains) is reported
|
|
@@ -1467,708 +1328,410 @@ export default class TestRunner {
|
|
|
1467
1328
|
* @returns {Promise<void>} - Resolves when cleanup hooks finish.
|
|
1468
1329
|
*/
|
|
1469
1330
|
async runAfterAllsForActiveScopes() {
|
|
1470
|
-
const
|
|
1471
|
-
/** @type {unknown[]} */
|
|
1472
|
-
const afterAllErrors = []
|
|
1331
|
+
const failureStart = this._suiteHookFailures.length
|
|
1473
1332
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1333
|
+
await this._packageRunner?.cleanupActiveSuites()
|
|
1334
|
+
this.throwAfterAllFailures(this._suiteHookFailures.slice(failureStart))
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/** Builds declaration metadata used only by framework adapters and projections. */
|
|
1338
|
+
analyzeDeclarations() {
|
|
1339
|
+
const visit = (/** @type {PackageSuiteDeclaration} */ suite, /** @type {PackageSuiteDeclaration[]} */ ancestors, /** @type {string | undefined} */ parentProfileScopeId) => {
|
|
1340
|
+
const suites = [...ancestors, suite]
|
|
1341
|
+
const descriptions = suites.map((entry) => entry.name).filter((name) => name !== "")
|
|
1342
|
+
const ownerFilePath = this._declarationOwners.get(suite) ?? suite.location.filePath
|
|
1343
|
+
const profileScopeId = this._profiler?.scopeId(suite, {
|
|
1344
|
+
descriptions,
|
|
1345
|
+
filePath: ownerFilePath,
|
|
1346
|
+
line: suite.location.line,
|
|
1347
|
+
parentId: parentProfileScopeId
|
|
1348
|
+
})
|
|
1349
|
+
|
|
1350
|
+
for (const hooks of Object.values(suite.hooks)) {
|
|
1351
|
+
hooks.forEach((hook, declarationIndex) => {
|
|
1352
|
+
this._hookMetadata.set(hook, {
|
|
1353
|
+
declarationIndex,
|
|
1354
|
+
declarationScopeId: profileScopeId,
|
|
1355
|
+
ownerFilePath: this._declarationOwners.get(hook) ?? hook.location.filePath ?? ownerFilePath
|
|
1356
|
+
})
|
|
1357
|
+
})
|
|
1479
1358
|
}
|
|
1480
|
-
}
|
|
1481
1359
|
|
|
1482
|
-
|
|
1360
|
+
for (const testDeclaration of suite.tests) {
|
|
1361
|
+
const fullDescription = this.buildFullDescription(descriptions, testDeclaration.name)
|
|
1362
|
+
const declarations = this._testsByFullName.get(fullDescription) || []
|
|
1363
|
+
|
|
1364
|
+
declarations.push(testDeclaration)
|
|
1365
|
+
this._testsByFullName.set(fullDescription, declarations)
|
|
1366
|
+
this._testMetadata.set(testDeclaration, {
|
|
1367
|
+
descriptions,
|
|
1368
|
+
testDescription: testDeclaration.name,
|
|
1369
|
+
fullDescription,
|
|
1370
|
+
ownerFilePath: this._declarationOwners.get(testDeclaration) ?? testDeclaration.location.filePath ?? ownerFilePath,
|
|
1371
|
+
suites
|
|
1372
|
+
})
|
|
1373
|
+
const legacyTestData = this._legacyFixtureDataByFullName?.get(fullDescription)
|
|
1374
|
+
if (legacyTestData) {
|
|
1375
|
+
this._testCompatibility.set(testDeclaration, {
|
|
1376
|
+
testArgs: this._testArguments.copy(testDeclaration),
|
|
1377
|
+
testData: legacyTestData
|
|
1378
|
+
})
|
|
1379
|
+
}
|
|
1380
|
+
this._testsCount++
|
|
1381
|
+
if (testDeclaration.state === "run" && (testDeclaration.focus || suites.some((entry) => entry.focus))) {
|
|
1382
|
+
this.anyTestsFocussed = true
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1483
1385
|
|
|
1484
|
-
|
|
1485
|
-
if (afterAllErrors.length > 1) {
|
|
1486
|
-
throw new AggregateError(afterAllErrors, "Multiple active afterAll scopes failed", {cause: afterAllErrors[0]})
|
|
1386
|
+
for (const childSuite of suite.suites) visit(childSuite, suites, profileScopeId)
|
|
1487
1387
|
}
|
|
1388
|
+
|
|
1389
|
+
for (const suite of this.getTestContext().registry.suites) visit(suite, [], undefined)
|
|
1488
1390
|
}
|
|
1489
1391
|
|
|
1490
1392
|
/**
|
|
1491
|
-
*
|
|
1492
|
-
* @param {
|
|
1493
|
-
* @returns {{
|
|
1393
|
+
* Gets package hook compatibility metadata.
|
|
1394
|
+
* @param {PackageHookDeclaration} hook - Package hook declaration.
|
|
1395
|
+
* @returns {{declarationIndex: number, declarationScopeId: string | undefined, ownerFilePath: string | undefined}} - Hook metadata.
|
|
1494
1396
|
*/
|
|
1495
|
-
|
|
1496
|
-
|
|
1397
|
+
hookMetadata(hook) {
|
|
1398
|
+
return this._hookMetadata.get(hook) || {declarationIndex: 0, declarationScopeId: undefined, ownerFilePath: hook.location.filePath}
|
|
1399
|
+
}
|
|
1497
1400
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1401
|
+
/**
|
|
1402
|
+
* Gets package test compatibility metadata.
|
|
1403
|
+
* @param {PackageTestDeclaration} test - Package test declaration.
|
|
1404
|
+
* @returns {{descriptions: string[], testDescription: string, fullDescription: string, ownerFilePath: string | undefined, suites: PackageSuiteDeclaration[]}} - Declaration metadata.
|
|
1405
|
+
*/
|
|
1406
|
+
testMetadata(test) {
|
|
1407
|
+
const metadata = this._testMetadata.get(test)
|
|
1408
|
+
if (!metadata) throw new Error(`Missing package test metadata: ${test.name}`)
|
|
1409
|
+
return metadata
|
|
1410
|
+
}
|
|
1501
1411
|
|
|
1502
|
-
|
|
1412
|
+
/**
|
|
1413
|
+
* Gets stable compatibility data for a package declaration.
|
|
1414
|
+
* @param {PackageTestDeclaration} test - Package test declaration.
|
|
1415
|
+
* @returns {{testArgs: TestArgs, testData: TestData}} - Stable compatibility data.
|
|
1416
|
+
*/
|
|
1417
|
+
testData(test) {
|
|
1418
|
+
let compatibility = this._testCompatibility.get(test)
|
|
1503
1419
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1420
|
+
if (!compatibility) {
|
|
1421
|
+
const testArgs = this._testArguments.copy(test)
|
|
1422
|
+
const metadata = this.testMetadata(test)
|
|
1423
|
+
const testData = {
|
|
1424
|
+
args: testArgs,
|
|
1425
|
+
declaration: test,
|
|
1426
|
+
filePath: test.location.filePath,
|
|
1427
|
+
function: test.callback,
|
|
1428
|
+
line: test.location.line,
|
|
1429
|
+
ownerFilePath: metadata.ownerFilePath
|
|
1507
1430
|
}
|
|
1431
|
+
compatibility = {testArgs, testData}
|
|
1432
|
+
this._testCompatibility.set(test, compatibility)
|
|
1508
1433
|
}
|
|
1509
1434
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
const {anyTestsFocussed} = this.analyzeTests(subTest)
|
|
1435
|
+
return compatibility
|
|
1436
|
+
}
|
|
1513
1437
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1438
|
+
/**
|
|
1439
|
+
* Injects framework collaborators into stable compatibility data once.
|
|
1440
|
+
* @param {PackageTestDeclaration} test - Package test declaration.
|
|
1441
|
+
* @returns {Promise<{testArgs: TestArgs, testData: TestData}>} - Injected compatibility data.
|
|
1442
|
+
*/
|
|
1443
|
+
async testCompatibility(test) {
|
|
1444
|
+
const compatibility = this.testData(test)
|
|
1517
1445
|
|
|
1518
|
-
|
|
1446
|
+
if (!this._injectedTests.has(test)) {
|
|
1447
|
+
await this._testArguments.inject(compatibility.testArgs)
|
|
1448
|
+
this._injectedTests.add(test)
|
|
1519
1449
|
}
|
|
1520
1450
|
|
|
1521
|
-
return
|
|
1451
|
+
return compatibility
|
|
1522
1452
|
}
|
|
1523
1453
|
|
|
1524
1454
|
/**
|
|
1525
|
-
*
|
|
1526
|
-
* @param {
|
|
1527
|
-
* @param {
|
|
1528
|
-
* @param {
|
|
1529
|
-
* @
|
|
1530
|
-
* @returns {Promise<void>} - Resolves after every hook runs.
|
|
1455
|
+
* Records a raw framework attempt outcome.
|
|
1456
|
+
* @param {PackageTestDeclaration} test - Package test declaration.
|
|
1457
|
+
* @param {number} attemptNumber - One-based attempt number.
|
|
1458
|
+
* @param {{abortRemainingTests: boolean, error: ReturnType<typeof JSON.parse>, failed: boolean}} outcome - Raw attempt outcome.
|
|
1459
|
+
* @returns {void}
|
|
1531
1460
|
*/
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1461
|
+
recordAttemptOutcome(test, attemptNumber, outcome) {
|
|
1462
|
+
const outcomes = this._attemptOutcomes.get(test) || new Map()
|
|
1463
|
+
outcomes.set(attemptNumber, outcome)
|
|
1464
|
+
this._attemptOutcomes.set(test, outcomes)
|
|
1465
|
+
if (outcome.abortRemainingTests) this._abortRemainingTests = true
|
|
1466
|
+
}
|
|
1535
1467
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
}, async () => {
|
|
1544
|
-
await afterEachData.callback({configuration: this.getConfiguration(), testArgs, testData})
|
|
1545
|
-
})
|
|
1546
|
-
} catch (error) {
|
|
1547
|
-
afterEachErrors.push(error)
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Gets a raw framework attempt outcome.
|
|
1470
|
+
* @param {PackageTestDeclaration} test - Package test declaration.
|
|
1471
|
+
* @param {number} attemptNumber - One-based attempt number.
|
|
1472
|
+
* @returns {{abortRemainingTests: boolean, error: ReturnType<typeof JSON.parse>, failed: boolean} | undefined} - Raw attempt outcome.
|
|
1473
|
+
*/
|
|
1474
|
+
attemptOutcome(test, attemptNumber) { return this._attemptOutcomes.get(test)?.get(attemptNumber) }
|
|
1550
1475
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1476
|
+
/**
|
|
1477
|
+
* Records a raw suite-hook failure.
|
|
1478
|
+
* @param {object} failure - Suite-hook failure.
|
|
1479
|
+
* @param {PackageSuiteDeclaration} failure.suite - Owning package suite.
|
|
1480
|
+
* @param {"beforeAll" | "afterAll"} failure.phase - Hook phase.
|
|
1481
|
+
* @param {ReturnType<typeof JSON.parse>} failure.error - Raw hook failure.
|
|
1482
|
+
* @returns {void}
|
|
1483
|
+
*/
|
|
1484
|
+
recordSuiteHookFailure(failure) { this._suiteHookFailures.push(failure) }
|
|
1556
1485
|
|
|
1557
1486
|
/**
|
|
1558
|
-
*
|
|
1559
|
-
* @param {
|
|
1560
|
-
* @
|
|
1561
|
-
* @param {Array<AfterBeforeEachCallbackObjectType>} args.beforeEaches - Before eaches.
|
|
1562
|
-
* @param {TestsArgument} args.tests - Tests.
|
|
1563
|
-
* @param {string[]} args.descriptions - Descriptions.
|
|
1564
|
-
* @param {number} args.indentLevel - Indent level.
|
|
1565
|
-
* @param {boolean} [args.lineMatchedInScope] - Whether line matched in scope.
|
|
1566
|
-
* @param {string} [args.parentProfileScopeId] - Parent profile scope.
|
|
1567
|
-
* @returns {Promise<void>} - Resolves when complete.
|
|
1487
|
+
* Gets the raw ancestor setup failure for a package test.
|
|
1488
|
+
* @param {PackageTestDeclaration} test - Package test declaration.
|
|
1489
|
+
* @returns {ReturnType<typeof JSON.parse>} - Raw setup failure.
|
|
1568
1490
|
*/
|
|
1569
|
-
|
|
1570
|
-
const
|
|
1491
|
+
setupFailureFor(test) {
|
|
1492
|
+
const suites = this.testMetadata(test).suites
|
|
1493
|
+
return this._suiteHookFailures.find((failure) => failure.phase === "beforeAll" && suites.includes(failure.suite))?.error
|
|
1494
|
+
}
|
|
1571
1495
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
parentId: parentProfileScopeId
|
|
1581
|
-
})
|
|
1582
|
-
const ownAfterEaches = [...this.profileHookEntries(tests.afterEaches, profileScopeId, scopeOwnerFilePath)].reverse()
|
|
1583
|
-
const ownBeforeEaches = this.profileHookEntries(tests.beforeEaches, profileScopeId, scopeOwnerFilePath)
|
|
1584
|
-
const newAfterEaches = [...ownAfterEaches, ...afterEaches]
|
|
1585
|
-
const newBeforeEaches = [...beforeEaches, ...ownBeforeEaches]
|
|
1586
|
-
const scopeLineMatch = lineMatchedInScope || this.matchesLineFilter(tests)
|
|
1587
|
-
const shouldRunAnyTests = this.hasRunnableTests(tests, descriptions, scopeLineMatch)
|
|
1496
|
+
/**
|
|
1497
|
+
* Finds the next incomplete declaration with a package full name.
|
|
1498
|
+
* @param {string} fullName - Package full name.
|
|
1499
|
+
* @returns {PackageTestDeclaration | undefined} - Next matching declaration.
|
|
1500
|
+
*/
|
|
1501
|
+
findTestDeclaration(fullName) {
|
|
1502
|
+
return this._testsByFullName.get(fullName)?.find((test) => !this._completedTests.has(test))
|
|
1503
|
+
}
|
|
1588
1504
|
|
|
1589
|
-
|
|
1505
|
+
/**
|
|
1506
|
+
* Marks a package declaration complete.
|
|
1507
|
+
* @param {PackageTestDeclaration} test - Completed declaration.
|
|
1508
|
+
* @returns {void}
|
|
1509
|
+
*/
|
|
1510
|
+
completeTestDeclaration(test) { this._completedTests.add(test) }
|
|
1590
1511
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1512
|
+
/**
|
|
1513
|
+
* Gets the effective package retry count.
|
|
1514
|
+
* @param {PackageTestDeclaration} test - Package test declaration.
|
|
1515
|
+
* @returns {number} - Effective retry count.
|
|
1516
|
+
*/
|
|
1517
|
+
retryCount(test) {
|
|
1518
|
+
const value = test.options.retries ?? test.options.retry ?? this.getTestContext().config.retries
|
|
1519
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0
|
|
1520
|
+
}
|
|
1596
1521
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1522
|
+
/**
|
|
1523
|
+
* Normalizes retry inputs for the package execution boundary while retaining
|
|
1524
|
+
* the declarations' original public options after the run.
|
|
1525
|
+
* @returns {() => void} - Restores original declaration options.
|
|
1526
|
+
*/
|
|
1527
|
+
normalizePackageRetriesForExecution() {
|
|
1528
|
+
/** @type {PackageRetryOptionRestoration[]} */
|
|
1529
|
+
const restorations = []
|
|
1530
|
+
/**
|
|
1531
|
+
* Normalizes declarations in one suite.
|
|
1532
|
+
* @param {PackageSuiteDeclaration} suite - Suite whose tests are normalized.
|
|
1533
|
+
*/
|
|
1534
|
+
const visit = (suite) => {
|
|
1535
|
+
for (const test of suite.tests) {
|
|
1536
|
+
// Capture compatibility arguments before temporarily adapting package
|
|
1537
|
+
// execution options so callbacks retain their declared values/identity.
|
|
1538
|
+
this.testData(test)
|
|
1539
|
+
restorations.push({
|
|
1540
|
+
hadRetries: Object.hasOwn(test.options, "retries"),
|
|
1541
|
+
options: test.options,
|
|
1542
|
+
retries: test.options.retries
|
|
1608
1543
|
})
|
|
1544
|
+
test.options.retries = this.retryCount(test)
|
|
1609
1545
|
}
|
|
1610
1546
|
|
|
1611
|
-
for (const
|
|
1612
|
-
|
|
1613
|
-
const testArgs = /** @type {TestArgs} */ (Object.assign({}, testData.args))
|
|
1614
|
-
const includeByLine = scopeLineMatch || this.matchesLineFilter(testData)
|
|
1615
|
-
|
|
1616
|
-
if (this._onlyFocussed && !testArgs.focus) continue
|
|
1617
|
-
if (this.shouldSkipTest(testArgs, testData, testDescription, descriptions, includeByLine)) continue
|
|
1618
|
-
|
|
1619
|
-
if (testArgs.type == "model" || testArgs.type == "request") {
|
|
1620
|
-
testArgs.application = await this.application()
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
if (testArgs.type == "request") {
|
|
1624
|
-
testArgs.client = await this.requestClient()
|
|
1625
|
-
}
|
|
1547
|
+
for (const childSuite of suite.suites) visit(childSuite)
|
|
1548
|
+
}
|
|
1626
1549
|
|
|
1627
|
-
|
|
1628
|
-
? Math.max(0, Math.floor(testArgs.retry))
|
|
1629
|
-
: 0
|
|
1630
|
-
const configTimeoutSeconds = typeof testConfig.defaultTimeoutSeconds === "number" ? testConfig.defaultTimeoutSeconds : undefined
|
|
1631
|
-
const timeoutSeconds = typeof testArgs.timeoutSeconds === "number" ? testArgs.timeoutSeconds : configTimeoutSeconds
|
|
1632
|
-
const useTimeout = typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0
|
|
1633
|
-
const timeoutMs = useTimeout ? timeoutSeconds * 1000 : undefined
|
|
1634
|
-
let retriesUsed = 0
|
|
1635
|
-
let attemptNumber = 1
|
|
1636
|
-
/**
|
|
1637
|
-
* Attempt console outputs.
|
|
1638
|
-
* @type {AttemptConsoleOutput[]} */
|
|
1639
|
-
const attemptConsoleOutputs = []
|
|
1640
|
-
|
|
1641
|
-
console.log(`${leftPadding}it ${testDescription}`)
|
|
1642
|
-
|
|
1643
|
-
const testStartMs = Date.now()
|
|
1644
|
-
|
|
1645
|
-
while (true) {
|
|
1646
|
-
let shouldRetry = false
|
|
1647
|
-
/**
|
|
1648
|
-
* Defines caughtError.
|
|
1649
|
-
* @type {ReturnType<typeof JSON.parse>} */
|
|
1650
|
-
let caughtError
|
|
1651
|
-
/**
|
|
1652
|
-
* Defines failedError.
|
|
1653
|
-
* @type {ReturnType<typeof JSON.parse>} */
|
|
1654
|
-
let failedError
|
|
1655
|
-
/**
|
|
1656
|
-
* Defines lastError.
|
|
1657
|
-
* @type {ReturnType<typeof JSON.parse>} */
|
|
1658
|
-
let lastError
|
|
1659
|
-
let willRetry = false
|
|
1660
|
-
/**
|
|
1661
|
-
* The per-test lifecycle promise, hoisted so the timeout branch can
|
|
1662
|
-
* still wait for it to settle after runWithTimeout has abandoned it.
|
|
1663
|
-
* @type {Promise<ReturnType<typeof JSON.parse>> | undefined} */
|
|
1664
|
-
let testLifecycle
|
|
1665
|
-
/** @type {{pool: import("../database/pool/base.js").default, registration: import("../database/pool/base.js").TestSharedConnectionRegistration}[]} */
|
|
1666
|
-
let testSharedConnectionRegistrations = []
|
|
1667
|
-
let testSharedConnectionsActive = false
|
|
1668
|
-
/** @type {SharedTransactionBrokerRegistration | undefined} */
|
|
1669
|
-
let sharedTransactionBrokerRegistration
|
|
1670
|
-
/** @type {SharedTransactionBrokerRegistration | undefined} */
|
|
1671
|
-
let sharedTransactionBrokerPreparation
|
|
1672
|
-
/** @type {TransactionalTenantRegistration[]} */
|
|
1673
|
-
const transactionalTenantRegistrations = []
|
|
1674
|
-
/** @type {BrowserDummyConnectionRegistration[]} */
|
|
1675
|
-
const browserDummyConnectionRegistrations = []
|
|
1676
|
-
const testDatabaseAccessScope = {revoked: false}
|
|
1677
|
-
/** @type {Set<Error>} */
|
|
1678
|
-
const recordedTimeoutCleanupErrors = new Set()
|
|
1679
|
-
testArgs.registerTransactionalTenant = async (args) => {
|
|
1680
|
-
await this.registerTransactionalTenant(args, transactionalTenantRegistrations)
|
|
1681
|
-
}
|
|
1682
|
-
const stopConsoleCapture = this.startConsoleCapture({
|
|
1683
|
-
passthrough: testConfig.consoleOutput === "live"
|
|
1684
|
-
})
|
|
1685
|
-
const profiler = this._profiler
|
|
1686
|
-
const profileAttempt = profiler?.startAttempt({
|
|
1687
|
-
descriptions,
|
|
1688
|
-
attemptNumber,
|
|
1689
|
-
testData,
|
|
1690
|
-
testDescription
|
|
1691
|
-
})
|
|
1692
|
-
let attemptTimedOut = false
|
|
1550
|
+
for (const suite of this.getTestContext().registry.suites) visit(suite)
|
|
1693
1551
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
const useSharedTestConnections = useTransaction || testArgs.type == "request"
|
|
1702
|
-
const useTestConnections = useSharedTestConnections || shouldTruncate
|
|
1703
|
-
const runTestAttempt = async () => {
|
|
1704
|
-
// Register dynamic candidates before hooks so transaction state changes
|
|
1705
|
-
// made during a hook are immediately visible to any in-process work.
|
|
1706
|
-
// Prepare transaction sharing before hooks so long-lived services cannot
|
|
1707
|
-
// use the shared connection while its coordinator is still missing.
|
|
1708
|
-
if (useSharedTestConnections) {
|
|
1709
|
-
testSharedConnectionRegistrations = this.activateTestSharedConnections()
|
|
1710
|
-
testSharedConnectionsActive = true
|
|
1711
|
-
}
|
|
1712
|
-
/** @type {unknown[]} */
|
|
1713
|
-
const lifecycleErrors = []
|
|
1714
|
-
let runCleanupHooks = false
|
|
1715
|
-
|
|
1716
|
-
try {
|
|
1717
|
-
if (useSharedTestConnections) {
|
|
1718
|
-
sharedTransactionBrokerPreparation = await this.prepareSharedTransactionBroker()
|
|
1719
|
-
}
|
|
1720
|
-
runCleanupHooks = true
|
|
1721
|
-
|
|
1722
|
-
clearDeliveries()
|
|
1723
|
-
for (const beforeEachData of newBeforeEaches) {
|
|
1724
|
-
await this.runProfileSpan({
|
|
1725
|
-
phase: "beforeEach",
|
|
1726
|
-
declarationIndex: beforeEachData.declarationIndex,
|
|
1727
|
-
declarationScopeId: beforeEachData.declarationScopeId,
|
|
1728
|
-
filePath: beforeEachData.ownerFilePath
|
|
1729
|
-
}, async () => {
|
|
1730
|
-
await beforeEachData.callback({configuration: this.getConfiguration(), testArgs, testData})
|
|
1731
|
-
})
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
if (useSharedTestConnections) {
|
|
1735
|
-
const activeSharedTransactionConnections = this.sharedTransactionConnections({transactionsOnly: true})
|
|
1736
|
-
if (sharedTransactionBrokerPreparation && !this.sharedTransactionBrokerMatchesConnections(sharedTransactionBrokerPreparation, activeSharedTransactionConnections)) {
|
|
1737
|
-
this.clearTestSharedConnections(testSharedConnectionRegistrations)
|
|
1738
|
-
testSharedConnectionRegistrations = []
|
|
1739
|
-
testSharedConnectionsActive = false
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
sharedTransactionBrokerRegistration = await this.startSharedTransactionBroker(sharedTransactionBrokerPreparation, activeSharedTransactionConnections)
|
|
1743
|
-
sharedTransactionBrokerPreparation = undefined
|
|
1744
|
-
if (sharedTransactionBrokerRegistration && !testSharedConnectionsActive) {
|
|
1745
|
-
testSharedConnectionRegistrations = this.activateTestSharedConnections()
|
|
1746
|
-
testSharedConnectionsActive = true
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
// Record which test is running so an async crash (an unhandled
|
|
1751
|
-
// rejection detached from any await) that fires during or shortly
|
|
1752
|
-
// after this test can be attributed to it in run()'s handler.
|
|
1753
|
-
this._lastTestContext = {
|
|
1754
|
-
fullDescription: this.buildFullDescription(descriptions, testDescription),
|
|
1755
|
-
filePath: testData.filePath ?? "<unknown>",
|
|
1756
|
-
line: testData.line ?? 0
|
|
1757
|
-
}
|
|
1758
|
-
await this.runProfileSpan({phase: "test body", filePath: testData.ownerFilePath ?? testData.filePath}, async () => {
|
|
1759
|
-
await testData.function(testArgs)
|
|
1760
|
-
})
|
|
1761
|
-
} catch (error) {
|
|
1762
|
-
lifecycleErrors.push(error)
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
if (runCleanupHooks) {
|
|
1766
|
-
try {
|
|
1767
|
-
// Framework-owned post-commit broadcasts are intentionally
|
|
1768
|
-
// detached; drain them before test cleanup so their DB
|
|
1769
|
-
// checkouts cannot leak into the next test's lifecycle.
|
|
1770
|
-
await this.getConfiguration().awaitPendingBroadcasts()
|
|
1771
|
-
} catch (error) {
|
|
1772
|
-
lifecycleErrors.push(error)
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
try {
|
|
1776
|
-
if (testSharedConnectionsActive) {
|
|
1777
|
-
this.clearTestSharedConnections(testSharedConnectionRegistrations)
|
|
1778
|
-
testSharedConnectionRegistrations = []
|
|
1779
|
-
testSharedConnectionsActive = false
|
|
1780
|
-
}
|
|
1781
|
-
} catch (error) {
|
|
1782
|
-
lifecycleErrors.push(error)
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
try {
|
|
1786
|
-
await this.stopSharedTransactionBroker(sharedTransactionBrokerRegistration || sharedTransactionBrokerPreparation)
|
|
1787
|
-
sharedTransactionBrokerRegistration = undefined
|
|
1788
|
-
sharedTransactionBrokerPreparation = undefined
|
|
1789
|
-
} catch (error) {
|
|
1790
|
-
lifecycleErrors.push(error)
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
try {
|
|
1794
|
-
await this.runAfterEaches({afterEaches: newAfterEaches, testArgs, testData})
|
|
1795
|
-
} catch (error) {
|
|
1796
|
-
lifecycleErrors.push(error)
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
try {
|
|
1800
|
-
await this.cleanupTransactionalTenants(transactionalTenantRegistrations)
|
|
1801
|
-
} catch (error) {
|
|
1802
|
-
lifecycleErrors.push(error)
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
if (testSharedConnectionsActive) {
|
|
1807
|
-
try {
|
|
1808
|
-
this.clearTestSharedConnections(testSharedConnectionRegistrations)
|
|
1809
|
-
} catch (error) {
|
|
1810
|
-
lifecycleErrors.push(error)
|
|
1811
|
-
}
|
|
1812
|
-
testSharedConnectionsActive = false
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
if (lifecycleErrors.length == 1) throw lifecycleErrors[0]
|
|
1816
|
-
if (lifecycleErrors.length > 1) {
|
|
1817
|
-
throw new AggregateError(lifecycleErrors, "Test lifecycle and cleanup failed", {cause: lifecycleErrors[0]})
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
if (useTestConnections) {
|
|
1822
|
-
// Database cleaning requires one connection for beforeEach, the test
|
|
1823
|
-
// body and afterEach; only transactions and requests share it dynamically.
|
|
1824
|
-
await this.getConfiguration().ensureConnections({name: `Test: ${testDescription}`}, runTestAttempt)
|
|
1825
|
-
} else {
|
|
1826
|
-
await runTestAttempt()
|
|
1827
|
-
}
|
|
1828
|
-
}, browserDummyConnectionRegistrations)
|
|
1829
|
-
const lifecycleCallback = async () => await this.getConfiguration().runWithTestDatabaseAccessScope(testDatabaseAccessScope, runLifecycleCallback)
|
|
1830
|
-
testLifecycle = profileAttempt && profiler
|
|
1831
|
-
? profiler.runAttempt(profileAttempt, lifecycleCallback)
|
|
1832
|
-
: lifecycleCallback()
|
|
1833
|
-
|
|
1834
|
-
// Time out the ENTIRE lifecycle, not just the test body. A hang in any
|
|
1835
|
-
// phase — a connection checkout that never resolves, a beforeEach/afterEach
|
|
1836
|
-
// waiting on a lock, or dummy server startup — would otherwise stall the
|
|
1837
|
-
// whole run indefinitely (until CI kills the build) instead of failing the
|
|
1838
|
-
// single offending test.
|
|
1839
|
-
if (useTimeout && timeoutMs !== undefined) {
|
|
1840
|
-
await runWithTimeout(testLifecycle, timeoutMs, testDescription)
|
|
1841
|
-
} else {
|
|
1842
|
-
await testLifecycle
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
// A test is successful only after its complete lifecycle settles.
|
|
1846
|
-
// Cleanup failures and timed-out detached work must not overlap the
|
|
1847
|
-
// final successful and failed counters used for executed-test totals.
|
|
1848
|
-
this._successfulTests++
|
|
1849
|
-
} catch (error) {
|
|
1850
|
-
caughtError = error
|
|
1851
|
-
lastError = error
|
|
1852
|
-
|
|
1853
|
-
// A timeout REJECTS while the lifecycle keeps running detached on the
|
|
1854
|
-
// shared per-suite connection — including its afterEach database
|
|
1855
|
-
// cleanup (e.g. transaction rollback). If the next test starts before
|
|
1856
|
-
// that rollback runs, its own startTransaction() implicitly COMMITS
|
|
1857
|
-
// the timed-out test's rows on the shared connection, poisoning every
|
|
1858
|
-
// later test in the shard (duplicate-key / foreign-key cascades from
|
|
1859
|
-
// leaked fixtures). Wait — bounded — for the abandoned lifecycle to
|
|
1860
|
-
// settle so its cleanup lands first. If it remains active after the
|
|
1861
|
-
// bounded grace, quarantine its browser connections and stop running
|
|
1862
|
-
// tests rather than sharing unsafe state.
|
|
1863
|
-
const timedOut = Boolean(/** @type {TestTimeoutError} */ (error)?.velociousTestTimeout)
|
|
1864
|
-
attemptTimedOut = timedOut
|
|
1865
|
-
|
|
1866
|
-
if (timedOut && testLifecycle) {
|
|
1867
|
-
const emergencyCleanupErrors = []
|
|
1868
|
-
|
|
1869
|
-
if (profileAttempt && profiler) profiler.finishAttempt(profileAttempt, "timed-out")
|
|
1870
|
-
const lifecycleOutcome = await awaitSettledOrGrace(testLifecycle, timeoutMs ?? 60000)
|
|
1871
|
-
|
|
1872
|
-
if (lifecycleOutcome.settled && lifecycleOutcome.status === "rejected") {
|
|
1873
|
-
emergencyCleanupErrors.push(lifecycleOutcome.reason)
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
// If the abandoned lifecycle never settled within the grace, its
|
|
1877
|
-
// cleanup has not completed. Quarantine browser-owned connections
|
|
1878
|
-
// before any scope cleanup can race the abandoned callback.
|
|
1879
|
-
if (!lifecycleOutcome.settled) {
|
|
1880
|
-
testDatabaseAccessScope.revoked = true
|
|
1881
|
-
void testLifecycle.catch((cleanupError) => {
|
|
1882
|
-
if (isTestDatabaseAccessRevocation(cleanupError)) return
|
|
1883
|
-
this.recordTimeoutCleanupFailure(cleanupError, "test lifecycle", recordedTimeoutCleanupErrors)
|
|
1884
|
-
})
|
|
1885
|
-
const quarantine = this.quarantineBrowserDummyConnections(browserDummyConnectionRegistrations)
|
|
1886
|
-
const quarantineOutcome = await awaitSettledOrGrace(quarantine, timeoutMs ?? 60000)
|
|
1887
|
-
const usesBrowserTransactions = testArgs.databaseCleaning?.transaction === true
|
|
1888
|
-
const usesBrowserTruncation = testArgs.databaseCleaning?.truncate ?? !usesBrowserTransactions
|
|
1889
|
-
|
|
1890
|
-
this._abortRemainingTests = this.isBrowserTestMode()
|
|
1891
|
-
&& this.hasTag(testArgs, "dummy")
|
|
1892
|
-
&& (usesBrowserTransactions || usesBrowserTruncation)
|
|
1893
|
-
|
|
1894
|
-
if (quarantineOutcome.settled && quarantineOutcome.status === "rejected") {
|
|
1895
|
-
emergencyCleanupErrors.push(quarantineOutcome.reason)
|
|
1896
|
-
} else if (!quarantineOutcome.settled) {
|
|
1897
|
-
void quarantine.catch((cleanupError) => {
|
|
1898
|
-
this.recordTimeoutCleanupFailure(cleanupError, "browser dummy connection quarantine", recordedTimeoutCleanupErrors)
|
|
1899
|
-
})
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
try {
|
|
1904
|
-
if (testSharedConnectionsActive) {
|
|
1905
|
-
this.clearTestSharedConnections(testSharedConnectionRegistrations)
|
|
1906
|
-
testSharedConnectionRegistrations = []
|
|
1907
|
-
testSharedConnectionsActive = false
|
|
1908
|
-
}
|
|
1909
|
-
} catch (cleanupError) {
|
|
1910
|
-
emergencyCleanupErrors.push(cleanupError)
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
const brokerCleanup = this.stopSharedTransactionBroker(sharedTransactionBrokerRegistration || sharedTransactionBrokerPreparation)
|
|
1914
|
-
const brokerCleanupOutcome = await awaitSettledOrGrace(brokerCleanup, timeoutMs ?? 60000)
|
|
1915
|
-
|
|
1916
|
-
if (brokerCleanupOutcome.settled && brokerCleanupOutcome.status === "rejected") {
|
|
1917
|
-
emergencyCleanupErrors.push(brokerCleanupOutcome.reason)
|
|
1918
|
-
} else if (!brokerCleanupOutcome.settled) {
|
|
1919
|
-
void brokerCleanup.catch((cleanupError) => {
|
|
1920
|
-
this.recordTimeoutCleanupFailure(cleanupError, "shared transaction broker", recordedTimeoutCleanupErrors)
|
|
1921
|
-
})
|
|
1922
|
-
}
|
|
1923
|
-
sharedTransactionBrokerRegistration = undefined
|
|
1924
|
-
sharedTransactionBrokerPreparation = undefined
|
|
1925
|
-
const emergencyCleanup = this.cleanupTransactionalTenants(transactionalTenantRegistrations, {discard: true})
|
|
1926
|
-
const emergencyCleanupOutcome = await awaitSettledOrGrace(emergencyCleanup, timeoutMs ?? 60000)
|
|
1927
|
-
|
|
1928
|
-
if (emergencyCleanupOutcome.settled && emergencyCleanupOutcome.status === "rejected") {
|
|
1929
|
-
emergencyCleanupErrors.push(emergencyCleanupOutcome.reason)
|
|
1930
|
-
} else if (!emergencyCleanupOutcome.settled) {
|
|
1931
|
-
// The timed-out attempt must not block the runner indefinitely, but a
|
|
1932
|
-
// later rollback/discard failure still becomes a visible test failure.
|
|
1933
|
-
void emergencyCleanup.catch((cleanupError) => {
|
|
1934
|
-
this.recordTimeoutCleanupFailure(cleanupError, "transactional tenant", recordedTimeoutCleanupErrors)
|
|
1935
|
-
})
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
if (emergencyCleanupErrors.length > 0) {
|
|
1939
|
-
caughtError = new AggregateError(
|
|
1940
|
-
[caughtError, ...emergencyCleanupErrors],
|
|
1941
|
-
"Test timeout and emergency cleanup failed",
|
|
1942
|
-
{cause: caughtError}
|
|
1943
|
-
)
|
|
1944
|
-
lastError = caughtError
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
if (browserDummyConnectionRegistrations.some((registration) => registration.quarantined)) {
|
|
1949
|
-
testDatabaseAccessScope.revoked = true
|
|
1950
|
-
this._abortRemainingTests = true
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
willRetry = !this._abortRemainingTests && retriesUsed < retryCount
|
|
1954
|
-
|
|
1955
|
-
if (willRetry) {
|
|
1956
|
-
retriesUsed++
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
if (willRetry) {
|
|
1960
|
-
shouldRetry = true
|
|
1961
|
-
} else {
|
|
1962
|
-
failedError = caughtError
|
|
1963
|
-
}
|
|
1964
|
-
} finally {
|
|
1965
|
-
testDatabaseAccessScope.revoked = true
|
|
1966
|
-
const consoleOutput = stopConsoleCapture()
|
|
1967
|
-
|
|
1968
|
-
if (profileAttempt && profiler) {
|
|
1969
|
-
profiler.finishAttempt(profileAttempt, caughtError === undefined
|
|
1970
|
-
? "passed"
|
|
1971
|
-
: (attemptTimedOut ? "timed-out" : "failed"))
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
if (consoleOutput) {
|
|
1975
|
-
attemptConsoleOutputs.push({attemptNumber, output: consoleOutput})
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1552
|
+
return () => {
|
|
1553
|
+
for (const restoration of restorations) {
|
|
1554
|
+
if (restoration.hadRetries) restoration.options.retries = restoration.retries
|
|
1555
|
+
else delete restoration.options.retries
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1978
1559
|
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
nextAttempt: willRetry ? attemptNumber + 1 : undefined,
|
|
1986
|
-
retriesUsed,
|
|
1987
|
-
retryCount,
|
|
1988
|
-
testArgs,
|
|
1989
|
-
testData,
|
|
1990
|
-
testDescription,
|
|
1991
|
-
testRunner: this,
|
|
1992
|
-
willRetry
|
|
1993
|
-
})
|
|
1994
|
-
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Records one completed test duration.
|
|
1562
|
+
* @param {{durationMs: number, filePath: string, fullDescription: string, line: number}} duration - Completed test duration.
|
|
1563
|
+
* @returns {void}
|
|
1564
|
+
*/
|
|
1565
|
+
recordTestDuration(duration) { this._testDurations.push(duration) }
|
|
1995
1566
|
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
await this.emitEvent("testRetrying", {
|
|
1999
|
-
configuration: this.getConfiguration(),
|
|
2000
|
-
descriptions,
|
|
2001
|
-
error: lastError,
|
|
2002
|
-
nextAttempt: attemptNumber + 1,
|
|
2003
|
-
retriesUsed,
|
|
2004
|
-
retryCount,
|
|
2005
|
-
testArgs,
|
|
2006
|
-
testData,
|
|
2007
|
-
testDescription,
|
|
2008
|
-
testRunner: this
|
|
2009
|
-
})
|
|
2010
|
-
}
|
|
1567
|
+
/** Records one successful package result. */
|
|
1568
|
+
recordSuccessfulTest() { this._successfulTests++ }
|
|
2011
1569
|
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
const consoleOutput = this.buildConsoleOutput(attemptConsoleOutputs)
|
|
2033
|
-
|
|
2034
|
-
if (failedError instanceof Error) {
|
|
2035
|
-
console.error(picocolors.red(`${leftPadding} Test failed: ${failedError.message}`))
|
|
2036
|
-
addTrackedStackToError(failedError)
|
|
2037
|
-
|
|
2038
|
-
const backtraceCleaner = new BacktraceCleaner(failedError)
|
|
2039
|
-
const cleanedStack = backtraceCleaner.getCleanedStack()
|
|
2040
|
-
const stackLines = cleanedStack?.split("\n")
|
|
2041
|
-
|
|
2042
|
-
if (stackLines) {
|
|
2043
|
-
for (const stackLine of stackLines) {
|
|
2044
|
-
console.error(picocolors.red(`${leftPadding} ${stackLine}`))
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
} else {
|
|
2048
|
-
console.error(picocolors.red(`${leftPadding} Test failed with a ${typeof failedError}: ${String(failedError)}`))
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
this.printFailedConsoleOutput({consoleOutput, leftPadding})
|
|
2052
|
-
this._failedTests++
|
|
2053
|
-
this._failedTestDetails.push({
|
|
2054
|
-
fullDescription: this.buildFullDescription(descriptions, testDescription),
|
|
2055
|
-
filePath: testData.filePath,
|
|
2056
|
-
line: testData.line,
|
|
2057
|
-
error: failedError,
|
|
2058
|
-
consoleOutput: consoleOutput || undefined
|
|
2059
|
-
})
|
|
2060
|
-
|
|
2061
|
-
await this.emitEvent("testFailed", {
|
|
2062
|
-
configuration: this.getConfiguration(),
|
|
2063
|
-
descriptions,
|
|
2064
|
-
error: failedError,
|
|
2065
|
-
testArgs,
|
|
2066
|
-
testData,
|
|
2067
|
-
testDescription,
|
|
2068
|
-
testRunner: this
|
|
2069
|
-
})
|
|
2070
|
-
|
|
2071
|
-
this.printRerunCommand({descriptions, testDescription, testData, leftPadding})
|
|
2072
|
-
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Records one failed package test in the legacy result projection.
|
|
1572
|
+
* @param {object} args - Failed test metadata.
|
|
1573
|
+
* @param {string[]} args.descriptions - Parent descriptions.
|
|
1574
|
+
* @param {ReturnType<typeof JSON.parse>} args.error - Raw failure.
|
|
1575
|
+
* @param {string} args.consoleOutput - Captured console output.
|
|
1576
|
+
* @param {TestData} args.testData - Compatibility test data.
|
|
1577
|
+
* @param {string} args.testDescription - Test description.
|
|
1578
|
+
* @returns {void}
|
|
1579
|
+
*/
|
|
1580
|
+
recordFailedTest({descriptions, error, consoleOutput, testData, testDescription}) {
|
|
1581
|
+
this._failedTests++
|
|
1582
|
+
this._failedTestDetails.push({
|
|
1583
|
+
fullDescription: this.buildFullDescription(descriptions, testDescription),
|
|
1584
|
+
filePath: testData.filePath,
|
|
1585
|
+
line: testData.line,
|
|
1586
|
+
error,
|
|
1587
|
+
consoleOutput: consoleOutput || undefined
|
|
1588
|
+
})
|
|
1589
|
+
}
|
|
2073
1590
|
|
|
2074
|
-
|
|
2075
|
-
|
|
1591
|
+
/**
|
|
1592
|
+
* Stores the completed package result.
|
|
1593
|
+
* @param {import("@velocious/testing/runner").TestRunResult} result - Package result.
|
|
1594
|
+
* @returns {void}
|
|
1595
|
+
*/
|
|
1596
|
+
recordPackageResult(result) { this._packageResult = result }
|
|
2076
1597
|
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
1598
|
+
/**
|
|
1599
|
+
* Runs the package kernel with Velocious framework adapters.
|
|
1600
|
+
* @returns {Promise<void>} - Resolves after execution and teardown.
|
|
1601
|
+
*/
|
|
1602
|
+
async runPackageTests() {
|
|
1603
|
+
const environmentHandler = this.getConfiguration().getEnvironmentHandler()
|
|
1604
|
+
environmentHandler.installSharedTransactionCoordinatorOwnerStorage(this._sharedTransactionCoordinatorOwnerStorage)
|
|
1605
|
+
environmentHandler.installTestDatabaseAccessScopeStorage(this._testDatabaseAccessScopeStorage)
|
|
1606
|
+
this._packageRunner = new PackageTestRunner({
|
|
1607
|
+
context: this.getTestContext(),
|
|
1608
|
+
includeTags: this._includeTags,
|
|
1609
|
+
excludeTags: [...this.getExcludeTagSet(), ...(this.isBrowserTestMode() ? [] : ["browser-only"])],
|
|
1610
|
+
examples: this.getExamplePatterns(),
|
|
1611
|
+
lineFilters: this.getLineFilters(),
|
|
1612
|
+
includeTagMode: "any",
|
|
1613
|
+
focusedTestsBypassIncludeTags: true,
|
|
1614
|
+
omitEmptySuiteNames: true,
|
|
1615
|
+
attemptExecutorOwnsTimeout: true,
|
|
1616
|
+
attemptExecutor: (input) => this._attemptExecutor.execute(input),
|
|
1617
|
+
testArgumentResolver: (input) => this._testArguments.resolve(input),
|
|
1618
|
+
suiteHookExecutor: (input) => this._suiteHookExecutor.execute(input),
|
|
1619
|
+
reporter: this._runnerReporter
|
|
1620
|
+
})
|
|
1621
|
+
const failureStart = this._suiteHookFailures.length
|
|
1622
|
+
const restoreRetryOptions = this.normalizePackageRetriesForExecution()
|
|
1623
|
+
let result
|
|
2083
1624
|
|
|
2084
|
-
|
|
2085
|
-
|
|
1625
|
+
try {
|
|
1626
|
+
try {
|
|
1627
|
+
result = await this._packageRunner.run()
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
if (!(error instanceof AbortRemainingTestsError)) throw error
|
|
2086
1630
|
|
|
2087
|
-
|
|
2088
|
-
if (this.
|
|
2089
|
-
|
|
2090
|
-
const subTest = tests.subs[subDescription]
|
|
2091
|
-
const newDecriptions = descriptions.concat([subDescription])
|
|
2092
|
-
const childScopeLineMatch = scopeLineMatch || this.matchesLineFilter(subTest)
|
|
2093
|
-
|
|
2094
|
-
if (!this._onlyFocussed || subTest.anyTestsFocussed) {
|
|
2095
|
-
console.log(`${leftPadding}${subDescription}`)
|
|
2096
|
-
await this.runTests({
|
|
2097
|
-
afterEaches: newAfterEaches,
|
|
2098
|
-
beforeEaches: newBeforeEaches,
|
|
2099
|
-
tests: subTest,
|
|
2100
|
-
descriptions: newDecriptions,
|
|
2101
|
-
indentLevel: indentLevel + 1,
|
|
2102
|
-
lineMatchedInScope: childScopeLineMatch,
|
|
2103
|
-
parentProfileScopeId: profileScopeId
|
|
2104
|
-
})
|
|
2105
|
-
}
|
|
1631
|
+
const afterAll = this.afterAllOutcome(this._suiteHookFailures.slice(failureStart))
|
|
1632
|
+
if (afterAll.failed) this.recordTimeoutCleanupFailure(afterAll.error, "afterAll")
|
|
1633
|
+
return
|
|
2106
1634
|
}
|
|
2107
|
-
} catch (error) {
|
|
2108
|
-
scopeErrors.push(error)
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
try {
|
|
2112
|
-
await this.runAfterAllsForScope(scopeEntry)
|
|
2113
|
-
} catch (error) {
|
|
2114
|
-
scopeErrors.push(error)
|
|
2115
|
-
}
|
|
2116
|
-
const scopeIndex = this._activeAfterAllScopes.indexOf(scopeEntry)
|
|
2117
1635
|
|
|
2118
|
-
|
|
2119
|
-
this.
|
|
1636
|
+
this.recordPackageResult(result)
|
|
1637
|
+
this.throwAfterAllFailures(this._suiteHookFailures.slice(failureStart))
|
|
1638
|
+
} finally {
|
|
1639
|
+
restoreRetryOptions()
|
|
2120
1640
|
}
|
|
1641
|
+
}
|
|
2121
1642
|
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
1643
|
+
/**
|
|
1644
|
+
* Aggregates raw after-all failures without using error truthiness.
|
|
1645
|
+
* @param {Array<{phase: "beforeAll" | "afterAll", error: ReturnType<typeof JSON.parse>}>} failures - Hook failures.
|
|
1646
|
+
* @returns {{failed: false} | {failed: true, error: ReturnType<typeof JSON.parse>}} - Explicit afterAll outcome.
|
|
1647
|
+
*/
|
|
1648
|
+
afterAllOutcome(failures) {
|
|
1649
|
+
const afterAllErrors = failures.filter((failure) => failure.phase === "afterAll").map((failure) => failure.error)
|
|
2126
1650
|
|
|
2127
|
-
|
|
2128
|
-
|
|
1651
|
+
if (afterAllErrors.length === 0) return {failed: false}
|
|
1652
|
+
if (afterAllErrors.length === 1) return {failed: true, error: afterAllErrors[0]}
|
|
1653
|
+
return {
|
|
1654
|
+
failed: true,
|
|
1655
|
+
error: new AggregateError(afterAllErrors, "Multiple active afterAll scopes failed", {cause: afterAllErrors[0]})
|
|
2129
1656
|
}
|
|
2130
|
-
if (scopeErrors.length == 1) throw scopeErrors[0]
|
|
2131
|
-
if (scopeErrors.length > 1) throw new AggregateError(scopeErrors, "Test scope and afterAll cleanup failed", {cause: scopeErrors[0]})
|
|
2132
1657
|
}
|
|
2133
1658
|
|
|
2134
1659
|
/**
|
|
2135
|
-
*
|
|
2136
|
-
* @param {
|
|
2137
|
-
* @returns {
|
|
1660
|
+
* Throws one raw or aggregated after-all failure.
|
|
1661
|
+
* @param {Array<{phase: "beforeAll" | "afterAll", error: ReturnType<typeof JSON.parse>}>} failures - Hook failures.
|
|
1662
|
+
* @returns {void}
|
|
2138
1663
|
*/
|
|
2139
|
-
|
|
2140
|
-
|
|
1664
|
+
throwAfterAllFailures(failures) {
|
|
1665
|
+
const afterAll = this.afterAllOutcome(failures)
|
|
2141
1666
|
|
|
2142
|
-
|
|
1667
|
+
if (afterAll.failed) throw afterAll.error
|
|
1668
|
+
}
|
|
2143
1669
|
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
1670
|
+
/**
|
|
1671
|
+
* Compatibility helper for focused framework lifecycle specs. It converts an
|
|
1672
|
+
* explicit legacy fixture into isolated package declarations; the package
|
|
1673
|
+
* runner remains the sole execution engine.
|
|
1674
|
+
* @param {object} args - Legacy fixture arguments.
|
|
1675
|
+
* @param {TestsArgument} args.tests - Fixture tree.
|
|
1676
|
+
* @returns {Promise<void>} - Resolves after package execution.
|
|
1677
|
+
*/
|
|
1678
|
+
async runTests({tests}) {
|
|
1679
|
+
const context = createTestContext()
|
|
1680
|
+
const originalContext = this._context
|
|
1681
|
+
context.configureTests({
|
|
1682
|
+
consoleOutput: originalContext.config.consoleOutput,
|
|
1683
|
+
defaultTimeoutMs: originalContext.config.defaultTimeoutMs,
|
|
1684
|
+
excludeTags: originalContext.config.excludeTags,
|
|
1685
|
+
failedConsoleOutputMaxLines: originalContext.config.failedConsoleOutputMaxLines,
|
|
1686
|
+
retries: originalContext.config.retries
|
|
1687
|
+
})
|
|
1688
|
+
this._context = context
|
|
1689
|
+
this._testsCount = 0
|
|
1690
|
+
this._testCompatibility = new WeakMap()
|
|
1691
|
+
this._injectedTests = new WeakSet()
|
|
1692
|
+
this._completedTests = new WeakSet()
|
|
1693
|
+
this._testMetadata = new WeakMap()
|
|
1694
|
+
this._hookMetadata = new WeakMap()
|
|
1695
|
+
this._attemptOutcomes = new WeakMap()
|
|
1696
|
+
this._suiteHookFailures = []
|
|
1697
|
+
this._testsByFullName = new Map()
|
|
1698
|
+
this._legacyFixtureDataByFullName = new Map()
|
|
1699
|
+
context.setDeclarationLocator(() => this._legacyFixtureLocation)
|
|
1700
|
+
this.declareLegacyFixture(context, "", tests, [])
|
|
1701
|
+
this.analyzeDeclarations()
|
|
2152
1702
|
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
declarationIndex: afterAllData.declarationIndex,
|
|
2158
|
-
declarationScopeId: afterAllData.declarationScopeId,
|
|
2159
|
-
filePath: afterAllData.ownerFilePath
|
|
2160
|
-
}, async () => {
|
|
2161
|
-
await afterAllData.callback({configuration: this.getConfiguration()})
|
|
2162
|
-
})
|
|
2163
|
-
} catch (error) {
|
|
2164
|
-
afterAllErrors.push(error)
|
|
2165
|
-
}
|
|
1703
|
+
try {
|
|
1704
|
+
await this.runPackageTests()
|
|
1705
|
+
} finally {
|
|
1706
|
+
this._context = originalContext
|
|
2166
1707
|
}
|
|
1708
|
+
}
|
|
2167
1709
|
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
1710
|
+
/**
|
|
1711
|
+
* Declares an isolated legacy-shaped test fixture into a package context.
|
|
1712
|
+
* @param {PackageTestContext} context - Isolated package context.
|
|
1713
|
+
* @param {string} name - Suite name.
|
|
1714
|
+
* @param {TestsArgument} scope - Legacy fixture scope.
|
|
1715
|
+
* @param {string[]} descriptions - Ancestor descriptions.
|
|
1716
|
+
* @returns {void}
|
|
1717
|
+
*/
|
|
1718
|
+
declareLegacyFixture(context, name, scope, descriptions) {
|
|
1719
|
+
this._legacyFixtureLocation = {filePath: scope.filePath, line: scope.line}
|
|
1720
|
+
context.describe(name, scope.args || {}, () => {
|
|
1721
|
+
for (const hook of scope.beforeAlls || []) context.beforeAll(hook.callback)
|
|
1722
|
+
for (const hook of scope.beforeEaches || []) context.beforeEach(hook.callback)
|
|
1723
|
+
for (const hook of scope.afterEaches || []) context.afterEach(hook.callback)
|
|
1724
|
+
for (const hook of scope.afterAlls || []) context.afterAll(hook.callback)
|
|
1725
|
+
const nextDescriptions = name === "" ? descriptions : [...descriptions, name]
|
|
1726
|
+
for (const [testName, testData] of Object.entries(scope.tests || {})) {
|
|
1727
|
+
this._legacyFixtureLocation = {filePath: testData.filePath, line: testData.line}
|
|
1728
|
+
this._legacyFixtureDataByFullName?.set(this.buildFullDescription(nextDescriptions, testName), testData)
|
|
1729
|
+
context.it(testName, testData.args, testData.function)
|
|
1730
|
+
}
|
|
1731
|
+
for (const [suiteName, childScope] of Object.entries(scope.subs || {})) {
|
|
1732
|
+
this.declareLegacyFixture(context, suiteName, childScope, nextDescriptions)
|
|
1733
|
+
}
|
|
1734
|
+
})
|
|
2172
1735
|
}
|
|
2173
1736
|
|
|
2174
1737
|
/**
|
|
@@ -2178,11 +1741,7 @@ export default class TestRunner {
|
|
|
2178
1741
|
* @returns {Promise<void>} - Resolves when all listeners complete.
|
|
2179
1742
|
*/
|
|
2180
1743
|
async emitEvent(eventName, payload) {
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
for (const listener of listeners) {
|
|
2184
|
-
await listener(payload)
|
|
2185
|
-
}
|
|
1744
|
+
await this._runnerReporter.emitEvent(eventName, payload)
|
|
2186
1745
|
}
|
|
2187
1746
|
|
|
2188
1747
|
/**
|
|
@@ -2298,57 +1857,4 @@ export default class TestRunner {
|
|
|
2298
1857
|
}
|
|
2299
1858
|
}
|
|
2300
1859
|
|
|
2301
|
-
/**
|
|
2302
|
-
* Runs start console capture.
|
|
2303
|
-
* @param {object} [args] - Options object.
|
|
2304
|
-
* @param {boolean} [args.passthrough] - Whether to pass through to the original console.
|
|
2305
|
-
* @returns {() => string} - Stops the capture and returns captured text.
|
|
2306
|
-
*/
|
|
2307
|
-
startConsoleCapture({passthrough = false} = {}) {
|
|
2308
|
-
/**
|
|
2309
|
-
* Lines.
|
|
2310
|
-
* @type {string[]} */
|
|
2311
|
-
const lines = []
|
|
2312
|
-
/**
|
|
2313
|
-
* Console object.
|
|
2314
|
-
* @type {Record<ConsoleMethodName, (...args: Array<ReturnType<typeof JSON.parse>>) => void>} */
|
|
2315
|
-
const consoleObject = /** @type {Record<ConsoleMethodName, (...args: Array<ReturnType<typeof JSON.parse>>) => void>} */ (console)
|
|
2316
|
-
/**
|
|
2317
|
-
* Original console methods captured as direct references so stopping restores
|
|
2318
|
-
* the exact method that was installed at capture start.
|
|
2319
|
-
* @type {Record<ConsoleMethodName, (...args: Array<ReturnType<typeof JSON.parse>>) => void>} */
|
|
2320
|
-
const originalConsoleMethods = {
|
|
2321
|
-
debug: consoleObject.debug,
|
|
2322
|
-
error: consoleObject.error,
|
|
2323
|
-
info: consoleObject.info,
|
|
2324
|
-
log: consoleObject.log,
|
|
2325
|
-
warn: consoleObject.warn
|
|
2326
|
-
}
|
|
2327
|
-
let stopped = false
|
|
2328
|
-
let outputText = ""
|
|
2329
|
-
|
|
2330
|
-
for (const methodName of CAPTURED_CONSOLE_METHODS) {
|
|
2331
|
-
consoleObject[methodName] = (...args) => {
|
|
2332
|
-
lines.push(`[${new Date().toISOString()}] [${methodName}] ${format(...args)}`)
|
|
2333
|
-
|
|
2334
|
-
if (passthrough) {
|
|
2335
|
-
originalConsoleMethods[methodName].apply(consoleObject, args)
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
return () => {
|
|
2341
|
-
if (!stopped) {
|
|
2342
|
-
stopped = true
|
|
2343
|
-
|
|
2344
|
-
for (const methodName of CAPTURED_CONSOLE_METHODS) {
|
|
2345
|
-
consoleObject[methodName] = originalConsoleMethods[methodName]
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
outputText = lines.join("\n")
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
|
-
return outputText
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
1860
|
}
|