@taicode/common-server 4.0.0-beta.1 → 4.0.0-beta.10
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/output/app/index.d.ts +18 -15
- package/output/app/index.d.ts.map +1 -1
- package/output/app/index.js +330 -18
- package/output/auth/index.d.ts +73 -0
- package/output/auth/index.d.ts.map +1 -0
- package/output/auth/index.js +196 -0
- package/output/core/assemble.d.ts +64 -0
- package/output/core/assemble.d.ts.map +1 -0
- package/output/core/assemble.js +88 -0
- package/output/core/boot/context.d.ts +109 -0
- package/output/core/boot/context.d.ts.map +1 -0
- package/output/core/boot/context.js +145 -0
- package/output/core/boot/lifecycle.d.ts +206 -0
- package/output/core/boot/lifecycle.d.ts.map +1 -0
- package/output/core/boot/lifecycle.js +255 -0
- package/output/core/boot/shutdown.d.ts +34 -0
- package/output/core/boot/shutdown.d.ts.map +1 -0
- package/output/core/boot/shutdown.js +50 -0
- package/output/core/boot/signal.d.ts +13 -0
- package/output/core/boot/signal.d.ts.map +1 -0
- package/output/{app → core/boot}/signal.js +3 -2
- package/output/core/boot/steps.d.ts +138 -0
- package/output/core/boot/steps.d.ts.map +1 -0
- package/output/core/boot/steps.js +259 -0
- package/output/core/entries.d.ts +151 -0
- package/output/core/entries.d.ts.map +1 -0
- package/output/core/entries.js +242 -0
- package/output/core/entries.test.d.ts +2 -0
- package/output/core/entries.test.d.ts.map +1 -0
- package/output/core/entries.test.js +458 -0
- package/output/core/env.d.ts +83 -0
- package/output/core/env.d.ts.map +1 -0
- package/output/core/env.js +124 -0
- package/output/{app → core}/env.test.d.ts.map +1 -1
- package/output/core/env.test.js +105 -0
- package/output/core/modules/auth-provider/index.d.ts +10 -0
- package/output/core/modules/auth-provider/index.d.ts.map +1 -0
- package/output/core/modules/auth-provider/index.js +9 -0
- package/output/core/modules/auth-provider/types.d.ts +71 -0
- package/output/core/modules/auth-provider/types.d.ts.map +1 -0
- package/output/core/modules/context/index.d.ts +18 -0
- package/output/core/modules/context/index.d.ts.map +1 -0
- package/output/core/modules/context/index.js +23 -0
- package/output/core/modules/context/index.test.d.ts.map +1 -0
- package/output/core/modules/cookie/index.d.ts +21 -0
- package/output/core/modules/cookie/index.d.ts.map +1 -0
- package/output/{app/core/cookie.js → core/modules/cookie/index.js} +1 -6
- package/output/core/modules/identity/index.d.ts +4 -0
- package/output/core/modules/identity/index.d.ts.map +1 -0
- package/output/{app/core/identity.js → core/modules/identity/index.js} +1 -1
- package/output/core/modules/logging/index.d.ts +5 -0
- package/output/core/modules/logging/index.d.ts.map +1 -0
- package/output/{app/core/logging.js → core/modules/logging/index.js} +2 -2
- package/output/core/modules/openapi/index.d.ts +7 -0
- package/output/core/modules/openapi/index.d.ts.map +1 -0
- package/output/{app/core/openapi.js → core/modules/openapi/index.js} +11 -12
- package/output/core/modules/permission-operations/index.d.ts +26 -0
- package/output/core/modules/permission-operations/index.d.ts.map +1 -0
- package/output/core/modules/permission-operations/index.js +36 -0
- package/output/core/modules/permission-operations/index.test.d.ts +2 -0
- package/output/core/modules/permission-operations/index.test.d.ts.map +1 -0
- package/output/core/modules/permission-operations/index.test.js +45 -0
- package/output/{app/core/request-id.d.ts → core/modules/request-id/index.d.ts} +1 -4
- package/output/core/modules/request-id/index.d.ts.map +1 -0
- package/output/{app/core/request-id.js → core/modules/request-id/index.js} +2 -2
- package/output/core/modules/response/index.d.ts +50 -0
- package/output/core/modules/response/index.d.ts.map +1 -0
- package/output/{app/core/response.js → core/modules/response/index.js} +31 -18
- package/output/{schema/index.d.ts → core/modules/schema/builders.d.ts} +8 -1
- package/output/core/modules/schema/builders.d.ts.map +1 -0
- package/output/{schema/index.js → core/modules/schema/builders.js} +7 -4
- package/output/core/modules/schema/index.d.ts +34 -0
- package/output/core/modules/schema/index.d.ts.map +1 -0
- package/output/core/modules/schema/index.js +62 -0
- package/output/core/modules/schema/index.test.d.ts +2 -0
- package/output/core/modules/schema/index.test.d.ts.map +1 -0
- package/output/core/modules/schema/index.test.js +55 -0
- package/output/core/modules/system-routes/index.d.ts +15 -0
- package/output/core/modules/system-routes/index.d.ts.map +1 -0
- package/output/core/modules/system-routes/index.js +205 -0
- package/output/core/resource.d.ts +56 -0
- package/output/core/resource.d.ts.map +1 -0
- package/output/core/resource.js +56 -0
- package/output/core/test-support/database.d.ts +89 -0
- package/output/core/test-support/database.d.ts.map +1 -0
- package/output/core/test-support/database.js +131 -0
- package/output/core/test-support/database.test.d.ts +2 -0
- package/output/core/test-support/database.test.d.ts.map +1 -0
- package/output/core/test-support/database.test.js +111 -0
- package/output/core/test-support/environment.d.ts +38 -0
- package/output/core/test-support/environment.d.ts.map +1 -0
- package/output/core/test-support/environment.js +45 -0
- package/output/core/test-support/service-context.d.ts +64 -0
- package/output/core/test-support/service-context.d.ts.map +1 -0
- package/output/core/test-support/service-context.js +63 -0
- package/output/core/test-support/service-context.test.d.ts +2 -0
- package/output/core/test-support/service-context.test.d.ts.map +1 -0
- package/output/core/test-support/service-context.test.js +55 -0
- package/output/core/test-support/test-server.d.ts +154 -0
- package/output/core/test-support/test-server.d.ts.map +1 -0
- package/output/core/test-support/test-server.js +114 -0
- package/output/core/test-support/test-server.test.d.ts +2 -0
- package/output/core/test-support/test-server.test.d.ts.map +1 -0
- package/output/core/test-support/test-server.test.js +296 -0
- package/output/core/test-support/testdata.d.ts +255 -0
- package/output/core/test-support/testdata.d.ts.map +1 -0
- package/output/core/test-support/testdata.js +271 -0
- package/output/core/test-support/testdata.test.d.ts +2 -0
- package/output/core/test-support/testdata.test.d.ts.map +1 -0
- package/output/core/test-support/testdata.test.js +334 -0
- package/output/{app → core}/type-augment.d.ts +2 -2
- package/output/core/type-augment.d.ts.map +1 -0
- package/output/core/types.d.ts +582 -0
- package/output/core/types.d.ts.map +1 -0
- package/output/cors/index.d.ts +3 -0
- package/output/cors/index.d.ts.map +1 -0
- package/output/cors/index.js +28 -0
- package/output/index.d.ts +10 -4
- package/output/index.d.ts.map +1 -1
- package/output/index.js +25 -5
- package/output/rate-limit/index.d.ts +23 -0
- package/output/rate-limit/index.d.ts.map +1 -0
- package/output/rate-limit/index.js +43 -0
- package/output/{rate-limiter/rate-limiter.d.ts → rate-limit/limiter.d.ts} +1 -1
- package/output/rate-limit/limiter.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/memory-store.d.ts +6 -0
- package/output/rate-limit/memory-store.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/memory-store.js +8 -0
- package/output/{rate-limiter → rate-limit}/redis-store.d.ts +18 -0
- package/output/rate-limit/redis-store.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/redis-store.js +35 -0
- package/output/rate-limit/redis-store.test.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/redis-store.test.js +2 -2
- package/output/rate-limit/types.d.ts +45 -0
- package/output/rate-limit/types.d.ts.map +1 -0
- package/output/rate-limit/types.js +1 -0
- package/output/{redis-cache/redis-cache.d.ts → redis/cache.d.ts} +1 -1
- package/output/redis/cache.d.ts.map +1 -0
- package/output/redis/cache.test.d.ts +2 -0
- package/output/redis/cache.test.d.ts.map +1 -0
- package/output/{redis-cache/redis-cache.test.js → redis/cache.test.js} +1 -1
- package/output/{app/redis.d.ts → redis/index.d.ts} +12 -7
- package/output/redis/index.d.ts.map +1 -0
- package/output/{app/redis.js → redis/index.js} +15 -5
- package/output/redis/index.test.d.ts +2 -0
- package/output/{context → redis}/index.test.d.ts.map +1 -1
- package/output/redis/index.test.js +55 -0
- package/output/redis-queue/index.js +1 -1
- package/output/redis-queue/test-helpers.d.ts +1 -1
- package/output/redis-queue/test-helpers.js +1 -1
- package/output/redis-queue/types.d.ts +3 -12
- package/output/redis-queue/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/output/app/auth.test.d.ts +0 -2
- package/output/app/auth.test.d.ts.map +0 -1
- package/output/app/auth.test.js +0 -148
- package/output/app/boot.d.ts +0 -48
- package/output/app/boot.d.ts.map +0 -1
- package/output/app/boot.js +0 -123
- package/output/app/config.test.d.ts +0 -2
- package/output/app/config.test.d.ts.map +0 -1
- package/output/app/config.test.js +0 -132
- package/output/app/context.d.ts +0 -68
- package/output/app/context.d.ts.map +0 -1
- package/output/app/context.js +0 -73
- package/output/app/core/cookie.d.ts +0 -25
- package/output/app/core/cookie.d.ts.map +0 -1
- package/output/app/core/identity.d.ts +0 -12
- package/output/app/core/identity.d.ts.map +0 -1
- package/output/app/core/index.d.ts +0 -44
- package/output/app/core/index.d.ts.map +0 -1
- package/output/app/core/index.js +0 -57
- package/output/app/core/logging.d.ts +0 -13
- package/output/app/core/logging.d.ts.map +0 -1
- package/output/app/core/openapi.d.ts +0 -17
- package/output/app/core/openapi.d.ts.map +0 -1
- package/output/app/core/request-id.d.ts.map +0 -1
- package/output/app/core/response.d.ts +0 -41
- package/output/app/core/response.d.ts.map +0 -1
- package/output/app/define-server.negative-cases.d.ts +0 -32
- package/output/app/define-server.negative-cases.d.ts.map +0 -1
- package/output/app/define-server.negative-cases.js +0 -209
- package/output/app/define-server.test.d.ts +0 -2
- package/output/app/define-server.test.d.ts.map +0 -1
- package/output/app/define-server.test.js +0 -97
- package/output/app/entries.d.ts +0 -29
- package/output/app/entries.d.ts.map +0 -1
- package/output/app/entries.js +0 -64
- package/output/app/env.d.ts +0 -59
- package/output/app/env.d.ts.map +0 -1
- package/output/app/env.js +0 -66
- package/output/app/env.test.js +0 -124
- package/output/app/error-contract.test.d.ts +0 -2
- package/output/app/error-contract.test.d.ts.map +0 -1
- package/output/app/error-contract.test.js +0 -125
- package/output/app/errors.d.ts +0 -20
- package/output/app/errors.d.ts.map +0 -1
- package/output/app/errors.js +0 -51
- package/output/app/generate-openapi.test.d.ts +0 -2
- package/output/app/generate-openapi.test.d.ts.map +0 -1
- package/output/app/generate-openapi.test.js +0 -123
- package/output/app/lifecycle-logging.test.d.ts +0 -2
- package/output/app/lifecycle-logging.test.d.ts.map +0 -1
- package/output/app/lifecycle-logging.test.js +0 -155
- package/output/app/lifecycle.d.ts +0 -70
- package/output/app/lifecycle.d.ts.map +0 -1
- package/output/app/lifecycle.js +0 -99
- package/output/app/lifecycle.test.d.ts +0 -2
- package/output/app/lifecycle.test.d.ts.map +0 -1
- package/output/app/lifecycle.test.js +0 -152
- package/output/app/middleware/auth.d.ts +0 -71
- package/output/app/middleware/auth.d.ts.map +0 -1
- package/output/app/middleware/auth.js +0 -190
- package/output/app/middleware/context.d.ts +0 -13
- package/output/app/middleware/context.d.ts.map +0 -1
- package/output/app/middleware/context.js +0 -15
- package/output/app/middleware/cors.d.ts +0 -14
- package/output/app/middleware/cors.d.ts.map +0 -1
- package/output/app/middleware/cors.js +0 -30
- package/output/app/middleware/index.d.ts +0 -17
- package/output/app/middleware/index.d.ts.map +0 -1
- package/output/app/middleware/index.js +0 -16
- package/output/app/middleware/rate-limit.d.ts +0 -24
- package/output/app/middleware/rate-limit.d.ts.map +0 -1
- package/output/app/middleware/rate-limit.js +0 -46
- package/output/app/middleware-order.test.d.ts +0 -2
- package/output/app/middleware-order.test.d.ts.map +0 -1
- package/output/app/middleware-order.test.js +0 -148
- package/output/app/permission-operations.d.ts +0 -48
- package/output/app/permission-operations.d.ts.map +0 -1
- package/output/app/permission-operations.js +0 -77
- package/output/app/permission-operations.test.d.ts +0 -2
- package/output/app/permission-operations.test.d.ts.map +0 -1
- package/output/app/permission-operations.test.js +0 -72
- package/output/app/permission-registration.test.d.ts +0 -2
- package/output/app/permission-registration.test.d.ts.map +0 -1
- package/output/app/permission-registration.test.js +0 -138
- package/output/app/redis.d.ts.map +0 -1
- package/output/app/registered-operations.test.d.ts +0 -2
- package/output/app/registered-operations.test.d.ts.map +0 -1
- package/output/app/registered-operations.test.js +0 -57
- package/output/app/request-id.test.d.ts +0 -2
- package/output/app/request-id.test.d.ts.map +0 -1
- package/output/app/request-id.test.js +0 -130
- package/output/app/resources.test.d.ts +0 -2
- package/output/app/resources.test.d.ts.map +0 -1
- package/output/app/resources.test.js +0 -197
- package/output/app/shutdown.d.ts +0 -22
- package/output/app/shutdown.d.ts.map +0 -1
- package/output/app/shutdown.js +0 -30
- package/output/app/shutdown.test.d.ts +0 -2
- package/output/app/shutdown.test.d.ts.map +0 -1
- package/output/app/shutdown.test.js +0 -103
- package/output/app/signal.d.ts +0 -12
- package/output/app/signal.d.ts.map +0 -1
- package/output/app/smoke.test.d.ts +0 -2
- package/output/app/smoke.test.d.ts.map +0 -1
- package/output/app/smoke.test.js +0 -210
- package/output/app/test-support.d.ts +0 -21
- package/output/app/test-support.d.ts.map +0 -1
- package/output/app/test-support.js +0 -24
- package/output/app/type-augment.d.ts.map +0 -1
- package/output/app/types.d.ts +0 -211
- package/output/app/types.d.ts.map +0 -1
- package/output/context/index.d.ts +0 -6
- package/output/context/index.d.ts.map +0 -1
- package/output/context/index.js +0 -14
- package/output/rate-limiter/index.d.ts +0 -5
- package/output/rate-limiter/index.d.ts.map +0 -1
- package/output/rate-limiter/index.js +0 -4
- package/output/rate-limiter/memory-store.d.ts.map +0 -1
- package/output/rate-limiter/rate-limiter.d.ts.map +0 -1
- package/output/rate-limiter/redis-store.d.ts.map +0 -1
- package/output/rate-limiter/redis-store.test.d.ts.map +0 -1
- package/output/rate-limiter/types.d.ts +0 -27
- package/output/rate-limiter/types.d.ts.map +0 -1
- package/output/redis-cache/index.d.ts +0 -4
- package/output/redis-cache/index.d.ts.map +0 -1
- package/output/redis-cache/index.js +0 -1
- package/output/redis-cache/redis-cache.d.ts.map +0 -1
- package/output/redis-cache/redis-cache.test.d.ts +0 -2
- package/output/redis-cache/redis-cache.test.d.ts.map +0 -1
- package/output/schema/index.d.ts.map +0 -1
- /package/output/{app → core}/env.test.d.ts +0 -0
- /package/output/{rate-limiter → core/modules/auth-provider}/types.js +0 -0
- /package/output/{context → core/modules/context}/index.test.d.ts +0 -0
- /package/output/{context → core/modules/context}/index.test.js +0 -0
- /package/output/{app → core}/type-augment.js +0 -0
- /package/output/{app → core}/types.js +0 -0
- /package/output/{rate-limiter/rate-limiter.js → rate-limit/limiter.js} +0 -0
- /package/output/{rate-limiter → rate-limit}/redis-store.test.d.ts +0 -0
- /package/output/{redis-cache/redis-cache.js → redis/cache.js} +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { AppEnv, ClientsView, LoggerLike, ResourcesView, Server } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* **测试数据(testdata)机制**:`packages/server/testdata/*.ts|js` 下的脚本,在 `development` /
|
|
4
|
+
* `test` 启动时自动加载,并在用例之间被重新加载。
|
|
5
|
+
*
|
|
6
|
+
* ## 为什么是「目录 + 约定方法」,而不是资源上的生命周期
|
|
7
|
+
*
|
|
8
|
+
* 「这条用例需要哪些样本数据」与「这份数据怎么删掉」是**同一件事的正反面**,写在同一个文件里
|
|
9
|
+
* 才不容易漏。把 `cleanup` 挂到资源单元上做不到这一点:资源只知道「有哪些表 / 哪些 key」,
|
|
10
|
+
* 不知道「谁往里写了什么」——新增一个 model 就要回去改一次清库清单,漏改只会安静地变脏。
|
|
11
|
+
*
|
|
12
|
+
* 所以机制倒过来:**数据脚本自己负责自己**。
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* // packages/server/testdata/01-base.ts(推荐写法)
|
|
16
|
+
* import { defineTestData } from '@taicode/common-server/app'
|
|
17
|
+
* import type { AppResources } from '../source/resources'
|
|
18
|
+
*
|
|
19
|
+
* export default defineTestData<AppResources>({
|
|
20
|
+
* async load(ctx) { …插入样本数据… },
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* 命名导出 `load(ctx)` / `cleanup(ctx)`(+ 可选 `dependsOn`)继续支持;两者都有时默认导出优先。
|
|
25
|
+
*
|
|
26
|
+
* ## 契约(只有三条)
|
|
27
|
+
*
|
|
28
|
+
* 1. **目录固定**:`<cwd>/testdata`(`tsx watch ./source/index.ts` 与 `vitest` 的 cwd 都是
|
|
29
|
+
* `packages/server`)。目录不存在 = 没有脚本。
|
|
30
|
+
* 2. **`load` 必需,`cleanup` 可选**:只写 `load` 的脚本是**合法**的——它的数据由资源单元的
|
|
31
|
+
* `clean()`(典型实现 `TRUNCATE ... CASCADE`)在重装前清掉。需要「资源清不掉的东西」
|
|
32
|
+
* (下游 mock 预置、临时文件)时才写 `cleanup`。
|
|
33
|
+
* 3. **顺序**:文件名(字典序升序)是默认序;需要「我必须排在某脚本之后」时用
|
|
34
|
+
* `dependsOn` 显式声明,见下节。
|
|
35
|
+
*
|
|
36
|
+
* ## 顺序:`dependsOn` 是权威,文件名只做同层破平
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* // packages/server/testdata/02-organization.ts
|
|
40
|
+
* import { defineTestData } from '@taicode/common-server/app'
|
|
41
|
+
*
|
|
42
|
+
* export default defineTestData({
|
|
43
|
+
* dependsOn: ['01-database'],
|
|
44
|
+
* async load(ctx) { … },
|
|
45
|
+
* })
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* - **引用名 = 去扩展名的文件名**(`01-base.ts` → `'01-base'`),与 `TestDataUnit.name` 一致。
|
|
49
|
+
* - **只影响顺序,不注入任何东西**:声明依赖**不会**把前一个脚本的客户端/数据递给你——需要共享
|
|
50
|
+
* 资源时用 `ctx.resources`(框架注入的装配产物),而不是依赖另一个脚本。
|
|
51
|
+
* - **`cleanup` 不用声明**:它是装配序的逆序,依赖自动反向成立(后建的先拆)。
|
|
52
|
+
* - **同层按文件名升序**:没有依赖关系的脚本之间的相对顺序由文件名决定,结果确定可复现。
|
|
53
|
+
* - **缺依赖 / 自依赖 / 环一律报错**(不是跳过、不是猜测顺序):顺序是安全属性,猜错只会
|
|
54
|
+
* 变成偶发的外键冲突。
|
|
55
|
+
*
|
|
56
|
+
* 为什么要有这个字段:数字前缀是「顺序的替身」,中间插一个脚本就得重命名后面全部文件——
|
|
57
|
+
* 纯噪音改动,还会切断 git 历史。依赖声明让「谁必须在谁之后」贴着原因写。
|
|
58
|
+
*
|
|
59
|
+
* > 字段名与已删除的**模块装配期** `dependsOn`(连同 `validateLifecycleDependencies`,§11-2)
|
|
60
|
+
* > 无关:那个是资源/模块之间的构造依赖,这个是数据脚本之间的**执行顺序**。
|
|
61
|
+
*
|
|
62
|
+
* ## 两条与「安全」有关的约定
|
|
63
|
+
*
|
|
64
|
+
* - **`_` 开头的文件不是脚本**:它们是脚本之间共享的 helper(`_client.ts`),不会被 `load` /
|
|
65
|
+
* `cleanup`。否则扫描目录就会把工具文件也当成脚本执行。
|
|
66
|
+
* - **只在可清理环境执行**:`load` / `cleanup` 两个执行入口的**第 0 步**都是
|
|
67
|
+
* `assertCleanableEnvironment(ctx.env.ENV)`。判据是装配期的 `ctx.env` 快照(事后改写
|
|
68
|
+
* `process.env` 不影响结论)。
|
|
69
|
+
*
|
|
70
|
+
* ## 执行顺序与失败语义
|
|
71
|
+
*
|
|
72
|
+
* | 入口 | 顺序 | 失败 |
|
|
73
|
+
* | --- | --- | --- |
|
|
74
|
+
* | `loadTestData` | **执行序**(`dependsOn` 拓扑序,同层按文件名升序) | 立即冒泡 |
|
|
75
|
+
* | `cleanupTestData` | **执行序逆序**(无 `cleanup` 的脚本跳过) | 立即冒泡 |
|
|
76
|
+
*
|
|
77
|
+
* `cleanup` 逆序是「后建的先拆」,依赖因此不必反向声明。
|
|
78
|
+
*
|
|
79
|
+
* 失败**一律冒泡**,不降级成「跳过这条脚本」:数据没准备好时静默继续只会让后续用例读到脏状态,
|
|
80
|
+
* 制造伪绿。
|
|
81
|
+
*
|
|
82
|
+
* ## 脚本拿到什么:`ctx`
|
|
83
|
+
*
|
|
84
|
+
* 脚本不再自己读 env、不再自己建连接——框架把装配好的东西注入进来。`load` 跑在
|
|
85
|
+
* `app.ready()` 之后、`listen` 之前,所以装配产物**全部已经存在**:
|
|
86
|
+
*
|
|
87
|
+
* | 字段 | 是什么 | 为什么给 |
|
|
88
|
+
* | --- | --- | --- |
|
|
89
|
+
* | `env` | 装配快照 `AppEnv<Env>` | 脚本不碰 `process.env`;看到的就是应用看到的那份 |
|
|
90
|
+
* | `resources` | `ResourcesView<Resources>`(单元已解包) | 脚本写数据的地方就是被测服务读数据的地方 |
|
|
91
|
+
* | `config` | 启动期配置快照 `Awaited<Config>`(只读) | 有些基线必须以配置为输入(按配置里的管理员名单造账号) |
|
|
92
|
+
* | `clients` | `ClientsView<Clients>` | 预置 / 清理下游 mock 状态的唯一正当通道 |
|
|
93
|
+
* | `services` | `Awaited<Services>`(已 `init()`) | 走服务造数据才会自动满足业务不变量(哈希、缓存失效、审计) |
|
|
94
|
+
* | `log` | `ctx.log` | 日志与装配期同一路输出 |
|
|
95
|
+
*
|
|
96
|
+
* 「脚本自己建一份客户端」是**错的**:同一份数据经两条连接写/读,缓存与连接级状态对不上
|
|
97
|
+
* (account 曾经的 `globalThis` 重置注册表就是这个坑的产物)。所以注入 `resources` 是结构性的,
|
|
98
|
+
* 不是便利。
|
|
99
|
+
*
|
|
100
|
+
* **为什么不给 `app`**:拿到 `app` 就能 `inject()`,等于「跑一条业务链路来造数据」。那会一次性
|
|
101
|
+
* 引入中间件链(鉴权 / 限流 / requestId)、响应信封与错误映射,让基线脚本的失败语义、执行顺序、
|
|
102
|
+
* 耗时全部失控——而这三样正是 `dependsOn` 与「失败直接冒泡」想守住的东西。要跑链路请写在用例里。
|
|
103
|
+
*
|
|
104
|
+
* ## 三条使用边界
|
|
105
|
+
*
|
|
106
|
+
* 1. **首选 `resources`,`services` 只在「必须满足业务不变量」时用**,并在文件头写明原因。
|
|
107
|
+
* 同一个目录里一半 raw write、一半走 service,数据形状迟早会不一致。
|
|
108
|
+
* 2. **`config` 是快照**:脚本写配置表**不会**刷新当前实例的 `ctx.config`,要生效只能重新装配。
|
|
109
|
+
* 3. **`clients` 只用于预置 / 清理下游 mock**:`development` 会跑同一批脚本,误发就是真的发出去了。
|
|
110
|
+
*/
|
|
111
|
+
export interface TestDataContext<Resources = object, Env = object, Config = object, Clients = object, Services = object> {
|
|
112
|
+
/** 本次装配的 env 快照(框架固定约定 + 应用扩展字段);脚本**不必也不该**再读 `process.env` */
|
|
113
|
+
readonly env: AppEnv<Env>;
|
|
114
|
+
/** 装配好的资源(单元已解包):写数据 / 清缓存都用它,而不是另建一份客户端 */
|
|
115
|
+
readonly resources: ResourcesView<Resources>;
|
|
116
|
+
/** 启动期配置快照(只读):写配置表不会刷新它,要生效需重新装配 */
|
|
117
|
+
readonly config: Awaited<Config>;
|
|
118
|
+
/** 外部客户端:只用于预置 / 清理下游 mock,不得产生真实外部副作用 */
|
|
119
|
+
readonly clients: ClientsView<Clients>;
|
|
120
|
+
/** 已 `init()` 完成的服务实例:走它造数据才会满足业务不变量 */
|
|
121
|
+
readonly services: Awaited<Services>;
|
|
122
|
+
/** 记录型 logger(与装配期同一个) */
|
|
123
|
+
readonly log: LoggerLike;
|
|
124
|
+
}
|
|
125
|
+
/** 一个测试数据脚本(`testdata/*.ts` 的约定导出) */
|
|
126
|
+
export interface TestDataScript<Resources = object, Env = object, Config = object, Clients = object, Services = object> {
|
|
127
|
+
/** 加载本脚本负责的测试数据(可重复执行) */
|
|
128
|
+
load(ctx: TestDataContext<Resources, Env, Config, Clients, Services>): void | Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* 清理本脚本负责的测试数据(可选)。
|
|
131
|
+
*
|
|
132
|
+
* **大多数脚本不需要它**:内存数据库的行由资源单元的 `clean()`(典型实现
|
|
133
|
+
* `TRUNCATE ... CASCADE`)在重新装配前清掉。只有「资源清不掉的东西」才需要自己写
|
|
134
|
+
* `cleanup`——下游 mock 的预置状态、临时文件、外部队列的 key。
|
|
135
|
+
*
|
|
136
|
+
* 提供时必须能把 `load()` 的效果完全撤销;不提供时重装前由资源兜底。
|
|
137
|
+
*/
|
|
138
|
+
cleanup?(ctx: TestDataContext<Resources, Env, Config, Clients, Services>): void | Promise<void>;
|
|
139
|
+
/**
|
|
140
|
+
* 必须在哪些脚本**之后**执行(去扩展名的文件名;缺省 = 无要求)。
|
|
141
|
+
*
|
|
142
|
+
* 只影响排序:`load` 按依赖拓扑序(同层按文件名升序),`cleanup` 取其逆序。
|
|
143
|
+
* 校验严格:引用不存在的脚本、引用自己、循环依赖都会让装载阶段**抛错**。
|
|
144
|
+
*/
|
|
145
|
+
readonly dependsOn?: readonly string[];
|
|
146
|
+
}
|
|
147
|
+
/** `defineTestData()` 的入参:与 `TestDataScript` 同形(只有 `load` 必需) */
|
|
148
|
+
export type TestDataDefinition<Resources = object, Env = object, Config = object, Clients = object, Services = object> = TestDataScript<Resources, Env, Config, Clients, Services>;
|
|
149
|
+
/**
|
|
150
|
+
* 由**一个** `Server` 类型推出脚本类型(`defineTestData<typeof server>({ … })` 用的就是这个)。
|
|
151
|
+
*
|
|
152
|
+
* 五个槽位泛型本来要一个个手写、顺序还得对上(`Resources, Env, Config, Clients, Services` 与
|
|
153
|
+
* `Server` 的 `Resources, Config, Clients, Services, Env` 不同序);直接给 `server` 就没这个问题:
|
|
154
|
+
* 写错泛型名会当场报错,而不是静默拿到 `object`。
|
|
155
|
+
*
|
|
156
|
+
* 非 `Server` 类型(如直接传 `AccountResources`)退化为「只声明 `resources`」的老写法。
|
|
157
|
+
*/
|
|
158
|
+
export type TestDataScriptOf<S, Env = object> = S extends Server<infer Resources, infer Config, infer Clients, infer Services, infer AppEnvShape> ? TestDataScript<Resources, AppEnvShape, Config, Clients, Services> : TestDataScript<S, Env>;
|
|
159
|
+
/** `defineTestData<S>()` 的入参(`TestDataScriptOf` 的别名,语义更贴近调用处) */
|
|
160
|
+
export type TestDataDefinitionOf<S, Env = object> = TestDataScriptOf<S, Env>;
|
|
161
|
+
/**
|
|
162
|
+
* 声明一个测试数据脚本(**唯一正当的写法**)。
|
|
163
|
+
*
|
|
164
|
+
* ```ts
|
|
165
|
+
* // packages/server/testdata/03-system-config.ts
|
|
166
|
+
* import { defineTestData } from '@taicode/common-server/app'
|
|
167
|
+
* import type { server } from '../source/server' // 纯类型导入,无副作用
|
|
168
|
+
*
|
|
169
|
+
* export default defineTestData<typeof server>({
|
|
170
|
+
* dependsOn: ['01-database'],
|
|
171
|
+
* async load(ctx) { …插入样本数据… },
|
|
172
|
+
* async cleanup(ctx) { …删掉自己插入的数据… },
|
|
173
|
+
* })
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* 泛型写 `typeof server` 而不是逐个槽位:`ctx` 上的 `resources` / `config` / `clients` /
|
|
177
|
+
* `services` / `env` 类型一次到位。只想声明 `resources` 时也可以写
|
|
178
|
+
* `defineTestData<AccountResources>({ … })`(其余槽位退化为 `object`)。
|
|
179
|
+
*
|
|
180
|
+
* ## 它解决什么
|
|
181
|
+
*
|
|
182
|
+
* 命名导出 `load` 是**约定**,写错只在装载期才会暴露(少写 `load` = 启动直接失败)。
|
|
183
|
+
* `defineTestData` 把这份约定变成**定义期**就检查的东西:
|
|
184
|
+
*
|
|
185
|
+
* 1. **缺 `load` 当场报错**(不是等到 `discoverTestDataModules()` 扫描到它);
|
|
186
|
+
* 2. **`dependsOn` 当场校验**(必须是字符串数组、不能有重复项;引用不存在的脚本 / 自依赖 /
|
|
187
|
+
* 环属于跨文件问题,仍在装载期由 `sortByDependencies()` 统一判定);
|
|
188
|
+
* 3. **结果冻结**:脚本之间不会互相改写 `load` / `cleanup`(执行器拿到的就是定义时那份)。
|
|
189
|
+
*
|
|
190
|
+
* `ctx` 的类型由 `S` 一次性带进来:`defineTestData<typeof server>({ load(ctx) {…} })` 里
|
|
191
|
+
* `ctx.services` / `ctx.resources` / `ctx.config` 全部有类型,不需要每个脚本再手写
|
|
192
|
+
* `type Ctx = TestDataContext<…>`。
|
|
193
|
+
*
|
|
194
|
+
* ## 与「命名导出」的关系
|
|
195
|
+
*
|
|
196
|
+
* 两种写法都合法,`defineTestData` 只是把结构显式化:
|
|
197
|
+
*
|
|
198
|
+
* ```ts
|
|
199
|
+
* // 推荐:默认导出一个定义对象
|
|
200
|
+
* export default defineTestData<typeof server>({ load })
|
|
201
|
+
*
|
|
202
|
+
* // 等价:命名导出(旧写法,继续支持)
|
|
203
|
+
* export async function load(ctx: TestDataContext<AccountResources>) { … }
|
|
204
|
+
* export const dependsOn = ['01-database']
|
|
205
|
+
* ```
|
|
206
|
+
*
|
|
207
|
+
* 同一个文件里两者都有时,**默认导出优先**——避免「改了默认导出、命名导出还在生效」的歧义。
|
|
208
|
+
*/
|
|
209
|
+
export declare function defineTestData<S = object, Env = object>(definition: TestDataDefinitionOf<S, Env>): TestDataScriptOf<S, Env>;
|
|
210
|
+
/** 一个已从磁盘装载好的脚本单元(含名字,用于日志与报错定位) */
|
|
211
|
+
export interface TestDataUnit<Resources = object, Env = object, Config = object, Clients = object, Services = object> extends TestDataScript<Resources, Env, Config, Clients, Services> {
|
|
212
|
+
/** 脚本文件名去掉扩展名(`01-base.ts` → `01-base`) */
|
|
213
|
+
readonly name: string;
|
|
214
|
+
}
|
|
215
|
+
/** 两个执行入口的共同入参 */
|
|
216
|
+
export interface TestDataRunParams<Resources = object, Env = object, Config = object, Clients = object, Services = object> {
|
|
217
|
+
/**
|
|
218
|
+
* 本次装配的 ctx(`env` + `resources` + `config` + `clients` + `services` + `log`)。**必填**:
|
|
219
|
+
*
|
|
220
|
+
* - 闸门读 `ctx.env.ENV`:非 `CLEANABLE_ENVS`(`development` / `test`)一律抛错,一个脚本都不跑;
|
|
221
|
+
* - 脚本拿到的就是这份 ctx,所以它看到的环境 / 资源与本次装配完全一致。
|
|
222
|
+
*/
|
|
223
|
+
ctx: TestDataContext<Resources, Env, Config, Clients, Services>;
|
|
224
|
+
/** 已装载的脚本(`discoverTestDataModules()` 的产物);缺省 = 没有脚本 */
|
|
225
|
+
units?: readonly TestDataUnit<Resources, Env, Config, Clients, Services>[];
|
|
226
|
+
/** 日志标签(`defineServer({ app: { service } })`) */
|
|
227
|
+
service?: string;
|
|
228
|
+
}
|
|
229
|
+
/** 默认脚本目录(相对 `process.cwd()`) */
|
|
230
|
+
export declare function resolveTestDataDirectory(): string;
|
|
231
|
+
/**
|
|
232
|
+
* 扫描目录并**装载**全部脚本(`import()` 一次,模块本身由 ESM 缓存复用)。
|
|
233
|
+
*
|
|
234
|
+
* 目录不存在 → 空数组(不是错误:没写测试数据的服务是合法的)。
|
|
235
|
+
* 文件存在但缺 `load` → **抛错**,并指出是哪个文件。
|
|
236
|
+
*
|
|
237
|
+
* 返回的顺序就是**执行序**:`dependsOn` 拓扑序 + 同层文件名升序(`load` 直接正序跑,
|
|
238
|
+
* `cleanup` 直接逆序跑)。
|
|
239
|
+
*/
|
|
240
|
+
export declare function discoverTestDataModules<Resources = object, Env = object, Config = object, Clients = object, Services = object>(directory?: string): Promise<TestDataUnit<Resources, Env, Config, Clients, Services>[]>;
|
|
241
|
+
/**
|
|
242
|
+
* 加载测试数据:按**执行序**逐项 `await load(ctx)`(依赖在前,同层按文件名升序)。
|
|
243
|
+
*
|
|
244
|
+
* 在 `createTestServer()` / `startServer()` 装配完成后自动执行一次;也可以由测试自己再调。
|
|
245
|
+
*/
|
|
246
|
+
export declare function loadTestData<Resources, Env, Config, Clients, Services>(params: TestDataRunParams<Resources, Env, Config, Clients, Services>): Promise<void>;
|
|
247
|
+
/**
|
|
248
|
+
* 清理测试数据:按执行序**逆序**逐项 `await cleanup()`(后建的先拆)。
|
|
249
|
+
*
|
|
250
|
+
* 依赖不必反向声明:`load` 的拓扑序取逆就是安全的拆除序。没有 `cleanup` 的脚本直接跳过。
|
|
251
|
+
*
|
|
252
|
+
* 注意:它只负责「把脚本插入的东西撤掉」,不是「把整个库清空」——库由资源单元的 `clean()` 清。
|
|
253
|
+
*/
|
|
254
|
+
export declare function cleanupTestData<Resources, Env, Config, Clients, Services>(params: TestDataRunParams<Resources, Env, Config, Clients, Services>): Promise<void>;
|
|
255
|
+
//# sourceMappingURL=testdata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testdata.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/testdata.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { basename, extname, join, resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { assertCleanableEnvironment } from '../env.js';
|
|
5
|
+
/**
|
|
6
|
+
* 声明一个测试数据脚本(**唯一正当的写法**)。
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // packages/server/testdata/03-system-config.ts
|
|
10
|
+
* import { defineTestData } from '@taicode/common-server/app'
|
|
11
|
+
* import type { server } from '../source/server' // 纯类型导入,无副作用
|
|
12
|
+
*
|
|
13
|
+
* export default defineTestData<typeof server>({
|
|
14
|
+
* dependsOn: ['01-database'],
|
|
15
|
+
* async load(ctx) { …插入样本数据… },
|
|
16
|
+
* async cleanup(ctx) { …删掉自己插入的数据… },
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* 泛型写 `typeof server` 而不是逐个槽位:`ctx` 上的 `resources` / `config` / `clients` /
|
|
21
|
+
* `services` / `env` 类型一次到位。只想声明 `resources` 时也可以写
|
|
22
|
+
* `defineTestData<AccountResources>({ … })`(其余槽位退化为 `object`)。
|
|
23
|
+
*
|
|
24
|
+
* ## 它解决什么
|
|
25
|
+
*
|
|
26
|
+
* 命名导出 `load` 是**约定**,写错只在装载期才会暴露(少写 `load` = 启动直接失败)。
|
|
27
|
+
* `defineTestData` 把这份约定变成**定义期**就检查的东西:
|
|
28
|
+
*
|
|
29
|
+
* 1. **缺 `load` 当场报错**(不是等到 `discoverTestDataModules()` 扫描到它);
|
|
30
|
+
* 2. **`dependsOn` 当场校验**(必须是字符串数组、不能有重复项;引用不存在的脚本 / 自依赖 /
|
|
31
|
+
* 环属于跨文件问题,仍在装载期由 `sortByDependencies()` 统一判定);
|
|
32
|
+
* 3. **结果冻结**:脚本之间不会互相改写 `load` / `cleanup`(执行器拿到的就是定义时那份)。
|
|
33
|
+
*
|
|
34
|
+
* `ctx` 的类型由 `S` 一次性带进来:`defineTestData<typeof server>({ load(ctx) {…} })` 里
|
|
35
|
+
* `ctx.services` / `ctx.resources` / `ctx.config` 全部有类型,不需要每个脚本再手写
|
|
36
|
+
* `type Ctx = TestDataContext<…>`。
|
|
37
|
+
*
|
|
38
|
+
* ## 与「命名导出」的关系
|
|
39
|
+
*
|
|
40
|
+
* 两种写法都合法,`defineTestData` 只是把结构显式化:
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* // 推荐:默认导出一个定义对象
|
|
44
|
+
* export default defineTestData<typeof server>({ load })
|
|
45
|
+
*
|
|
46
|
+
* // 等价:命名导出(旧写法,继续支持)
|
|
47
|
+
* export async function load(ctx: TestDataContext<AccountResources>) { … }
|
|
48
|
+
* export const dependsOn = ['01-database']
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* 同一个文件里两者都有时,**默认导出优先**——避免「改了默认导出、命名导出还在生效」的歧义。
|
|
52
|
+
*/
|
|
53
|
+
export function defineTestData(definition) {
|
|
54
|
+
// 类型上 `load` 必填,但脚本是纯 JS 生态:运行期仍要挡一次(写错时在定义期就炸,而不是装载期)
|
|
55
|
+
const script = definition;
|
|
56
|
+
if (typeof script.load !== 'function') {
|
|
57
|
+
throw new Error('[testdata] defineTestData 缺少 load:必须导出 load(ctx)');
|
|
58
|
+
}
|
|
59
|
+
if (script.cleanup !== undefined && typeof script.cleanup !== 'function') {
|
|
60
|
+
throw new Error('[testdata] defineTestData 的 cleanup 必须是函数(不需要清理就不写)');
|
|
61
|
+
}
|
|
62
|
+
const dependsOn = script.dependsOn;
|
|
63
|
+
if (dependsOn === undefined) {
|
|
64
|
+
// `TestDataScriptOf<S, Env>` 是条件类型,裸泛型参数上无法自动归约;这里的结构已经由入参约束过
|
|
65
|
+
return Object.freeze({ load: definition.load, cleanup: definition.cleanup });
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(dependsOn) || dependsOn.some((item) => typeof item !== 'string' || item.length === 0)) {
|
|
68
|
+
throw new Error('[testdata] defineTestData 的 dependsOn 必须是非空字符串数组(引用其它脚本的去扩展名文件名)');
|
|
69
|
+
}
|
|
70
|
+
if (new Set(dependsOn).size !== dependsOn.length) {
|
|
71
|
+
throw new Error(`[testdata] defineTestData 的 dependsOn 有重复项:${dependsOn.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
load: definition.load,
|
|
75
|
+
cleanup: definition.cleanup,
|
|
76
|
+
dependsOn: Object.freeze([...dependsOn]),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** 默认脚本目录(相对 `process.cwd()`) */
|
|
80
|
+
export function resolveTestDataDirectory() {
|
|
81
|
+
return resolve(process.cwd(), DEFAULT_TESTDATA_DIRECTORY);
|
|
82
|
+
}
|
|
83
|
+
const DEFAULT_TESTDATA_DIRECTORY = 'testdata';
|
|
84
|
+
/** 会被当作脚本的扩展名 */
|
|
85
|
+
const TESTDATA_EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'];
|
|
86
|
+
/** `_` 前缀 = 共享 helper,不参与 load / cleanup */
|
|
87
|
+
const HELPER_PREFIX = '_';
|
|
88
|
+
function isTestDataFile(name) {
|
|
89
|
+
if (name.startsWith(HELPER_PREFIX)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
// `.d.ts` 是纯声明,没有运行期实现
|
|
93
|
+
if (name.endsWith('.d.ts')) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return TESTDATA_EXTENSIONS.includes(extname(name));
|
|
97
|
+
}
|
|
98
|
+
function isRecord(value) {
|
|
99
|
+
return typeof value === 'object' && value !== null;
|
|
100
|
+
}
|
|
101
|
+
/** 读一个脚本模块的 `dependsOn`(缺省 = 空数组;类型不对直接报错) */
|
|
102
|
+
function readDependsOn(file, script) {
|
|
103
|
+
const value = script.dependsOn;
|
|
104
|
+
if (value === undefined) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
108
|
+
throw new Error(`[testdata] "${file}" 的 dependsOn 必须是字符串数组(引用其它脚本的去扩展名文件名)`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 从一个已 `import()` 的模块里取出脚本本体。
|
|
114
|
+
*
|
|
115
|
+
* **默认导出优先**:`export default defineTestData({ … })` 是推荐写法;同一个文件里还留着
|
|
116
|
+
* 命名导出时以默认导出为准,避免「改了默认导出、命名导出仍在生效」这种看不见的分裂。
|
|
117
|
+
*/
|
|
118
|
+
function readScript(file, loaded) {
|
|
119
|
+
const candidate = loaded.default;
|
|
120
|
+
if (isRecord(candidate) && typeof candidate.load === 'function') {
|
|
121
|
+
return candidate;
|
|
122
|
+
}
|
|
123
|
+
return loaded;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 按 `dependsOn` 做**稳定**拓扑排序(Kahn):同层取文件名升序。
|
|
127
|
+
*
|
|
128
|
+
* 三个不变式:
|
|
129
|
+
* - **依赖是权威**:只要 A 声明依赖 B,A 一定排在 B 之后,文件名管不了它。
|
|
130
|
+
* - **同层可复现**:没有依赖关系的脚本按文件名升序,跑多少次都是同一个序。
|
|
131
|
+
* - **坏图直接报错**:未知依赖 / 自依赖 / 循环依赖都在装载期抛错,不猜顺序——
|
|
132
|
+
* 猜错只会变成偶发的外键冲突,比当场失败难查得多。
|
|
133
|
+
*/
|
|
134
|
+
function sortByDependencies(units) {
|
|
135
|
+
const known = new Set(units.map((unit) => unit.name));
|
|
136
|
+
const pending = new Map();
|
|
137
|
+
const dependents = new Map();
|
|
138
|
+
for (const unit of units) {
|
|
139
|
+
const deps = [...new Set(unit.dependsOn ?? [])];
|
|
140
|
+
for (const dep of deps) {
|
|
141
|
+
if (dep === unit.name) {
|
|
142
|
+
throw new Error(`[testdata] "${unit.name}" 依赖了自己:dependsOn 只能引用其它脚本`);
|
|
143
|
+
}
|
|
144
|
+
if (!known.has(dep)) {
|
|
145
|
+
throw new Error(`[testdata] "${unit.name}" 依赖的 "${dep}" 不存在:dependsOn 用去扩展名的文件名`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
pending.set(unit.name, deps.length);
|
|
149
|
+
for (const dep of deps) {
|
|
150
|
+
dependents.set(dep, [...(dependents.get(dep) ?? []), unit.name]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// 候选池保持升序:同层先跑文件名小的
|
|
154
|
+
const ready = units.filter((unit) => pending.get(unit.name) === 0).map((unit) => unit.name).sort();
|
|
155
|
+
const order = [];
|
|
156
|
+
while (ready.length > 0) {
|
|
157
|
+
const name = ready.shift();
|
|
158
|
+
order.push(name);
|
|
159
|
+
for (const next of dependents.get(name) ?? []) {
|
|
160
|
+
const rest = (pending.get(next) ?? 0) - 1;
|
|
161
|
+
pending.set(next, rest);
|
|
162
|
+
if (rest === 0) {
|
|
163
|
+
ready.push(next);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
ready.sort();
|
|
167
|
+
}
|
|
168
|
+
if (order.length !== units.length) {
|
|
169
|
+
const stuck = units.filter((unit) => !order.includes(unit.name)).map((unit) => unit.name);
|
|
170
|
+
throw new Error(`[testdata] 存在循环依赖:${stuck.join(' → ')}`);
|
|
171
|
+
}
|
|
172
|
+
return order.map((name) => units.find((unit) => unit.name === name));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 扫描目录并**装载**全部脚本(`import()` 一次,模块本身由 ESM 缓存复用)。
|
|
176
|
+
*
|
|
177
|
+
* 目录不存在 → 空数组(不是错误:没写测试数据的服务是合法的)。
|
|
178
|
+
* 文件存在但缺 `load` → **抛错**,并指出是哪个文件。
|
|
179
|
+
*
|
|
180
|
+
* 返回的顺序就是**执行序**:`dependsOn` 拓扑序 + 同层文件名升序(`load` 直接正序跑,
|
|
181
|
+
* `cleanup` 直接逆序跑)。
|
|
182
|
+
*/
|
|
183
|
+
export async function discoverTestDataModules(directory = resolveTestDataDirectory()) {
|
|
184
|
+
let entries;
|
|
185
|
+
try {
|
|
186
|
+
entries = (await readdir(directory, { withFileTypes: true }))
|
|
187
|
+
.filter((entry) => entry.isFile())
|
|
188
|
+
.map((entry) => entry.name);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (isRecord(error) && error.code === 'ENOENT') {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
const files = entries.filter(isTestDataFile).sort();
|
|
197
|
+
const units = [];
|
|
198
|
+
for (const file of files) {
|
|
199
|
+
const name = basename(file, extname(file));
|
|
200
|
+
const moduleUrl = pathToFileURL(join(directory, file)).href;
|
|
201
|
+
let loaded;
|
|
202
|
+
try {
|
|
203
|
+
loaded = await import(moduleUrl);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
throw new Error(`[testdata] 装载 "${file}" 失败:${error instanceof Error ? error.message : String(error)}`, {
|
|
207
|
+
cause: error,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (!isRecord(loaded)) {
|
|
211
|
+
throw new Error(`[testdata] "${file}" 没有导出任何东西:必须导出 load(或用 defineTestData 作为默认导出)`);
|
|
212
|
+
}
|
|
213
|
+
const script = readScript(file, loaded);
|
|
214
|
+
if (typeof script.load !== 'function') {
|
|
215
|
+
throw new Error(`[testdata] "${file}" 缺少 load 导出:必须导出 load(ctx)`);
|
|
216
|
+
}
|
|
217
|
+
if (script.cleanup !== undefined && typeof script.cleanup !== 'function') {
|
|
218
|
+
throw new Error(`[testdata] "${file}" 的 cleanup 必须是函数(不需要清理就删掉这个导出)`);
|
|
219
|
+
}
|
|
220
|
+
units.push({
|
|
221
|
+
name,
|
|
222
|
+
dependsOn: readDependsOn(file, script),
|
|
223
|
+
load: script.load,
|
|
224
|
+
cleanup: script.cleanup,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return sortByDependencies(units);
|
|
228
|
+
}
|
|
229
|
+
/** 跑一个脚本步骤(统一日志,失败冒泡给调用方决定) */
|
|
230
|
+
async function runTestDataUnit(params, unit, phase) {
|
|
231
|
+
const step = unit[phase];
|
|
232
|
+
if (!step) {
|
|
233
|
+
// `cleanup` 可选:没写的脚本由资源单元的 `clean()` 兜底
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const fields = { service: params.service ?? '', unit: unit.name, stage: 'testdata', phase };
|
|
237
|
+
params.ctx.log.debug(fields, `[testdata] ${phase} "${unit.name}" start`);
|
|
238
|
+
try {
|
|
239
|
+
await step(params.ctx);
|
|
240
|
+
params.ctx.log.debug(fields, `[testdata] ${phase} "${unit.name}" done`);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
params.ctx.log.error({ ...fields, error }, `[testdata] ${phase} "${unit.name}" failed`);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* 加载测试数据:按**执行序**逐项 `await load(ctx)`(依赖在前,同层按文件名升序)。
|
|
249
|
+
*
|
|
250
|
+
* 在 `createTestServer()` / `startServer()` 装配完成后自动执行一次;也可以由测试自己再调。
|
|
251
|
+
*/
|
|
252
|
+
export async function loadTestData(params) {
|
|
253
|
+
// 闸门先于一切:非可清理环境下一个脚本都不许跑
|
|
254
|
+
assertCleanableEnvironment(params.ctx.env.ENV);
|
|
255
|
+
for (const unit of sortByDependencies(params.units ?? [])) {
|
|
256
|
+
await runTestDataUnit(params, unit, 'load');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 清理测试数据:按执行序**逆序**逐项 `await cleanup()`(后建的先拆)。
|
|
261
|
+
*
|
|
262
|
+
* 依赖不必反向声明:`load` 的拓扑序取逆就是安全的拆除序。没有 `cleanup` 的脚本直接跳过。
|
|
263
|
+
*
|
|
264
|
+
* 注意:它只负责「把脚本插入的东西撤掉」,不是「把整个库清空」——库由资源单元的 `clean()` 清。
|
|
265
|
+
*/
|
|
266
|
+
export async function cleanupTestData(params) {
|
|
267
|
+
assertCleanableEnvironment(params.ctx.env.ENV);
|
|
268
|
+
for (const unit of sortByDependencies(params.units ?? []).reverse()) {
|
|
269
|
+
await runTestDataUnit(params, unit, 'cleanup');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testdata.test.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/testdata.test.ts"],"names":[],"mappings":""}
|