@meshxdata/fops 0.1.48 → 0.1.50
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/CHANGELOG.md +368 -0
- package/package.json +1 -1
- package/src/commands/lifecycle.js +30 -11
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-core.js +347 -6
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-data-bootstrap.js +421 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-flux.js +5 -179
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-naming.js +14 -4
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-postgres.js +171 -4
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-storage.js +303 -8
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks.js +2 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-auth.js +1 -1
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet-swarm.js +936 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet.js +10 -918
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +5 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-keyvault-keys.js +413 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-keyvault.js +14 -399
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-config.js +754 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-knock.js +527 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-ssh.js +427 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +99 -1686
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-health.js +279 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-init.js +186 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +66 -444
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-results.js +11 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-vm-lifecycle.js +5 -540
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-vm-terraform.js +544 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/infra-cmds.js +75 -3
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +227 -11
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +2 -1
- package/src/plugins/bundled/fops-plugin-azure/lib/pytest-parse.js +21 -0
- package/src/plugins/bundled/fops-plugin-foundation/index.js +309 -44
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { resolveRemoteAuth, suppressTlsWarning } from "../azure-auth.js";
|
|
6
|
-
import { parsePytestSummary, parsePytestDurations } from "../pytest-parse.js";
|
|
6
|
+
import { parsePytestSummary, parsePytestDurations, parsePytestFailedTests } from "../pytest-parse.js";
|
|
7
7
|
|
|
8
8
|
export function registerTestCommands(azure) {
|
|
9
9
|
const test = azure
|
|
@@ -89,6 +89,39 @@ export function registerTestCommands(azure) {
|
|
|
89
89
|
} catch { /* optional — tests still run without it */ }
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Fetch QA_TEST_USER_PASSWORD for role-based test accounts
|
|
93
|
+
// Priority: local .env file → process.env → ~/.fops.json → remote VM .env
|
|
94
|
+
let qaTestUserPassword = "";
|
|
95
|
+
try {
|
|
96
|
+
const localEnv = await fsp.readFile(localEnvPath, "utf8");
|
|
97
|
+
const pwMatch = localEnv.match(/^QA_TEST_USER_PASSWORD=(.+)$/m);
|
|
98
|
+
if (pwMatch) qaTestUserPassword = pwMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
99
|
+
} catch { /* no local .env */ }
|
|
100
|
+
if (!qaTestUserPassword) {
|
|
101
|
+
qaTestUserPassword = process.env.QA_TEST_USER_PASSWORD || "";
|
|
102
|
+
}
|
|
103
|
+
if (!qaTestUserPassword) {
|
|
104
|
+
try {
|
|
105
|
+
const os = await import("node:os");
|
|
106
|
+
const fopsJson = JSON.parse(await fsp.readFile(path.join(os.homedir(), ".fops.json"), "utf8"));
|
|
107
|
+
qaTestUserPassword = fopsJson?.plugins?.entries?.["fops-plugin-foundation"]?.config?.qaTestUserPassword || "";
|
|
108
|
+
} catch { /* no ~/.fops.json */ }
|
|
109
|
+
}
|
|
110
|
+
if (!qaTestUserPassword && ip) {
|
|
111
|
+
try {
|
|
112
|
+
const sshUser = state?.adminUser || "azureuser";
|
|
113
|
+
const { stdout: pwOut } = await sshCmd(execa, ip, sshUser,
|
|
114
|
+
"grep -E '^QA_TEST_USER_PASSWORD=' /opt/foundation-compose/.env",
|
|
115
|
+
10_000,
|
|
116
|
+
);
|
|
117
|
+
const pwMatch = (pwOut || "").match(/^QA_TEST_USER_PASSWORD=(.+)$/m);
|
|
118
|
+
if (pwMatch) {
|
|
119
|
+
qaTestUserPassword = pwMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
120
|
+
console.log(chalk.green(" ✓ Got QA_TEST_USER_PASSWORD from VM"));
|
|
121
|
+
}
|
|
122
|
+
} catch { /* optional */ }
|
|
123
|
+
}
|
|
124
|
+
|
|
92
125
|
if (!bearerToken && !qaUser) {
|
|
93
126
|
console.error(chalk.red("\n No credentials found (local or remote)."));
|
|
94
127
|
console.error(chalk.dim(" Set BEARER_TOKEN or QA_USERNAME/QA_PASSWORD, or ensure the VM has Auth0 configured in .env\n"));
|
|
@@ -124,7 +157,7 @@ export function registerTestCommands(azure) {
|
|
|
124
157
|
envContent = setVar(envContent, "ADMIN_PASSWORD", qaPass);
|
|
125
158
|
envContent = setVar(envContent, "ADMIN_X_ACCOUNT", "root");
|
|
126
159
|
envContent = setVar(envContent, "OWNER_EMAIL", qaUser);
|
|
127
|
-
envContent = setVar(envContent, "OWNER_NAME", "Foundation Operator");
|
|
160
|
+
envContent = setVar(envContent, "OWNER_NAME", '"Foundation Operator"');
|
|
128
161
|
if (bearerToken) {
|
|
129
162
|
envContent = setVar(envContent, "BEARER_TOKEN", bearerToken);
|
|
130
163
|
envContent = setVar(envContent, "TOKEN_AUTH0", bearerToken);
|
|
@@ -133,6 +166,15 @@ export function registerTestCommands(azure) {
|
|
|
133
166
|
envContent = setVar(envContent, "CF_ACCESS_CLIENT_ID", cfAccessClientId);
|
|
134
167
|
envContent = setVar(envContent, "CF_ACCESS_CLIENT_SECRET", cfAccessClientSecret);
|
|
135
168
|
}
|
|
169
|
+
if (qaTestUserPassword) {
|
|
170
|
+
envContent = setVar(envContent, "QA_TEST_USER_PASSWORD", `'${qaTestUserPassword}'`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Set URN values for policy creation (required by setup_user_role.py)
|
|
174
|
+
envContent = setVar(envContent, "ORG_ROOT_RESOURCE_PREFIX", "urn:meshx::data:root");
|
|
175
|
+
envContent = setVar(envContent, "ORG_A_RESOURCE_PREFIX", "urn:meshx::data:khoa");
|
|
176
|
+
envContent = setVar(envContent, "ORG_ROOT_URN", "urn:meshx::iam:root:organization:root");
|
|
177
|
+
envContent = setVar(envContent, "ORG_A_URN", "urn:meshx::iam:root:organization:khoa");
|
|
136
178
|
|
|
137
179
|
await fsp.writeFile(envPath, envContent);
|
|
138
180
|
console.log(chalk.green(` ✓ Configured QA .env → ${apiUrl}`));
|
|
@@ -151,16 +193,13 @@ export function registerTestCommands(azure) {
|
|
|
151
193
|
|
|
152
194
|
const authMode = useTokenMode ? "bearer token (--use-token)" : `user/pass (${qaUser})`;
|
|
153
195
|
console.log(chalk.cyan(`\n Running QA tests against ${state.vmName} (${vmUrl}) [${authMode}]…\n`));
|
|
154
|
-
let setupCmd = `./scripts/set_up_role.sh --env ${targetName}`;
|
|
155
|
-
let runCmd = `./scripts/run_all_tests.sh --env ${targetName} --role-priority p0`;
|
|
156
196
|
|
|
197
|
+
// Run pytest directly - role tests with P0 priority
|
|
198
|
+
let pytestArgs = `pytest tests/roles --env ${targetName} -v -x --tb=short`;
|
|
157
199
|
if (useTokenMode) {
|
|
158
|
-
|
|
159
|
-
runCmd += " --use-token";
|
|
200
|
+
pytestArgs += " --use-token";
|
|
160
201
|
}
|
|
161
202
|
|
|
162
|
-
const pytestArgs = `${setupCmd} && ${runCmd}`;
|
|
163
|
-
|
|
164
203
|
const testEnv = {
|
|
165
204
|
...process.env,
|
|
166
205
|
API_URL: apiUrl,
|
|
@@ -192,7 +231,7 @@ export function registerTestCommands(azure) {
|
|
|
192
231
|
const proc = execa(
|
|
193
232
|
"bash",
|
|
194
233
|
["-c", `source venv/bin/activate && ${pytestArgs}`],
|
|
195
|
-
{ cwd: qaDir, timeout:
|
|
234
|
+
{ cwd: qaDir, timeout: 1_800_000, reject: false, env: testEnv }, // 30 min timeout
|
|
196
235
|
);
|
|
197
236
|
let captured = "";
|
|
198
237
|
proc.stdout?.on("data", (d) => { const s = d.toString(); captured += s; process.stdout.write(s); });
|
|
@@ -203,6 +242,7 @@ export function registerTestCommands(azure) {
|
|
|
203
242
|
|
|
204
243
|
const counts = parsePytestSummary(captured);
|
|
205
244
|
const timing = parsePytestDurations(captured);
|
|
245
|
+
const failedTests = parsePytestFailedTests(captured);
|
|
206
246
|
const { writeVmState } = await import("../azure-state.js");
|
|
207
247
|
const qaResult = {
|
|
208
248
|
passed: actualExit === 0,
|
|
@@ -215,14 +255,35 @@ export function registerTestCommands(azure) {
|
|
|
215
255
|
...(counts.skipped != null && { numSkipped: counts.skipped }),
|
|
216
256
|
durationSec: counts.durationSec || wallSec,
|
|
217
257
|
...(timing && { timing }),
|
|
258
|
+
...(failedTests.length > 0 && { failedTests: failedTests.slice(0, 50) }),
|
|
218
259
|
};
|
|
219
260
|
writeVmState(state.vmName, { qa: qaResult });
|
|
220
261
|
|
|
221
262
|
if (actualExit === 0) {
|
|
222
263
|
console.log(chalk.green("\n ✓ QA tests passed\n"));
|
|
223
264
|
} else {
|
|
224
|
-
console.error(chalk.red(`\n QA tests failed (exit ${actualExit})
|
|
225
|
-
|
|
265
|
+
console.error(chalk.red(`\n ✗ QA tests failed (exit ${actualExit})`));
|
|
266
|
+
|
|
267
|
+
// Parse and display failed tests summary
|
|
268
|
+
const failedTests = parsePytestFailedTests(captured);
|
|
269
|
+
if (failedTests.length > 0) {
|
|
270
|
+
console.error(chalk.red(`\n Failed tests (${failedTests.length}):`));
|
|
271
|
+
for (const { test, reason } of failedTests.slice(0, 20)) {
|
|
272
|
+
const shortTest = test.replace(/^tests\//, "");
|
|
273
|
+
const reasonStr = reason ? chalk.dim(` - ${reason.slice(0, 60)}`) : "";
|
|
274
|
+
console.error(chalk.red(` • ${shortTest}${reasonStr}`));
|
|
275
|
+
}
|
|
276
|
+
if (failedTests.length > 20) {
|
|
277
|
+
console.error(chalk.dim(` ... and ${failedTests.length - 20} more`));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Show summary counts
|
|
282
|
+
if (counts.passed != null || counts.failed != null) {
|
|
283
|
+
console.error(chalk.dim(`\n Summary: ${counts.passed || 0} passed, ${counts.failed || 0} failed, ${counts.skipped || 0} skipped`));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.error(chalk.dim(`\n Report: ${path.join(qaDir, "playwright-report")}\n`));
|
|
226
287
|
process.exitCode = 1;
|
|
227
288
|
}
|
|
228
289
|
|
|
@@ -251,6 +312,161 @@ export function registerTestCommands(azure) {
|
|
|
251
312
|
await resultsSetup({ account: opts.account });
|
|
252
313
|
});
|
|
253
314
|
|
|
315
|
+
test
|
|
316
|
+
.command("setup-users [name]")
|
|
317
|
+
.description("Create/update QA test users on target environment")
|
|
318
|
+
.option("--vm-name <name>", "Target VM (default: active)")
|
|
319
|
+
.option("--env <name>", "Target environment name (staging, dev, etc.)")
|
|
320
|
+
.action(async (name, opts) => {
|
|
321
|
+
const { resolveCliSrc, lazyExeca, ensureAzCli, ensureAzAuth, resolvePublicIp } = await import("../azure-helpers.js");
|
|
322
|
+
const { requireVmState, knockForVm, sshCmd } = await import("../azure.js");
|
|
323
|
+
const { rootDir } = await import(resolveCliSrc("project.js"));
|
|
324
|
+
const { resolveRemoteAuth, suppressTlsWarning } = await import("../azure-auth.js");
|
|
325
|
+
const fsp = await import("node:fs/promises");
|
|
326
|
+
const path = await import("node:path");
|
|
327
|
+
|
|
328
|
+
const root = rootDir();
|
|
329
|
+
if (!root) {
|
|
330
|
+
console.error(chalk.red("\n Foundation project root not found.\n"));
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const qaDir = path.join(root, "foundation-qa-automation");
|
|
335
|
+
try {
|
|
336
|
+
await fsp.access(qaDir);
|
|
337
|
+
} catch {
|
|
338
|
+
console.error(chalk.red("\n foundation-qa-automation/ not found."));
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const execa = await lazyExeca();
|
|
343
|
+
let envName = opts.env;
|
|
344
|
+
let apiUrl;
|
|
345
|
+
let bearerToken;
|
|
346
|
+
|
|
347
|
+
if (!envName) {
|
|
348
|
+
// Resolve from VM
|
|
349
|
+
await ensureAzCli(execa);
|
|
350
|
+
await ensureAzAuth(execa);
|
|
351
|
+
const state = requireVmState(opts.vmName || name);
|
|
352
|
+
const ip = await resolvePublicIp(execa, state.resourceGroup, state.vmName, state.publicIp);
|
|
353
|
+
if (!ip) {
|
|
354
|
+
console.error(chalk.red("\n No IP address. Is the VM running?\n"));
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
const vmUrl = state.publicUrl || `https://${ip}`;
|
|
358
|
+
apiUrl = `${vmUrl}/api`;
|
|
359
|
+
envName = opts.vmName || name || state.vmName;
|
|
360
|
+
|
|
361
|
+
const auth = await resolveRemoteAuth({
|
|
362
|
+
apiUrl, ip, vmState: state,
|
|
363
|
+
execaFn: execa, sshCmd, knockForVm, suppressTlsWarning,
|
|
364
|
+
});
|
|
365
|
+
bearerToken = auth.bearerToken;
|
|
366
|
+
} else {
|
|
367
|
+
// Use named environment - authenticate via Auth0 ROPC
|
|
368
|
+
const envUrls = {
|
|
369
|
+
staging: "https://staging.meshx.app/api",
|
|
370
|
+
dev: "https://dev.meshx.app/api",
|
|
371
|
+
demo: "https://demo.meshx.app/api",
|
|
372
|
+
integration: "https://integration.meshx.app/api",
|
|
373
|
+
};
|
|
374
|
+
apiUrl = envUrls[envName] || `https://${envName}.meshx.app/api`;
|
|
375
|
+
|
|
376
|
+
// Try to get bearer token for named environment
|
|
377
|
+
const { resolveFoundationCreds, resolveAuth0Config, isJwt, isJwtExpired } = await import("../azure-auth.js");
|
|
378
|
+
const creds = resolveFoundationCreds();
|
|
379
|
+
|
|
380
|
+
// Check for existing valid bearer token
|
|
381
|
+
if (creds?.bearerToken && isJwt(creds.bearerToken) && !isJwtExpired(creds.bearerToken)) {
|
|
382
|
+
bearerToken = creds.bearerToken;
|
|
383
|
+
console.log(chalk.green(" ✓ Using existing bearer token"));
|
|
384
|
+
} else if (creds?.user && creds?.password) {
|
|
385
|
+
// Authenticate via Auth0 ROPC
|
|
386
|
+
const auth0Cfg = resolveAuth0Config();
|
|
387
|
+
if (auth0Cfg) {
|
|
388
|
+
console.log(chalk.dim(` Authenticating via Auth0 (${auth0Cfg.domain})…`));
|
|
389
|
+
const body = {
|
|
390
|
+
grant_type: "password",
|
|
391
|
+
client_id: auth0Cfg.clientId,
|
|
392
|
+
username: creds.user,
|
|
393
|
+
password: creds.password,
|
|
394
|
+
scope: "openid",
|
|
395
|
+
};
|
|
396
|
+
if (auth0Cfg.clientSecret) body.client_secret = auth0Cfg.clientSecret;
|
|
397
|
+
if (auth0Cfg.audience) body.audience = auth0Cfg.audience;
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const resp = await fetch(`https://${auth0Cfg.domain}/oauth/token`, {
|
|
401
|
+
method: "POST",
|
|
402
|
+
headers: { "Content-Type": "application/json" },
|
|
403
|
+
body: JSON.stringify(body),
|
|
404
|
+
signal: AbortSignal.timeout(15_000),
|
|
405
|
+
});
|
|
406
|
+
if (resp.ok) {
|
|
407
|
+
const data = await resp.json();
|
|
408
|
+
if (data.access_token) {
|
|
409
|
+
bearerToken = data.access_token;
|
|
410
|
+
console.log(chalk.green(` ✓ Authenticated as ${creds.user}`));
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
const errText = await resp.text().catch(() => "");
|
|
414
|
+
console.error(chalk.yellow(` ⚠ Auth0 rejected credentials: HTTP ${resp.status}`));
|
|
415
|
+
if (errText) console.error(chalk.dim(` ${errText.slice(0, 200)}`));
|
|
416
|
+
}
|
|
417
|
+
} catch (e) {
|
|
418
|
+
console.error(chalk.yellow(` ⚠ Auth0 auth failed: ${e.message}`));
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
console.error(chalk.yellow(" ⚠ No Auth0 config found (check .env or ~/.fops.json)"));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!bearerToken) {
|
|
426
|
+
console.error(chalk.red("\n No bearer token available for named environment."));
|
|
427
|
+
console.error(chalk.dim(" Set BEARER_TOKEN env var or ensure QA_USERNAME/QA_PASSWORD + Auth0 config are set.\n"));
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
console.log(chalk.cyan(`\n Setting up QA test users on ${envName} (${apiUrl})…\n`));
|
|
433
|
+
|
|
434
|
+
// Ensure venv
|
|
435
|
+
try {
|
|
436
|
+
await fsp.access(path.join(qaDir, "venv"));
|
|
437
|
+
} catch {
|
|
438
|
+
console.log(chalk.cyan(" Setting up QA automation environment…"));
|
|
439
|
+
await execa("python3", ["-m", "venv", "venv"], { cwd: qaDir, stdio: "inherit" });
|
|
440
|
+
await execa("bash", ["-c", "source venv/bin/activate && pip install -r requirements.txt"], { cwd: qaDir, stdio: "inherit" });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const setupEnv = {
|
|
444
|
+
...process.env,
|
|
445
|
+
TARGET_ENV: envName,
|
|
446
|
+
USE_TOKEN: bearerToken ? "1" : "0",
|
|
447
|
+
API_URL: apiUrl,
|
|
448
|
+
};
|
|
449
|
+
if (bearerToken) {
|
|
450
|
+
setupEnv.TOKEN_AUTH0 = bearerToken;
|
|
451
|
+
setupEnv.BEARER_TOKEN = bearerToken;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Use the shell script which handles all setup
|
|
455
|
+
const useTokenFlag = bearerToken ? "--use-token" : "";
|
|
456
|
+
const { exitCode } = await execa(
|
|
457
|
+
"bash",
|
|
458
|
+
["scripts/set_up_user_role.sh", "--env", envName, useTokenFlag].filter(Boolean),
|
|
459
|
+
{ cwd: qaDir, stdio: "inherit", env: setupEnv, reject: false, timeout: 300_000 },
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if (exitCode === 0) {
|
|
463
|
+
console.log(chalk.green("\n ✓ QA test users created/updated\n"));
|
|
464
|
+
} else {
|
|
465
|
+
console.error(chalk.red(`\n ✗ User setup failed (exit ${exitCode})\n`));
|
|
466
|
+
process.exitCode = 1;
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
254
470
|
test
|
|
255
471
|
.command("list [target]")
|
|
256
472
|
.description("List stored test results across VMs (optionally filter by name)")
|
|
@@ -166,9 +166,10 @@ export function registerVmCommands(azure, api, registry) {
|
|
|
166
166
|
.option("--cost", "Show estimated cost per resource (queries Azure Cost Management)")
|
|
167
167
|
.option("--days <days>", "Days to look back for cost (default: 30)", "30")
|
|
168
168
|
.option("--versions", "Show service image version matrix")
|
|
169
|
+
.option("--json", "Output as JSON (for programmatic use)")
|
|
169
170
|
.action(async (opts) => {
|
|
170
171
|
const { azureList } = await import("../azure.js");
|
|
171
|
-
await azureList({ live: opts.live, verbose: opts.verbose, cost: opts.cost, days: parseInt(opts.days), versions: opts.versions });
|
|
172
|
+
await azureList({ live: opts.live, verbose: opts.verbose, cost: opts.cost, days: parseInt(opts.days), versions: opts.versions, json: opts.json });
|
|
172
173
|
});
|
|
173
174
|
|
|
174
175
|
// ── ip ─────────────────────────────────────────────────────────────────
|
|
@@ -25,6 +25,27 @@ export function parsePytestSummary(output) {
|
|
|
25
25
|
return counts;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export function parsePytestFailedTests(output) {
|
|
29
|
+
const failed = [];
|
|
30
|
+
const lines = output.split("\n");
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
// Match "FAILED tests/path/test_file.py::test_name - reason"
|
|
34
|
+
const failMatch = line.match(/^FAILED\s+(\S+)(?:\s+-\s+(.*))?$/);
|
|
35
|
+
if (failMatch) {
|
|
36
|
+
failed.push({ test: failMatch[1], reason: failMatch[2] || "" });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// Match verbose format "tests/path::test_name FAILED"
|
|
40
|
+
const vMatch = line.match(/^(\S+::\S+)\s+FAILED/);
|
|
41
|
+
if (vMatch) {
|
|
42
|
+
failed.push({ test: vMatch[1], reason: "" });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return failed;
|
|
47
|
+
}
|
|
48
|
+
|
|
28
49
|
export function parsePytestDurations(output) {
|
|
29
50
|
const durations = [];
|
|
30
51
|
const lines = output.split("\n");
|