scripts-test-template.mjs
6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#!/usr/bin/env node
// scripts/test.mjs —— 合并到默认分支(main / master)前的测试闸门。
// 顺序:detect → setup-db(复制源库→副本)→ build → lint → unit+integration → e2e → 绿则晋升新迁移到源库
// → finally 删副本(无论红绿;源库绝不被 drop)
// 由 coding.mjs 的 test-gate stage(通过子会话)调用。
//
// 跨平台:所有命令经 child_process.spawnSync(cmd, { shell:true }) 执行,
// 在 Windows 走 cmd.exe,在 *nix 走 /bin/sh,无需 WSL / Git-Bash。
// 命令字符串来自 docs/04 §零(构建/lint/单测/e2e)——由 skeleton-gen 在 Plan 期填充。
import { spawnSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const PROJECT_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
const CONFIG_FILE = join(PROJECT_ROOT, 'config-vars.yaml')
// 从 config-vars.yaml 只取 database.schema(源库名)——best-effort,缺文件/缺值返回空串。
function readSourceSchema() {
if (!existsSync(CONFIG_FILE)) return ''
let inDatabase = false
for (const raw of readFileSync(CONFIG_FILE, 'utf8').split('\n')) {
const line = raw.replace(/\r$/, '')
if (line.trim() === '' || line.trim()[0] === '#') continue
const indent = line.length - line.replace(/^\s+/, '').length
if (indent === 0) { inDatabase = /^database\s*:/.test(line.trim()); continue }
if (!inDatabase) continue
const m = line.match(/^\s+schema\s*:\s*(.+?)\s*$/)
if (m) {
let v = m[1].trim()
if (v[0] === '"' || v[0] === "'") { const e = v.indexOf(v[0], 1); if (e !== -1) v = v.slice(1, e) }
else { const h = v.indexOf(' #'); if (h !== -1) v = v.slice(0, h).trim() }
return v
}
}
return ''
}
// 测试 DB = 一次性**副本库**(源库永不被本闸门 drop)。副本名经 ERP_TEST_DB_SCHEMA env 贯通到全部子进程——
// setup/drop/promote 脚本直读该 env;gradle test fork 的 JVM 与 e2e globalSetup 起的 bootRun 默认继承 env,
// 由后端 application*.yml 的 ${ERP_TEST_DB_SCHEMA:<source>__test} 占位消费(锁定约定见 docs/04 §一)。
// env 缺省时读 lane 兜底 marker(.tmp/erp-lane-db,并行波次执行器建 lane worktree 后写入)回填 process.env。
if (!process.env.ERP_TEST_DB_SCHEMA) {
const markerFile = join(PROJECT_ROOT, '.tmp', 'erp-lane-db')
if (existsSync(markerFile)) {
const laneSchema = readFileSync(markerFile, 'utf8').split('\n')[0].trim()
if (laneSchema !== '') {
process.env.ERP_TEST_DB_SCHEMA = laneSchema
console.log(`[test.mjs] lane DB marker 生效: ERP_TEST_DB_SCHEMA=${laneSchema}`)
}
}
}
// 串行缺省:仍无 env → 显式派生 `<source>__test` 并导出到所有子进程。这一步是安全关键——
// 即便后端 application*.yml 占位缺省万一回退到源库,此处显式 env 也会把它覆写到副本,测试绝不触碰源库。
if (!process.env.ERP_TEST_DB_SCHEMA) {
const src = readSourceSchema()
if (src !== '') {
process.env.ERP_TEST_DB_SCHEMA = `${src}__test`
console.log(`[test.mjs] 串行缺省副本库: ERP_TEST_DB_SCHEMA=${src}__test`)
}
}
// 闸门失败类错误:携带退出码,供顶层在跑完 finally(删副本)后透传。
class GateError extends Error {
constructor(message, code) { super(message); this.code = code }
}
// 在指定子目录下跑一条 shell 命令;非零退出码即抛 GateError(由顶层 finally 兜底删副本后透传码)。
function run(label, command, cwd = PROJECT_ROOT) {
console.log(`[test.mjs] ${label}: ${command}`)
const res = spawnSync(command, { cwd, shell: true, stdio: 'inherit' })
if (res.error) throw new GateError(`无法执行 (${label}): ${res.error.message}`, 1)
if (res.status !== 0) throw new GateError(`FAIL (${label}) exit=${res.status}`, res.status === null ? 1 : res.status)
}
// Stack detection (runtime, mode-agnostic)
const hasBackend = existsSync(join(PROJECT_ROOT, 'backend'))
const hasFrontend = existsSync(join(PROJECT_ROOT, 'frontend'))
if (!hasBackend && !hasFrontend) {
console.error('[test.mjs] FATAL: neither backend/ nor frontend/ exists')
process.exit(1)
}
const backendDir = join(PROJECT_ROOT, 'backend')
const frontendDir = join(PROJECT_ROOT, 'frontend')
const nodeScript = (name) => `node ${JSON.stringify(join('scripts', name))}`
let failure = null
try {
console.log('[test.mjs] 1/5 setup test db(复制源库 → 一次性副本)')
run('setup-test-db', nodeScript('setup-test-db.mjs'))
console.log('[test.mjs] 2/5 build')
if (hasBackend) run('backend build', '{{backend_build}}', backendDir)
else console.log('[test.mjs] skip backend build')
if (hasFrontend) run('frontend build', '{{frontend_build}}', frontendDir)
else console.log('[test.mjs] skip frontend build')
console.log('[test.mjs] 3/5 lint')
if (hasBackend) run('backend lint', '{{backend_lint}}', backendDir)
else console.log('[test.mjs] skip backend lint')
if (hasFrontend) run('frontend lint', '{{frontend_lint}}', frontendDir)
else console.log('[test.mjs] skip frontend lint')
console.log('[test.mjs] 4/5 unit + integration')
if (hasBackend) run('backend test', '{{backend_test}}', backendDir)
else console.log('[test.mjs] skip backend test')
if (hasFrontend) run('frontend test', '{{frontend_test}}', frontendDir)
else console.log('[test.mjs] skip frontend test')
console.log('[test.mjs] 5/5 E2E')
run('e2e', '{{e2e_cmd}}')
// 绿 → 把副本上已验证的本轮新迁移晋升到源库(幂等;跨 lane 串行由编排层 test-gate 起栈互斥保证)。
console.log('[test.mjs] promote:晋升本轮新迁移到源库')
run('promote-to-source', nodeScript('promote-to-source.mjs'))
console.log('[test.mjs] GREEN')
} catch (e) {
failure = e instanceof GateError ? e : new GateError(String(e?.message || e), 1)
console.error(`[test.mjs] ${failure.message}`)
} finally {
// 做完测试直接删副本(无论红绿;源库绝不被删)。
try {
run('drop-test-db', nodeScript('drop-test-db.mjs'))
} catch (e) {
console.error(`[test.mjs] 警告:删副本失败:${e?.message || e}`)
if (!failure) failure = e instanceof GateError ? e : new GateError(String(e?.message || e), 1)
}
}
if (failure) process.exit(failure.code ?? 1)