@elench/testkit 0.1.52 → 0.1.54
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 +14 -0
- package/bin/testkit.mjs +4 -6
- package/lib/cli/command-helpers.mjs +170 -0
- package/lib/cli/commands/artifacts.mjs +45 -0
- package/lib/cli/commands/cleanup.mjs +15 -0
- package/lib/cli/commands/db/snapshot/capture.mjs +22 -0
- package/lib/cli/commands/destroy.mjs +15 -0
- package/lib/cli/commands/known-failures/render.mjs +19 -0
- package/lib/cli/commands/known-failures/validate.mjs +20 -0
- package/lib/cli/commands/logs.mjs +47 -0
- package/lib/cli/commands/run.mjs +23 -0
- package/lib/cli/commands/show.mjs +47 -0
- package/lib/cli/commands/status.mjs +15 -0
- package/lib/cli/commands/watch.mjs +23 -0
- package/lib/cli/entrypoint.mjs +83 -0
- package/lib/cli/index.mjs +6 -116
- package/lib/cli/presentation/code-frames.mjs +57 -0
- package/lib/cli/presentation/code-frames.test.mjs +71 -0
- package/lib/cli/presentation/colors.mjs +29 -0
- package/lib/cli/presentation/run-reporter.mjs +100 -0
- package/lib/cli/tui/watch-app.mjs +104 -0
- package/lib/cli/viewer.mjs +268 -0
- package/lib/known-failures/index.mjs +1 -1
- package/lib/known-failures/index.test.mjs +46 -0
- package/lib/runner/artifacts.mjs +35 -0
- package/lib/runner/default-runtime-errors.mjs +66 -0
- package/lib/runner/default-runtime-runner.mjs +52 -11
- package/lib/runner/failure-details.mjs +31 -0
- package/lib/runner/failure-details.test.mjs +51 -0
- package/lib/runner/formatting.mjs +207 -0
- package/lib/runner/formatting.test.mjs +81 -6
- package/lib/runner/logs.mjs +89 -0
- package/lib/runner/orchestrator.mjs +51 -20
- package/lib/runner/playwright-runner.mjs +15 -7
- package/lib/runner/processes.mjs +9 -11
- package/lib/runner/reporting.mjs +5 -1
- package/lib/runner/reporting.test.mjs +4 -1
- package/lib/runner/runtime-contexts.mjs +7 -3
- package/lib/runner/runtime-manager.mjs +8 -2
- package/lib/runner/runtime-preparation.mjs +9 -4
- package/lib/runner/services.mjs +25 -8
- package/lib/runner/template-steps.mjs +4 -3
- package/lib/runner/triage.mjs +67 -0
- package/lib/runner/worker-loop.mjs +8 -7
- package/lib/runtime/index.d.ts +60 -0
- package/lib/runtime/index.mjs +12 -0
- package/lib/runtime-src/k6/checks.js +45 -12
- package/lib/runtime-src/k6/http-assertions.js +214 -0
- package/lib/runtime-src/k6/http.js +261 -13
- package/lib/runtime-src/k6/suite.js +46 -1
- package/lib/toolchains/index.mjs +6 -3
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -47,6 +47,14 @@ npx @elench/testkit --type int --write-status
|
|
|
47
47
|
# Lifecycle
|
|
48
48
|
npx @elench/testkit status
|
|
49
49
|
npx @elench/testkit destroy
|
|
50
|
+
npx @elench/testkit cleanup
|
|
51
|
+
|
|
52
|
+
# Inspect the latest run artifact
|
|
53
|
+
npx @elench/testkit show
|
|
54
|
+
npx @elench/testkit show __testkit__/health/health.int.testkit.ts
|
|
55
|
+
npx @elench/testkit artifacts __testkit__/health/health.int.testkit.ts
|
|
56
|
+
npx @elench/testkit logs __testkit__/health/health.int.testkit.ts
|
|
57
|
+
npx @elench/testkit watch
|
|
50
58
|
|
|
51
59
|
# Known-failures tooling
|
|
52
60
|
npx @elench/testkit known-failures validate --issue-mode error
|
|
@@ -56,6 +64,12 @@ npx @elench/testkit known-failures render --output KNOWN_FAILURES.md
|
|
|
56
64
|
npx @elench/testkit db snapshot capture --service api --output scripts/testkit/schema-baseline.sql
|
|
57
65
|
```
|
|
58
66
|
|
|
67
|
+
`testkit` now keeps the default terminal output intentionally short: one line
|
|
68
|
+
per completed file, a concise failure block, and a final summary. Service logs,
|
|
69
|
+
captured runtime output, emitted artifacts, and user-visible LLM responses are
|
|
70
|
+
persisted under `.testkit/results/` and inspected on demand with `show`,
|
|
71
|
+
`artifacts`, `logs`, or `watch`.
|
|
72
|
+
|
|
59
73
|
## Setup
|
|
60
74
|
|
|
61
75
|
Create `testkit.setup.ts` at repo root:
|
package/bin/testkit.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { execute } from "@oclif/core";
|
|
3
|
+
import { normalizeCliArgs } from "../lib/cli/entrypoint.mjs";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
throw error;
|
|
7
|
-
});
|
|
8
|
-
});
|
|
5
|
+
const effectiveArgs = normalizeCliArgs(process.argv.slice(2));
|
|
6
|
+
await execute({ args: effectiveArgs, dir: import.meta.url });
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { Flags } from "@oclif/core";
|
|
3
|
+
import { loadConfigs } from "../config/index.mjs";
|
|
4
|
+
import {
|
|
5
|
+
parseFileTimeoutOption,
|
|
6
|
+
parseShardOption,
|
|
7
|
+
parseSuiteOption,
|
|
8
|
+
parseTypeOption,
|
|
9
|
+
parseWorkersOption,
|
|
10
|
+
resolveRequestedFiles,
|
|
11
|
+
} from "./args.mjs";
|
|
12
|
+
import * as runner from "../runner/index.mjs";
|
|
13
|
+
import { createRunReporter } from "./presentation/run-reporter.mjs";
|
|
14
|
+
|
|
15
|
+
export const sharedFlags = {
|
|
16
|
+
dir: Flags.string({
|
|
17
|
+
description: "Explicit product directory",
|
|
18
|
+
}),
|
|
19
|
+
service: Flags.string({
|
|
20
|
+
description: "Run or inspect only one service",
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const runFlags = {
|
|
25
|
+
...sharedFlags,
|
|
26
|
+
type: Flags.string({
|
|
27
|
+
char: "t",
|
|
28
|
+
multiple: true,
|
|
29
|
+
description: "Run specific suite type(s): int, e2e, dal, load, pw, all",
|
|
30
|
+
}),
|
|
31
|
+
suite: Flags.string({
|
|
32
|
+
char: "s",
|
|
33
|
+
multiple: true,
|
|
34
|
+
description: "Run specific suite(s)",
|
|
35
|
+
}),
|
|
36
|
+
file: Flags.string({
|
|
37
|
+
char: "f",
|
|
38
|
+
multiple: true,
|
|
39
|
+
description: "Run specific file(s)",
|
|
40
|
+
}),
|
|
41
|
+
workers: Flags.string({
|
|
42
|
+
description: "Number of test executors for the whole run",
|
|
43
|
+
}),
|
|
44
|
+
"file-timeout-seconds": Flags.string({
|
|
45
|
+
description: "Per-file wall-clock timeout in seconds",
|
|
46
|
+
}),
|
|
47
|
+
shard: Flags.string({
|
|
48
|
+
description: "Run only shard i of n at suite granularity",
|
|
49
|
+
}),
|
|
50
|
+
"write-status": Flags.boolean({
|
|
51
|
+
description: "Write a deterministic testkit.status.json snapshot",
|
|
52
|
+
default: false,
|
|
53
|
+
}),
|
|
54
|
+
"allow-partial-status": Flags.boolean({
|
|
55
|
+
description: "Allow --write-status for filtered runs",
|
|
56
|
+
default: false,
|
|
57
|
+
}),
|
|
58
|
+
"ignore-skip-rules": Flags.boolean({
|
|
59
|
+
description: "Run files even if testkit.setup.ts marks them skipped",
|
|
60
|
+
default: false,
|
|
61
|
+
}),
|
|
62
|
+
"output-mode": Flags.string({
|
|
63
|
+
description: "Reporter mode",
|
|
64
|
+
options: ["compact", "debug"],
|
|
65
|
+
}),
|
|
66
|
+
debug: Flags.boolean({
|
|
67
|
+
description: "Alias for --output-mode debug",
|
|
68
|
+
default: false,
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export async function resolveConfigsForCommand(flags) {
|
|
73
|
+
const allConfigs = await loadConfigs({ dir: flags.dir });
|
|
74
|
+
const configs = flags.service
|
|
75
|
+
? allConfigs.filter((config) => config.name === flags.service)
|
|
76
|
+
: allConfigs;
|
|
77
|
+
if (flags.service && configs.length === 0) {
|
|
78
|
+
const available = allConfigs.map((config) => config.name).join(", ");
|
|
79
|
+
throw new Error(`Service "${flags.service}" not found. Available: ${available}`);
|
|
80
|
+
}
|
|
81
|
+
return { allConfigs, configs };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function executeRunCommand(command, flags, positionalType = null) {
|
|
85
|
+
const { allConfigs, configs } = await resolveConfigsForCommand(flags);
|
|
86
|
+
const workers = flags.workers == null ? null : parseWorkersOption(flags.workers);
|
|
87
|
+
const fileTimeoutSeconds =
|
|
88
|
+
flags["file-timeout-seconds"] == null
|
|
89
|
+
? null
|
|
90
|
+
: parseFileTimeoutOption(flags["file-timeout-seconds"]);
|
|
91
|
+
const shard = parseShardOption(flags.shard);
|
|
92
|
+
const typeValues = parseTypeOption(flags.type, positionalType);
|
|
93
|
+
const suiteSelectors = parseSuiteOption(flags.suite);
|
|
94
|
+
const rawFileNames = Array.isArray(flags.file) ? flags.file : [flags.file].filter(Boolean);
|
|
95
|
+
const productDir = allConfigs[0]?.productDir || process.cwd();
|
|
96
|
+
const fileNames = resolveRequestedFiles(rawFileNames, productDir, process.cwd());
|
|
97
|
+
const outputMode = command.jsonEnabled()
|
|
98
|
+
? "json"
|
|
99
|
+
: flags.debug
|
|
100
|
+
? "debug"
|
|
101
|
+
: flags["output-mode"] || "compact";
|
|
102
|
+
const reporter = createRunReporter({ outputMode });
|
|
103
|
+
const result = await runner.runAll(
|
|
104
|
+
configs,
|
|
105
|
+
typeValues,
|
|
106
|
+
suiteSelectors,
|
|
107
|
+
{
|
|
108
|
+
...flags,
|
|
109
|
+
typeValues,
|
|
110
|
+
fileNames,
|
|
111
|
+
workers,
|
|
112
|
+
fileTimeoutSeconds,
|
|
113
|
+
shard,
|
|
114
|
+
serviceFilter: flags.service || null,
|
|
115
|
+
reporter,
|
|
116
|
+
writeStatus: flags["write-status"],
|
|
117
|
+
allowPartialStatus: flags["allow-partial-status"],
|
|
118
|
+
ignoreSkipRules: flags["ignore-skip-rules"],
|
|
119
|
+
},
|
|
120
|
+
allConfigs
|
|
121
|
+
);
|
|
122
|
+
return {
|
|
123
|
+
outputMode,
|
|
124
|
+
...result,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function runStatusLike(commandName, flags) {
|
|
129
|
+
const { allConfigs, configs } = await resolveConfigsForCommand(flags);
|
|
130
|
+
|
|
131
|
+
if (commandName === "cleanup") {
|
|
132
|
+
await runner.cleanup(allConfigs[0]?.productDir || process.cwd());
|
|
133
|
+
return { ok: true };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const productResults = [];
|
|
137
|
+
for (const config of configs) {
|
|
138
|
+
if (commandName === "status") {
|
|
139
|
+
productResults.push(runner.showStatus(config));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
await runner.destroy(config);
|
|
143
|
+
productResults.push({ name: config.name, destroyed: true });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { ok: true, results: productResults };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function makeKnownFailuresFlags() {
|
|
150
|
+
return {
|
|
151
|
+
...sharedFlags,
|
|
152
|
+
input: Flags.string({
|
|
153
|
+
description: "Known failures JSON path (repo-relative)",
|
|
154
|
+
}),
|
|
155
|
+
output: Flags.string({
|
|
156
|
+
description: "Output path",
|
|
157
|
+
}),
|
|
158
|
+
status: Flags.string({
|
|
159
|
+
description: "Status artifact path",
|
|
160
|
+
}),
|
|
161
|
+
"issue-mode": Flags.string({
|
|
162
|
+
description: "Issue validation mode override: off, warn, error",
|
|
163
|
+
options: ["off", "warn", "error"],
|
|
164
|
+
}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function relativeToProduct(productDir, targetPath) {
|
|
169
|
+
return path.relative(productDir, targetPath);
|
|
170
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Args, Command } from "@oclif/core";
|
|
2
|
+
import { sharedFlags } from "../command-helpers.mjs";
|
|
3
|
+
import { collectArtifactEntries, loadLatestRunArtifact } from "../viewer.mjs";
|
|
4
|
+
|
|
5
|
+
export default class ArtifactsCommand extends Command {
|
|
6
|
+
static summary = "List persisted artifacts from the latest run";
|
|
7
|
+
|
|
8
|
+
static enableJsonFlag = true;
|
|
9
|
+
|
|
10
|
+
static args = {
|
|
11
|
+
file: Args.string({
|
|
12
|
+
description: "Optional file path to filter artifacts",
|
|
13
|
+
required: false,
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
static flags = sharedFlags;
|
|
18
|
+
|
|
19
|
+
async run() {
|
|
20
|
+
const { args, flags } = await this.parse(ArtifactsCommand);
|
|
21
|
+
const productDir = flags.dir || process.cwd();
|
|
22
|
+
const runArtifact = loadLatestRunArtifact(productDir);
|
|
23
|
+
const entries = collectArtifactEntries(productDir, runArtifact, args.file || null, flags.service || null)
|
|
24
|
+
.map((entry) => ({
|
|
25
|
+
service: entry.service.name,
|
|
26
|
+
suite: `${entry.suite.type}:${entry.suite.name}`,
|
|
27
|
+
file: entry.file.path,
|
|
28
|
+
name: entry.artifactRef.name,
|
|
29
|
+
kind: entry.artifactRef.kind,
|
|
30
|
+
summary: entry.artifactRef.summary,
|
|
31
|
+
path: entry.artifactRef.path,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
if (!this.jsonEnabled()) {
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
this.log(`${entry.file}`);
|
|
37
|
+
this.log(` ${entry.name}${entry.kind ? ` [${entry.kind}]` : ""}`);
|
|
38
|
+
if (entry.summary) this.log(` ${entry.summary}`);
|
|
39
|
+
this.log(` ${entry.path}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return entries;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
import { runStatusLike, sharedFlags } from "../command-helpers.mjs";
|
|
3
|
+
|
|
4
|
+
export default class CleanupCommand extends Command {
|
|
5
|
+
static summary = "Clean stale local testkit state";
|
|
6
|
+
|
|
7
|
+
static enableJsonFlag = true;
|
|
8
|
+
|
|
9
|
+
static flags = sharedFlags;
|
|
10
|
+
|
|
11
|
+
async run() {
|
|
12
|
+
const { flags } = await this.parse(CleanupCommand);
|
|
13
|
+
return runStatusLike("cleanup", flags);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command, Flags } from "@oclif/core";
|
|
2
|
+
import { sharedFlags } from "../../../command-helpers.mjs";
|
|
3
|
+
import { runDatabaseSnapshotCaptureCommand } from "../../../db.mjs";
|
|
4
|
+
|
|
5
|
+
export default class DbSnapshotCaptureCommand extends Command {
|
|
6
|
+
static summary = "Capture a database schema snapshot";
|
|
7
|
+
|
|
8
|
+
static enableJsonFlag = true;
|
|
9
|
+
|
|
10
|
+
static flags = {
|
|
11
|
+
...sharedFlags,
|
|
12
|
+
output: Flags.string({
|
|
13
|
+
description: "Output path for the snapshot",
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async run() {
|
|
18
|
+
const { flags } = await this.parse(DbSnapshotCaptureCommand);
|
|
19
|
+
await runDatabaseSnapshotCaptureCommand(flags);
|
|
20
|
+
return { ok: true };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
import { runStatusLike, sharedFlags } from "../command-helpers.mjs";
|
|
3
|
+
|
|
4
|
+
export default class DestroyCommand extends Command {
|
|
5
|
+
static summary = "Destroy local testkit state";
|
|
6
|
+
|
|
7
|
+
static enableJsonFlag = true;
|
|
8
|
+
|
|
9
|
+
static flags = sharedFlags;
|
|
10
|
+
|
|
11
|
+
async run() {
|
|
12
|
+
const { flags } = await this.parse(DestroyCommand);
|
|
13
|
+
return runStatusLike("destroy", flags);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
import { makeKnownFailuresFlags } from "../../command-helpers.mjs";
|
|
3
|
+
import { runKnownFailuresRenderCommand } from "../../known-failures.mjs";
|
|
4
|
+
|
|
5
|
+
export default class KnownFailuresRenderCommand extends Command {
|
|
6
|
+
static summary = "Render known failures markdown";
|
|
7
|
+
|
|
8
|
+
static enableJsonFlag = true;
|
|
9
|
+
|
|
10
|
+
static flags = makeKnownFailuresFlags();
|
|
11
|
+
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(KnownFailuresRenderCommand);
|
|
14
|
+
await runKnownFailuresRenderCommand({
|
|
15
|
+
...flags,
|
|
16
|
+
});
|
|
17
|
+
return { ok: true };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
import { makeKnownFailuresFlags } from "../../command-helpers.mjs";
|
|
3
|
+
import { runKnownFailuresValidateCommand } from "../../known-failures.mjs";
|
|
4
|
+
|
|
5
|
+
export default class KnownFailuresValidateCommand extends Command {
|
|
6
|
+
static summary = "Validate known failures against the latest status artifact";
|
|
7
|
+
|
|
8
|
+
static enableJsonFlag = true;
|
|
9
|
+
|
|
10
|
+
static flags = makeKnownFailuresFlags();
|
|
11
|
+
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(KnownFailuresValidateCommand);
|
|
14
|
+
await runKnownFailuresValidateCommand({
|
|
15
|
+
...flags,
|
|
16
|
+
issueMode: flags["issue-mode"] || null,
|
|
17
|
+
});
|
|
18
|
+
return { ok: true };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Args, Command, Flags } from "@oclif/core";
|
|
2
|
+
import { sharedFlags } from "../command-helpers.mjs";
|
|
3
|
+
import { readLogTail } from "../../runner/logs.mjs";
|
|
4
|
+
import { getServiceLogRefs, loadLatestRunArtifact, resolveFileSubject } from "../viewer.mjs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
export default class LogsCommand extends Command {
|
|
8
|
+
static summary = "Show backend log tails relevant to one file from the latest run";
|
|
9
|
+
|
|
10
|
+
static enableJsonFlag = true;
|
|
11
|
+
|
|
12
|
+
static args = {
|
|
13
|
+
file: Args.string({
|
|
14
|
+
description: "Optional file path; defaults to the first failed file",
|
|
15
|
+
required: false,
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
static flags = {
|
|
20
|
+
...sharedFlags,
|
|
21
|
+
tail: Flags.integer({
|
|
22
|
+
description: "Number of lines to show from each log",
|
|
23
|
+
default: 40,
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
async run() {
|
|
28
|
+
const { args, flags } = await this.parse(LogsCommand);
|
|
29
|
+
const productDir = flags.dir || process.cwd();
|
|
30
|
+
const runArtifact = loadLatestRunArtifact(productDir);
|
|
31
|
+
const subject = resolveFileSubject(runArtifact, args.file || null, flags.service || null);
|
|
32
|
+
const logs = getServiceLogRefs(runArtifact, subject.service.name).map((entry) => ({
|
|
33
|
+
...entry,
|
|
34
|
+
lines: readLogTail(path.join(productDir, entry.path), flags.tail),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
if (!this.jsonEnabled()) {
|
|
38
|
+
for (const entry of logs) {
|
|
39
|
+
this.log(`${entry.runtimeLabel}`);
|
|
40
|
+
this.log(` ${entry.path}`);
|
|
41
|
+
for (const line of entry.lines) this.log(` ${line}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return logs;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Args, Command } from "@oclif/core";
|
|
2
|
+
import { executeRunCommand, runFlags } from "../command-helpers.mjs";
|
|
3
|
+
|
|
4
|
+
export default class RunCommand extends Command {
|
|
5
|
+
static summary = "Run test suites";
|
|
6
|
+
|
|
7
|
+
static enableJsonFlag = true;
|
|
8
|
+
|
|
9
|
+
static args = {
|
|
10
|
+
type: Args.string({
|
|
11
|
+
description: "Optional suite type shortcut: int, e2e, dal, load, pw, all",
|
|
12
|
+
required: false,
|
|
13
|
+
options: ["int", "e2e", "dal", "load", "pw", "all"],
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
static flags = runFlags;
|
|
18
|
+
|
|
19
|
+
async run() {
|
|
20
|
+
const { args, flags } = await this.parse(RunCommand);
|
|
21
|
+
return executeRunCommand(this, flags, args.type || null);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Args, Command, Flags } from "@oclif/core";
|
|
2
|
+
import { sharedFlags } from "../command-helpers.mjs";
|
|
3
|
+
import { formatFileDetail, loadLatestRunArtifact, resolveFileSubject } from "../viewer.mjs";
|
|
4
|
+
|
|
5
|
+
export default class ShowCommand extends Command {
|
|
6
|
+
static summary = "Show the most useful details for one file from the latest run";
|
|
7
|
+
|
|
8
|
+
static enableJsonFlag = true;
|
|
9
|
+
|
|
10
|
+
static args = {
|
|
11
|
+
file: Args.string({
|
|
12
|
+
description: "File path to inspect; defaults to the first failed file",
|
|
13
|
+
required: false,
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
static flags = {
|
|
18
|
+
...sharedFlags,
|
|
19
|
+
"log-tail": Flags.integer({
|
|
20
|
+
description: "Number of backend log lines to include",
|
|
21
|
+
default: 12,
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async run() {
|
|
26
|
+
const { args, flags } = await this.parse(ShowCommand);
|
|
27
|
+
const productDir = flags.dir || process.cwd();
|
|
28
|
+
const runArtifact = loadLatestRunArtifact(productDir);
|
|
29
|
+
const subject = resolveFileSubject(runArtifact, args.file || null, flags.service || null);
|
|
30
|
+
const result = {
|
|
31
|
+
file: subject.file,
|
|
32
|
+
suite: {
|
|
33
|
+
name: subject.suite.name,
|
|
34
|
+
type: subject.suite.type,
|
|
35
|
+
},
|
|
36
|
+
service: {
|
|
37
|
+
name: subject.service.name,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
if (!this.jsonEnabled()) {
|
|
41
|
+
for (const line of formatFileDetail(productDir, runArtifact, subject, { logTail: flags["log-tail"] })) {
|
|
42
|
+
this.log(line);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
import { runStatusLike, sharedFlags } from "../command-helpers.mjs";
|
|
3
|
+
|
|
4
|
+
export default class StatusCommand extends Command {
|
|
5
|
+
static summary = "Show local testkit state";
|
|
6
|
+
|
|
7
|
+
static enableJsonFlag = true;
|
|
8
|
+
|
|
9
|
+
static flags = sharedFlags;
|
|
10
|
+
|
|
11
|
+
async run() {
|
|
12
|
+
const { flags } = await this.parse(StatusCommand);
|
|
13
|
+
return runStatusLike("status", flags);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { createElement } from "react";
|
|
2
|
+
import { Command } from "@oclif/core";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import { sharedFlags } from "../command-helpers.mjs";
|
|
5
|
+
import { WatchApp } from "../tui/watch-app.mjs";
|
|
6
|
+
|
|
7
|
+
export default class WatchCommand extends Command {
|
|
8
|
+
static summary = "Open an interactive viewer for the latest run artifact";
|
|
9
|
+
|
|
10
|
+
static flags = sharedFlags;
|
|
11
|
+
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(WatchCommand);
|
|
14
|
+
const productDir = flags.dir || process.cwd();
|
|
15
|
+
const app = render(
|
|
16
|
+
createElement(WatchApp, {
|
|
17
|
+
productDir,
|
|
18
|
+
serviceFilter: flags.service || null,
|
|
19
|
+
})
|
|
20
|
+
);
|
|
21
|
+
await app.waitUntilExit();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export function normalizeCliArgs(argv) {
|
|
2
|
+
const topLevelCommands = new Set([
|
|
3
|
+
"run",
|
|
4
|
+
"status",
|
|
5
|
+
"destroy",
|
|
6
|
+
"cleanup",
|
|
7
|
+
"show",
|
|
8
|
+
"logs",
|
|
9
|
+
"artifacts",
|
|
10
|
+
"watch",
|
|
11
|
+
"known-failures",
|
|
12
|
+
"db",
|
|
13
|
+
"help",
|
|
14
|
+
"--help",
|
|
15
|
+
"-h",
|
|
16
|
+
"--version",
|
|
17
|
+
"-v",
|
|
18
|
+
]);
|
|
19
|
+
const runTypeShortcuts = new Set(["int", "e2e", "dal", "load", "pw", "all"]);
|
|
20
|
+
const valueFlags = new Set([
|
|
21
|
+
"--dir",
|
|
22
|
+
"--service",
|
|
23
|
+
"--type",
|
|
24
|
+
"--suite",
|
|
25
|
+
"--file",
|
|
26
|
+
"--workers",
|
|
27
|
+
"--file-timeout-seconds",
|
|
28
|
+
"--shard",
|
|
29
|
+
"--input",
|
|
30
|
+
"--output",
|
|
31
|
+
"--status",
|
|
32
|
+
"--issue-mode",
|
|
33
|
+
"--output-mode",
|
|
34
|
+
"--tail",
|
|
35
|
+
"--log-tail",
|
|
36
|
+
]);
|
|
37
|
+
const positionals = findPositionals(argv, valueFlags);
|
|
38
|
+
const firstPositional = positionals[0] || null;
|
|
39
|
+
const shouldPrefixRun =
|
|
40
|
+
!firstPositional ||
|
|
41
|
+
runTypeShortcuts.has(firstPositional.value) ||
|
|
42
|
+
!topLevelCommands.has(firstPositional.value);
|
|
43
|
+
|
|
44
|
+
if (shouldPrefixRun) {
|
|
45
|
+
return ["run", ...argv];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (topLevelCommands.has(firstPositional.value) && argv[0] !== firstPositional.value) {
|
|
49
|
+
return reorderCommandArgs(argv, positionals);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return argv;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function findPositionals(args, flagsWithValues) {
|
|
56
|
+
const positionals = [];
|
|
57
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
58
|
+
const value = args[index];
|
|
59
|
+
if (!value.startsWith("-")) {
|
|
60
|
+
positionals.push({ index, value });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (flagsWithValues.has(value)) {
|
|
64
|
+
index += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return positionals;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function reorderCommandArgs(args, positionals) {
|
|
71
|
+
const commandTokens = [positionals[0]];
|
|
72
|
+
if (positionals[0]?.value === "known-failures" && positionals[1]) {
|
|
73
|
+
commandTokens.push(positionals[1]);
|
|
74
|
+
}
|
|
75
|
+
if (positionals[0]?.value === "db" && positionals[1] && positionals[2]) {
|
|
76
|
+
commandTokens.push(positionals[1], positionals[2]);
|
|
77
|
+
}
|
|
78
|
+
const commandIndexes = new Set(commandTokens.map((token) => token.index));
|
|
79
|
+
return [
|
|
80
|
+
...commandTokens.map((token) => token.value),
|
|
81
|
+
...args.filter((_value, index) => !commandIndexes.has(index)),
|
|
82
|
+
];
|
|
83
|
+
}
|