@elench/testkit 0.1.115 → 0.1.116
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 +33 -8
- package/lib/cli/args.mjs +3 -3
- package/lib/cli/command-flags.mjs +4 -0
- package/lib/cli/commands/db/schema/refresh.mjs +21 -0
- package/lib/cli/commands/db/schema/verify.mjs +27 -0
- package/lib/cli/entrypoint.mjs +1 -0
- package/lib/cli/operations/db/schema/refresh/operation.mjs +56 -0
- package/lib/cli/operations/db/{snapshot/capture → schema/verify}/operation.mjs +6 -27
- package/lib/cli/operations/run/operation.mjs +1 -0
- package/lib/cli/renderers/db-schema/text.mjs +7 -0
- package/lib/config/database.mjs +64 -0
- package/lib/config-api/index.d.ts +16 -1
- package/lib/config-api/index.mjs +31 -16
- package/lib/database/fingerprint.mjs +2 -0
- package/lib/database/index.mjs +142 -104
- package/lib/database/schema-source.mjs +295 -0
- package/lib/database/template-steps.mjs +158 -38
- package/lib/runner/orchestrator.mjs +4 -3
- package/lib/runner/template-steps.mjs +12 -1
- package/lib/runner/template.mjs +16 -1
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/node_modules/es-toolkit/CHANGELOG.md +801 -0
- package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +1 -0
- package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +3 -0
- package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +4 -0
- package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +4 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/package.json +8 -5
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/lib/cli/commands/db/snapshot/capture.mjs +0 -26
- package/lib/cli/renderers/db-snapshot-capture/text.mjs +0 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
2
3
|
import path from "path";
|
|
3
4
|
import { execa } from "execa";
|
|
4
5
|
import { buildTemplateExecutionEnv } from "../runner/template.mjs";
|
|
@@ -25,6 +26,25 @@ export async function runTemplateStage(config, stageName, databaseUrl, options =
|
|
|
25
26
|
reporter: options.reporter || null,
|
|
26
27
|
setupRegistry: options.setupRegistry || null,
|
|
27
28
|
parentOperation: options.parentOperation || null,
|
|
29
|
+
afterStep: options.afterStep || null,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function runTemplateStep(config, stageName, step, stepIndex, databaseUrl, options = {}) {
|
|
34
|
+
const env = {
|
|
35
|
+
...buildTemplateExecutionEnv(config, {}, process.env),
|
|
36
|
+
DATABASE_URL: databaseUrl,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
await runConfiguredSteps({
|
|
40
|
+
config,
|
|
41
|
+
steps: [step],
|
|
42
|
+
env,
|
|
43
|
+
labelPrefix: `template:${stageName}`,
|
|
44
|
+
reporter: options.reporter || null,
|
|
45
|
+
setupRegistry: options.setupRegistry || null,
|
|
46
|
+
parentOperation: options.parentOperation || null,
|
|
47
|
+
startIndex: stepIndex,
|
|
28
48
|
});
|
|
29
49
|
}
|
|
30
50
|
|
|
@@ -36,45 +56,95 @@ export function collectTemplateInputs(productDir, template = {}) {
|
|
|
36
56
|
});
|
|
37
57
|
}
|
|
38
58
|
|
|
39
|
-
export async function
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
59
|
+
export async function captureTemplateSnapshotText(config, databaseUrl, options = {}) {
|
|
60
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-schema-snapshot-"));
|
|
61
|
+
const tempPath = path.join(tempDir, "schema.sql");
|
|
62
|
+
try {
|
|
63
|
+
await dumpPostgresSchemaToFile(config, tempPath, databaseUrl, options);
|
|
64
|
+
return fs.readFileSync(tempPath, "utf8");
|
|
65
|
+
} finally {
|
|
66
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
43
69
|
|
|
44
|
-
|
|
45
|
-
|
|
70
|
+
export async function dumpPostgresSchemaToFile(config, outputPath, databaseUrl, options = {}) {
|
|
71
|
+
const env = {
|
|
72
|
+
...buildTemplateExecutionEnv(config, {}, process.env),
|
|
73
|
+
...buildPostgresConnectionEnv(databaseUrl),
|
|
74
|
+
};
|
|
75
|
+
const result = await runPgDumpCommand(config, "pg_dump", pgDumpArgs(), env, options);
|
|
76
|
+
if (result.exitCode !== 0 && isPgDumpServerVersionMismatch(result)) {
|
|
77
|
+
const serverMajor = parsePgDumpServerMajor(result);
|
|
78
|
+
if (serverMajor) {
|
|
79
|
+
const fallback = await runDockerizedPgDump(config, serverMajor, env, options);
|
|
80
|
+
if (fallback.exitCode === 0) {
|
|
81
|
+
fs.writeFileSync(outputPath, fallback.stdout);
|
|
82
|
+
sanitizeSnapshotFile(outputPath);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw new Error(fallback.shortMessage || fallback.stderr || fallback.stdout || "dockerized pg_dump failed");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (result.exitCode !== 0) {
|
|
89
|
+
throw new Error(result.shortMessage || result.stderr || result.stdout || "pg_dump failed");
|
|
90
|
+
}
|
|
91
|
+
fs.writeFileSync(outputPath, result.stdout);
|
|
92
|
+
sanitizeSnapshotFile(outputPath);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function pgDumpArgs() {
|
|
96
|
+
return [
|
|
97
|
+
"--schema-only",
|
|
98
|
+
"--no-owner",
|
|
99
|
+
"--no-privileges",
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function runDockerizedPgDump(config, serverMajor, env, options) {
|
|
104
|
+
const image = `${process.env.TESTKIT_PG_DUMP_IMAGE_PREFIX || "postgres"}:${serverMajor}`;
|
|
105
|
+
return runPgDumpCommand(
|
|
106
|
+
config,
|
|
107
|
+
"docker",
|
|
46
108
|
[
|
|
47
|
-
"
|
|
48
|
-
"--
|
|
49
|
-
"--
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
109
|
+
"run",
|
|
110
|
+
"--rm",
|
|
111
|
+
"--network",
|
|
112
|
+
"host",
|
|
113
|
+
"-e",
|
|
114
|
+
"PGHOST",
|
|
115
|
+
"-e",
|
|
116
|
+
"PGPORT",
|
|
117
|
+
"-e",
|
|
118
|
+
"PGDATABASE",
|
|
119
|
+
"-e",
|
|
120
|
+
"PGUSER",
|
|
121
|
+
"-e",
|
|
122
|
+
"PGPASSWORD",
|
|
123
|
+
"-e",
|
|
124
|
+
"PGSSLMODE",
|
|
125
|
+
image,
|
|
126
|
+
"pg_dump",
|
|
127
|
+
...pgDumpArgs(),
|
|
53
128
|
],
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
env: {
|
|
57
|
-
...buildTemplateExecutionEnv(config, {}, process.env),
|
|
58
|
-
DATABASE_URL: templateDbUrl,
|
|
59
|
-
},
|
|
60
|
-
stdout: "pipe",
|
|
61
|
-
stderr: "pipe",
|
|
62
|
-
reject: false,
|
|
63
|
-
}
|
|
129
|
+
env,
|
|
130
|
+
options
|
|
64
131
|
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function runPgDumpCommand(config, command, args, env, options = {}) {
|
|
135
|
+
const child = execa(command, args, {
|
|
136
|
+
cwd: config.productDir,
|
|
137
|
+
env,
|
|
138
|
+
stdout: "pipe",
|
|
139
|
+
stderr: "pipe",
|
|
140
|
+
reject: false,
|
|
141
|
+
});
|
|
65
142
|
const liveWriter =
|
|
66
143
|
options.reporter?.outputMode === "debug"
|
|
67
144
|
? (line) => options.reporter.writeDebugLine?.(line)
|
|
68
145
|
: null;
|
|
69
146
|
const logRecord = options.logRecord || null;
|
|
70
147
|
const drains = [
|
|
71
|
-
captureOutput(child.stdout, {
|
|
72
|
-
livePrefix: `[${config.runtimeLabel || config.name}:${config.name}]`,
|
|
73
|
-
liveWriter,
|
|
74
|
-
onLine(line) {
|
|
75
|
-
if (logRecord) logRecord.stream.write(`${new Date().toISOString()} [stdout] ${line}\n`);
|
|
76
|
-
},
|
|
77
|
-
}),
|
|
78
148
|
captureOutput(child.stderr, {
|
|
79
149
|
livePrefix: `[${config.runtimeLabel || config.name}:${config.name}]`,
|
|
80
150
|
liveWriter,
|
|
@@ -85,29 +155,79 @@ export async function captureTemplateSnapshot(config, outputPath, databaseUrl, o
|
|
|
85
155
|
];
|
|
86
156
|
const result = await child;
|
|
87
157
|
await Promise.all(drains);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isPgDumpServerVersionMismatch(result) {
|
|
162
|
+
const text = `${result.stderr || ""}\n${result.stdout || ""}`;
|
|
163
|
+
return text.includes("server version mismatch");
|
|
164
|
+
}
|
|
91
165
|
|
|
92
|
-
|
|
93
|
-
|
|
166
|
+
function parsePgDumpServerMajor(result) {
|
|
167
|
+
const text = `${result.stderr || ""}\n${result.stdout || ""}`;
|
|
168
|
+
const match = text.match(/server version:\s*([0-9]+)/i);
|
|
169
|
+
return match ? Number(match[1]) : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildPostgresConnectionEnv(databaseUrl) {
|
|
173
|
+
const parsed = new URL(databaseUrl);
|
|
174
|
+
return compactObject({
|
|
175
|
+
PGHOST: parsed.hostname,
|
|
176
|
+
PGPORT: parsed.port || "5432",
|
|
177
|
+
PGDATABASE: decodeURIComponent(parsed.pathname.replace(/^\//, "")),
|
|
178
|
+
PGUSER: decodeURIComponent(parsed.username || ""),
|
|
179
|
+
PGPASSWORD: decodeURIComponent(parsed.password || ""),
|
|
180
|
+
PGSSLMODE: parsed.searchParams.get("sslmode") || undefined,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function compactObject(value) {
|
|
185
|
+
return Object.fromEntries(
|
|
186
|
+
Object.entries(value).filter(([_key, entry]) => entry !== undefined && entry !== null && entry !== "")
|
|
187
|
+
);
|
|
94
188
|
}
|
|
95
189
|
|
|
96
190
|
export function sanitizeSnapshotFile(filePath) {
|
|
97
191
|
const dump = fs.readFileSync(filePath, "utf8");
|
|
98
|
-
const sanitized = dump
|
|
99
|
-
|
|
192
|
+
const sanitized = sanitizeSnapshotText(dump);
|
|
193
|
+
|
|
194
|
+
if (sanitized !== dump) {
|
|
195
|
+
fs.writeFileSync(filePath, sanitized);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function sanitizeSnapshotText(dump) {
|
|
200
|
+
return removePublicSchemaInitdbBlock(String(dump).split("\n"))
|
|
100
201
|
.filter((line) => {
|
|
101
202
|
const trimmed = line.trim();
|
|
102
203
|
return (
|
|
204
|
+
!trimmed.startsWith("-- Dumped from database version ") &&
|
|
205
|
+
!trimmed.startsWith("-- Dumped by pg_dump version ") &&
|
|
103
206
|
trimmed !== "SET transaction_timeout = 0;" &&
|
|
104
207
|
!trimmed.startsWith("\\restrict ") &&
|
|
105
208
|
!trimmed.startsWith("\\unrestrict ")
|
|
106
209
|
);
|
|
107
210
|
})
|
|
108
211
|
.join("\n");
|
|
212
|
+
}
|
|
109
213
|
|
|
110
|
-
|
|
111
|
-
|
|
214
|
+
function removePublicSchemaInitdbBlock(lines) {
|
|
215
|
+
const normalized = [];
|
|
216
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
217
|
+
if (lines[index]?.trim() === "--" && lines[index + 1]?.startsWith("-- Name: public; Type: SCHEMA;")) {
|
|
218
|
+
let cursor = index + 1;
|
|
219
|
+
while (cursor < lines.length && !lines[cursor]?.includes("*not* creating schema")) {
|
|
220
|
+
cursor += 1;
|
|
221
|
+
}
|
|
222
|
+
if (cursor < lines.length) {
|
|
223
|
+
while (cursor + 1 < lines.length && lines[cursor + 1]?.trim() === "") {
|
|
224
|
+
cursor += 1;
|
|
225
|
+
}
|
|
226
|
+
index = cursor;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
normalized.push(lines[index]);
|
|
112
231
|
}
|
|
232
|
+
return normalized;
|
|
113
233
|
}
|
|
@@ -146,9 +146,10 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
146
146
|
runtimeOptions: {
|
|
147
147
|
reporter,
|
|
148
148
|
logRegistry,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
149
|
+
setupRegistry,
|
|
150
|
+
skipSchemaSourceVerify: opts.skipSchemaSourceVerify,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
152
153
|
const timingUpdates = [];
|
|
153
154
|
|
|
154
155
|
try {
|
|
@@ -51,13 +51,16 @@ export async function runConfiguredSteps({
|
|
|
51
51
|
reporter = null,
|
|
52
52
|
setupRegistry = null,
|
|
53
53
|
parentOperation = null,
|
|
54
|
+
startIndex = 0,
|
|
55
|
+
afterStep = null,
|
|
54
56
|
}) {
|
|
55
57
|
if (steps.length === 0) return;
|
|
56
58
|
const resolvedToolchain = await resolveConfiguredToolchain(config);
|
|
57
59
|
await announceResolvedToolchain(config, resolvedToolchain, reporter);
|
|
58
60
|
|
|
59
61
|
for (const [index, step] of steps.entries()) {
|
|
60
|
-
const
|
|
62
|
+
const stepNumber = startIndex + index + 1;
|
|
63
|
+
const label = `${labelPrefix}:${config.name}:${stepNumber}`;
|
|
61
64
|
const stepOperation = setupRegistry?.start({
|
|
62
65
|
config,
|
|
63
66
|
stage: label,
|
|
@@ -89,6 +92,14 @@ export async function runConfiguredSteps({
|
|
|
89
92
|
if (finished) reporter?.setupOperationFinished?.(finished);
|
|
90
93
|
throw error;
|
|
91
94
|
}
|
|
95
|
+
if (afterStep) {
|
|
96
|
+
await afterStep({
|
|
97
|
+
step,
|
|
98
|
+
index: startIndex + index,
|
|
99
|
+
stepNumber,
|
|
100
|
+
label,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
|
package/lib/runner/template.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
finalizeConfiguredInputs,
|
|
4
|
+
finalizeConfiguredSteps,
|
|
5
|
+
} from "../shared/configured-steps.mjs";
|
|
3
6
|
import { readDatabaseInfo } from "./state-io.mjs";
|
|
4
7
|
|
|
5
8
|
const PORT_STRIDE = 100;
|
|
@@ -135,6 +138,7 @@ export function resolveRuntimeConfig(
|
|
|
135
138
|
const database = config.testkit.database
|
|
136
139
|
? {
|
|
137
140
|
...config.testkit.database,
|
|
141
|
+
sourceSchema: finalizeSourceSchema(config.testkit.database.sourceSchema, context),
|
|
138
142
|
template: finalizeDatabaseTemplate(config.testkit.database.template, context),
|
|
139
143
|
}
|
|
140
144
|
: undefined;
|
|
@@ -193,6 +197,17 @@ function finalizeDatabaseTemplate(template, context) {
|
|
|
193
197
|
};
|
|
194
198
|
}
|
|
195
199
|
|
|
200
|
+
function finalizeSourceSchema(sourceSchema, context) {
|
|
201
|
+
if (!sourceSchema) return null;
|
|
202
|
+
return {
|
|
203
|
+
...sourceSchema,
|
|
204
|
+
...(typeof sourceSchema.env === "string" ? { env: finalizeString(sourceSchema.env, context) } : {}),
|
|
205
|
+
...(typeof sourceSchema.cachePath === "string"
|
|
206
|
+
? { cachePath: finalizeString(sourceSchema.cachePath, context) }
|
|
207
|
+
: {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
196
211
|
export function resolveServiceStateDir(runtimeDir, config) {
|
|
197
212
|
return path.join(runtimeDir, "services", config.name);
|
|
198
213
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.116",
|
|
4
4
|
"description": "Browser bridge helpers for testkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@elench/testkit-protocol": "0.1.
|
|
25
|
+
"@elench/testkit-protocol": "0.1.116"
|
|
26
26
|
},
|
|
27
27
|
"private": false
|
|
28
28
|
}
|