@elench/testkit 0.1.34 → 0.1.36
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 +30 -11
- package/lib/cli/args.mjs +28 -20
- package/lib/cli/args.test.mjs +22 -17
- package/lib/cli/index.mjs +27 -38
- package/lib/config/index.mjs +116 -0
- package/lib/reporters/playwright.mjs +41 -5
- package/lib/reporters/playwright.test.mjs +83 -0
- package/lib/runner/formatting.mjs +36 -4
- package/lib/runner/formatting.test.mjs +49 -0
- package/lib/runner/orchestrator.mjs +13 -16
- package/lib/runner/planning.mjs +65 -13
- package/lib/runner/planning.test.mjs +57 -5
- package/lib/runner/playwright-runner.mjs +2 -1
- package/lib/runner/reporting.mjs +43 -24
- package/lib/runner/reporting.test.mjs +41 -20
- package/lib/runner/results.mjs +56 -15
- package/lib/runner/results.test.mjs +114 -0
- package/lib/runner/selection.mjs +9 -7
- package/lib/runner/selection.test.mjs +5 -6
- package/lib/runner/suite-selection.mjs +91 -0
- package/lib/runner/suite-selection.test.mjs +42 -0
- package/lib/setup/index.d.ts +16 -0
- package/package.json +1 -1
|
@@ -10,11 +10,13 @@ describe("runner reporting", () => {
|
|
|
10
10
|
skipped: false,
|
|
11
11
|
suiteCount: 1,
|
|
12
12
|
completedSuiteCount: 1,
|
|
13
|
+
skippedSuiteCount: 0,
|
|
13
14
|
failedSuiteCount: 0,
|
|
14
15
|
totalFileCount: 3,
|
|
15
16
|
completedFileCount: 3,
|
|
16
17
|
passedFileCount: 3,
|
|
17
18
|
failedFileCount: 0,
|
|
19
|
+
skippedFileCount: 0,
|
|
18
20
|
notRunFileCount: 0,
|
|
19
21
|
durationMs: 1200,
|
|
20
22
|
totalTaskDurationMs: 2400,
|
|
@@ -28,11 +30,13 @@ describe("runner reporting", () => {
|
|
|
28
30
|
skipped: true,
|
|
29
31
|
suiteCount: 0,
|
|
30
32
|
completedSuiteCount: 0,
|
|
33
|
+
skippedSuiteCount: 0,
|
|
31
34
|
failedSuiteCount: 0,
|
|
32
35
|
totalFileCount: 0,
|
|
33
36
|
completedFileCount: 0,
|
|
34
37
|
passedFileCount: 0,
|
|
35
38
|
failedFileCount: 0,
|
|
39
|
+
skippedFileCount: 0,
|
|
36
40
|
notRunFileCount: 0,
|
|
37
41
|
durationMs: 0,
|
|
38
42
|
totalTaskDurationMs: 0,
|
|
@@ -49,10 +53,9 @@ describe("runner reporting", () => {
|
|
|
49
53
|
finishedAt: 4000,
|
|
50
54
|
requestedJobs: 2,
|
|
51
55
|
workerCount: 1,
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
typeValues: ["all"],
|
|
57
|
+
suiteSelectors: [],
|
|
54
58
|
fileNames: [],
|
|
55
|
-
framework: "all",
|
|
56
59
|
shard: null,
|
|
57
60
|
serviceFilter: null,
|
|
58
61
|
metadata: {
|
|
@@ -71,11 +74,25 @@ describe("runner reporting", () => {
|
|
|
71
74
|
});
|
|
72
75
|
|
|
73
76
|
expect(artifact.product.name).toBe("my-product");
|
|
74
|
-
expect(artifact.
|
|
77
|
+
expect(artifact.schemaVersion).toBe(3);
|
|
78
|
+
expect(artifact.summary.services).toEqual({
|
|
79
|
+
total: 1,
|
|
80
|
+
passed: 1,
|
|
81
|
+
failed: 0,
|
|
82
|
+
skipped: 0,
|
|
83
|
+
});
|
|
84
|
+
expect(artifact.summary.suites).toEqual({
|
|
85
|
+
total: 1,
|
|
86
|
+
completed: 1,
|
|
87
|
+
passed: 1,
|
|
88
|
+
failed: 0,
|
|
89
|
+
skipped: 0,
|
|
90
|
+
});
|
|
75
91
|
expect(artifact.summary.files).toEqual({
|
|
76
92
|
total: 3,
|
|
77
93
|
passed: 3,
|
|
78
94
|
failed: 0,
|
|
95
|
+
skipped: 0,
|
|
79
96
|
notRun: 0,
|
|
80
97
|
});
|
|
81
98
|
expect(artifact.services[0].durationMs).toBe(1200);
|
|
@@ -93,20 +110,23 @@ describe("runner reporting", () => {
|
|
|
93
110
|
suites: [
|
|
94
111
|
{
|
|
95
112
|
name: "health",
|
|
96
|
-
type: "
|
|
113
|
+
type: "int",
|
|
97
114
|
framework: "k6",
|
|
98
115
|
files: [
|
|
99
116
|
{ path: "tests/api/integration/a.int.testkit.ts", status: "passed" },
|
|
100
|
-
{
|
|
117
|
+
{
|
|
118
|
+
path: "tests/api/integration/b.int.testkit.ts",
|
|
119
|
+
status: "skipped",
|
|
120
|
+
reason: "Billing is stubbed",
|
|
121
|
+
},
|
|
101
122
|
],
|
|
102
123
|
},
|
|
103
124
|
],
|
|
104
125
|
},
|
|
105
126
|
],
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
typeValues: ["int"],
|
|
128
|
+
suiteSelectors: [{ kind: "plain", name: "health", raw: "health" }],
|
|
108
129
|
fileNames: ["tests/api/integration/b.int.testkit.ts"],
|
|
109
|
-
framework: "default",
|
|
110
130
|
shard: null,
|
|
111
131
|
serviceFilter: "api",
|
|
112
132
|
metadata: {
|
|
@@ -119,7 +139,7 @@ describe("runner reporting", () => {
|
|
|
119
139
|
});
|
|
120
140
|
|
|
121
141
|
expect(status).toEqual({
|
|
122
|
-
schemaVersion:
|
|
142
|
+
schemaVersion: 3,
|
|
123
143
|
source: "testkit",
|
|
124
144
|
notice: "Generated file. Do not edit manually.",
|
|
125
145
|
product: {
|
|
@@ -131,10 +151,9 @@ describe("runner reporting", () => {
|
|
|
131
151
|
},
|
|
132
152
|
testkitVersion: "0.1.20",
|
|
133
153
|
scope: {
|
|
134
|
-
|
|
135
|
-
|
|
154
|
+
types: ["int"],
|
|
155
|
+
suiteSelectors: ["health"],
|
|
136
156
|
fileNames: ["tests/api/integration/b.int.testkit.ts"],
|
|
137
|
-
framework: "default",
|
|
138
157
|
shard: null,
|
|
139
158
|
serviceFilter: "api",
|
|
140
159
|
isFullRun: false,
|
|
@@ -144,26 +163,29 @@ describe("runner reporting", () => {
|
|
|
144
163
|
total: 1,
|
|
145
164
|
passed: 0,
|
|
146
165
|
failed: 1,
|
|
166
|
+
skipped: 0,
|
|
147
167
|
},
|
|
148
168
|
tests: {
|
|
149
169
|
total: 2,
|
|
150
170
|
passed: 1,
|
|
151
|
-
failed:
|
|
171
|
+
failed: 0,
|
|
172
|
+
skipped: 1,
|
|
152
173
|
notRun: 0,
|
|
153
174
|
},
|
|
154
175
|
},
|
|
155
176
|
tests: [
|
|
156
177
|
{
|
|
157
178
|
service: "api",
|
|
158
|
-
type: "
|
|
179
|
+
type: "int",
|
|
159
180
|
path: "tests/api/integration/a.int.testkit.ts",
|
|
160
181
|
status: "passed",
|
|
161
182
|
},
|
|
162
183
|
{
|
|
163
184
|
service: "api",
|
|
164
|
-
type: "
|
|
185
|
+
type: "int",
|
|
165
186
|
path: "tests/api/integration/b.int.testkit.ts",
|
|
166
|
-
status: "
|
|
187
|
+
status: "skipped",
|
|
188
|
+
reason: "Billing is stubbed",
|
|
167
189
|
},
|
|
168
190
|
],
|
|
169
191
|
});
|
|
@@ -173,10 +195,9 @@ describe("runner reporting", () => {
|
|
|
173
195
|
const status = buildStatusArtifact({
|
|
174
196
|
productDir: "/tmp/my-product",
|
|
175
197
|
results: [],
|
|
176
|
-
|
|
177
|
-
|
|
198
|
+
typeValues: ["all"],
|
|
199
|
+
suiteSelectors: [],
|
|
178
200
|
fileNames: [],
|
|
179
|
-
framework: "all",
|
|
180
201
|
shard: null,
|
|
181
202
|
serviceFilter: null,
|
|
182
203
|
metadata: {
|
package/lib/runner/results.mjs
CHANGED
|
@@ -26,14 +26,15 @@ export function buildServiceTrackers(servicePlans, startedAt) {
|
|
|
26
26
|
key: `${suite.type}:${suite.name}`,
|
|
27
27
|
name: suite.name,
|
|
28
28
|
type: suite.type,
|
|
29
|
+
displayType: suite.displayType || suite.type,
|
|
29
30
|
framework: suite.framework,
|
|
30
31
|
orderIndex: suite.orderIndex,
|
|
31
|
-
fileCount: suite.files.length,
|
|
32
|
+
fileCount: suite.totalFileCount ?? suite.files.length,
|
|
32
33
|
completedFileCount: 0,
|
|
33
34
|
failedFiles: [],
|
|
34
35
|
failedFileSet: new Set(),
|
|
35
|
-
fileResultsByPath: new Map(
|
|
36
|
-
suite.files.map((file) => {
|
|
36
|
+
fileResultsByPath: new Map([
|
|
37
|
+
...suite.files.map((file) => {
|
|
37
38
|
const normalizedPath = normalizePathSeparators(file);
|
|
38
39
|
return [
|
|
39
40
|
normalizedPath,
|
|
@@ -42,11 +43,23 @@ export function buildServiceTrackers(servicePlans, startedAt) {
|
|
|
42
43
|
failed: false,
|
|
43
44
|
durationMs: 0,
|
|
44
45
|
error: null,
|
|
46
|
+
reason: null,
|
|
45
47
|
status: "not_run",
|
|
46
48
|
},
|
|
47
49
|
];
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
+
}),
|
|
51
|
+
...(suite.skippedFiles || []).map((file) => [
|
|
52
|
+
file.path,
|
|
53
|
+
{
|
|
54
|
+
path: file.path,
|
|
55
|
+
failed: false,
|
|
56
|
+
durationMs: 0,
|
|
57
|
+
error: null,
|
|
58
|
+
reason: file.reason,
|
|
59
|
+
status: "skipped",
|
|
60
|
+
},
|
|
61
|
+
]),
|
|
62
|
+
]),
|
|
50
63
|
durationMs: 0,
|
|
51
64
|
error: null,
|
|
52
65
|
}));
|
|
@@ -92,25 +105,30 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
|
|
|
92
105
|
: Math.max(tracker.lastTaskAt, outcomeFinishedAt);
|
|
93
106
|
tracker.totalTaskDurationMs += outcomeDurationMs;
|
|
94
107
|
|
|
95
|
-
suite.completedFileCount += 1;
|
|
96
108
|
suite.durationMs += outcomeDurationMs;
|
|
97
109
|
const normalizedPath = normalizePathSeparators(task.file);
|
|
98
110
|
const existingFileResult = suite.fileResultsByPath.get(normalizedPath);
|
|
111
|
+
const status = normalizeOutcomeStatus(outcome);
|
|
112
|
+
if (status !== "skipped") {
|
|
113
|
+
suite.completedFileCount += 1;
|
|
114
|
+
}
|
|
99
115
|
if (existingFileResult) {
|
|
100
|
-
existingFileResult.failed =
|
|
116
|
+
existingFileResult.failed = status === "failed";
|
|
101
117
|
existingFileResult.durationMs = outcomeDurationMs;
|
|
102
118
|
existingFileResult.error = outcome.error;
|
|
103
|
-
existingFileResult.
|
|
119
|
+
existingFileResult.reason = outcome.reason || null;
|
|
120
|
+
existingFileResult.status = status;
|
|
104
121
|
} else {
|
|
105
122
|
suite.fileResultsByPath.set(normalizedPath, {
|
|
106
123
|
path: normalizedPath,
|
|
107
|
-
failed:
|
|
124
|
+
failed: status === "failed",
|
|
108
125
|
durationMs: outcomeDurationMs,
|
|
109
126
|
error: outcome.error,
|
|
110
|
-
|
|
127
|
+
reason: outcome.reason || null,
|
|
128
|
+
status,
|
|
111
129
|
});
|
|
112
130
|
}
|
|
113
|
-
if (
|
|
131
|
+
if (status === "failed" && !suite.failedFileSet.has(task.file)) {
|
|
114
132
|
suite.failedFileSet.add(task.file);
|
|
115
133
|
suite.failedFiles.push(task.file);
|
|
116
134
|
}
|
|
@@ -144,7 +162,14 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
|
144
162
|
skipped: true,
|
|
145
163
|
suiteCount: 0,
|
|
146
164
|
completedSuiteCount: 0,
|
|
165
|
+
skippedSuiteCount: 0,
|
|
147
166
|
failedSuiteCount: 0,
|
|
167
|
+
totalFileCount: 0,
|
|
168
|
+
completedFileCount: 0,
|
|
169
|
+
passedFileCount: 0,
|
|
170
|
+
failedFileCount: 0,
|
|
171
|
+
skippedFileCount: 0,
|
|
172
|
+
notRunFileCount: 0,
|
|
148
173
|
durationMs: 0,
|
|
149
174
|
totalTaskDurationMs: 0,
|
|
150
175
|
suites: [],
|
|
@@ -157,14 +182,18 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
|
157
182
|
.map((suite) => finalizeSuite(suite));
|
|
158
183
|
|
|
159
184
|
const completedSuiteCount = suites.filter(
|
|
160
|
-
(suite) => suite.completedFileCount === suite.fileCount
|
|
185
|
+
(suite) => suite.completedFileCount + suite.skippedFileCount === suite.fileCount
|
|
186
|
+
).length;
|
|
187
|
+
const skippedSuiteCount = suites.filter(
|
|
188
|
+
(suite) => suite.skippedFileCount === suite.fileCount && suite.fileCount > 0
|
|
161
189
|
).length;
|
|
162
190
|
const failedSuiteCount = suites.filter((suite) => suite.failedFileCount > 0).length;
|
|
163
191
|
const totalFileCount = suites.reduce((sum, suite) => sum + suite.fileCount, 0);
|
|
164
192
|
const completedFileCount = suites.reduce((sum, suite) => sum + suite.completedFileCount, 0);
|
|
165
193
|
const failedFileCount = suites.reduce((sum, suite) => sum + suite.failedFileCount, 0);
|
|
166
194
|
const passedFileCount = suites.reduce((sum, suite) => sum + suite.passedFileCount, 0);
|
|
167
|
-
const
|
|
195
|
+
const skippedFileCount = suites.reduce((sum, suite) => sum + suite.skippedFileCount, 0);
|
|
196
|
+
const notRunFileCount = totalFileCount - completedFileCount - skippedFileCount;
|
|
168
197
|
const totalTaskDurationMs =
|
|
169
198
|
tracker.totalTaskDurationMs || suites.reduce((sum, suite) => sum + suite.durationMs, 0);
|
|
170
199
|
const durationMs =
|
|
@@ -182,11 +211,13 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
|
182
211
|
skipped: false,
|
|
183
212
|
suiteCount: tracker.suiteCount,
|
|
184
213
|
completedSuiteCount,
|
|
214
|
+
skippedSuiteCount,
|
|
185
215
|
failedSuiteCount,
|
|
186
216
|
totalFileCount,
|
|
187
217
|
completedFileCount,
|
|
188
218
|
passedFileCount,
|
|
189
219
|
failedFileCount,
|
|
220
|
+
skippedFileCount,
|
|
190
221
|
notRunFileCount,
|
|
191
222
|
durationMs,
|
|
192
223
|
totalTaskDurationMs,
|
|
@@ -211,18 +242,23 @@ function finalizeSuite(suite) {
|
|
|
211
242
|
status: file.status,
|
|
212
243
|
durationMs: file.durationMs,
|
|
213
244
|
error: file.error,
|
|
245
|
+
reason: file.reason,
|
|
214
246
|
}));
|
|
215
247
|
|
|
216
248
|
return {
|
|
217
249
|
name: suite.name,
|
|
218
|
-
type: suite.
|
|
250
|
+
type: suite.displayType,
|
|
219
251
|
framework: formatFrameworkForArtifact(suite.framework),
|
|
220
252
|
failed: suite.failedFiles.length > 0,
|
|
221
253
|
fileCount: suite.fileCount,
|
|
222
254
|
completedFileCount: suite.completedFileCount,
|
|
223
255
|
passedFileCount: files.filter((file) => file.status === "passed").length,
|
|
224
256
|
failedFileCount: suite.failedFiles.length,
|
|
225
|
-
|
|
257
|
+
skippedFileCount: files.filter((file) => file.status === "skipped").length,
|
|
258
|
+
notRunFileCount:
|
|
259
|
+
suite.fileCount -
|
|
260
|
+
suite.completedFileCount -
|
|
261
|
+
files.filter((file) => file.status === "skipped").length,
|
|
226
262
|
failedFiles: suite.failedFiles,
|
|
227
263
|
durationMs: suite.durationMs,
|
|
228
264
|
error: suite.error,
|
|
@@ -238,3 +274,8 @@ function formatFrameworkForArtifact(framework) {
|
|
|
238
274
|
if (framework === "k6") return "default";
|
|
239
275
|
return framework;
|
|
240
276
|
}
|
|
277
|
+
|
|
278
|
+
function normalizeOutcomeStatus(outcome) {
|
|
279
|
+
if (outcome?.status === "skipped") return "skipped";
|
|
280
|
+
return outcome?.failed ? "failed" : "passed";
|
|
281
|
+
}
|
|
@@ -72,6 +72,7 @@ describe("runner results", () => {
|
|
|
72
72
|
status: "failed",
|
|
73
73
|
durationMs: 250,
|
|
74
74
|
error: "boom",
|
|
75
|
+
reason: null,
|
|
75
76
|
},
|
|
76
77
|
]);
|
|
77
78
|
});
|
|
@@ -193,6 +194,119 @@ describe("runner results", () => {
|
|
|
193
194
|
expect(result.totalTaskDurationMs).toBe(3_000);
|
|
194
195
|
});
|
|
195
196
|
|
|
197
|
+
it("counts config-skipped files and runtime-skipped files separately from passed work", () => {
|
|
198
|
+
const trackers = buildServiceTrackers(
|
|
199
|
+
[
|
|
200
|
+
{
|
|
201
|
+
skipped: false,
|
|
202
|
+
config: {
|
|
203
|
+
name: "api",
|
|
204
|
+
testkit: {
|
|
205
|
+
database: {
|
|
206
|
+
selectedBackend: null,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
suites: [
|
|
211
|
+
{
|
|
212
|
+
name: "billing",
|
|
213
|
+
type: "integration",
|
|
214
|
+
framework: "k6",
|
|
215
|
+
files: ["tests/billing-live.int.testkit.ts"],
|
|
216
|
+
skippedFiles: [
|
|
217
|
+
{
|
|
218
|
+
path: "tests/billing-stubbed.int.testkit.ts",
|
|
219
|
+
reason: "Billing is stubbed",
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
totalFileCount: 2,
|
|
223
|
+
orderIndex: 0,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "frontend-smoke",
|
|
227
|
+
type: "e2e",
|
|
228
|
+
framework: "playwright",
|
|
229
|
+
files: ["tests/frontend-smoke.pw.testkit.ts"],
|
|
230
|
+
orderIndex: 1,
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
1000
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
recordTaskOutcome(
|
|
239
|
+
trackers,
|
|
240
|
+
{
|
|
241
|
+
serviceName: "api",
|
|
242
|
+
suiteKey: "integration:billing",
|
|
243
|
+
file: "tests/billing-live.int.testkit.ts",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
failed: false,
|
|
247
|
+
durationMs: 100,
|
|
248
|
+
error: null,
|
|
249
|
+
},
|
|
250
|
+
1100
|
|
251
|
+
);
|
|
252
|
+
recordTaskOutcome(
|
|
253
|
+
trackers,
|
|
254
|
+
{
|
|
255
|
+
serviceName: "api",
|
|
256
|
+
suiteKey: "e2e:frontend-smoke",
|
|
257
|
+
file: "tests/frontend-smoke.pw.testkit.ts",
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
failed: false,
|
|
261
|
+
status: "skipped",
|
|
262
|
+
reason: "Playwright test.skip()",
|
|
263
|
+
durationMs: 50,
|
|
264
|
+
error: null,
|
|
265
|
+
},
|
|
266
|
+
1150
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const result = finalizeServiceResult(trackers.get("api"), 1000, 1200);
|
|
270
|
+
|
|
271
|
+
expect(result.failed).toBe(false);
|
|
272
|
+
expect(result.completedSuiteCount).toBe(2);
|
|
273
|
+
expect(result.skippedSuiteCount).toBe(1);
|
|
274
|
+
expect(result.totalFileCount).toBe(3);
|
|
275
|
+
expect(result.completedFileCount).toBe(1);
|
|
276
|
+
expect(result.passedFileCount).toBe(1);
|
|
277
|
+
expect(result.failedFileCount).toBe(0);
|
|
278
|
+
expect(result.skippedFileCount).toBe(2);
|
|
279
|
+
expect(result.notRunFileCount).toBe(0);
|
|
280
|
+
expect(result.suites[0].files).toEqual([
|
|
281
|
+
{
|
|
282
|
+
path: "tests/billing-live.int.testkit.ts",
|
|
283
|
+
failed: false,
|
|
284
|
+
status: "passed",
|
|
285
|
+
durationMs: 100,
|
|
286
|
+
error: null,
|
|
287
|
+
reason: null,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
path: "tests/billing-stubbed.int.testkit.ts",
|
|
291
|
+
failed: false,
|
|
292
|
+
status: "skipped",
|
|
293
|
+
durationMs: 0,
|
|
294
|
+
error: null,
|
|
295
|
+
reason: "Billing is stubbed",
|
|
296
|
+
},
|
|
297
|
+
]);
|
|
298
|
+
expect(result.suites[1].files).toEqual([
|
|
299
|
+
{
|
|
300
|
+
path: "tests/frontend-smoke.pw.testkit.ts",
|
|
301
|
+
failed: false,
|
|
302
|
+
status: "skipped",
|
|
303
|
+
durationMs: 50,
|
|
304
|
+
error: null,
|
|
305
|
+
reason: "Playwright test.skip()",
|
|
306
|
+
},
|
|
307
|
+
]);
|
|
308
|
+
});
|
|
309
|
+
|
|
196
310
|
it("summarizes mixed db backends", () => {
|
|
197
311
|
expect(
|
|
198
312
|
summarizeDbBackend([{ dbBackend: "local" }, { dbBackend: "neon" }])
|
package/lib/runner/selection.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
export function findUnmatchedRequestedFiles(
|
|
2
2
|
configs,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
framework,
|
|
3
|
+
typeValues,
|
|
4
|
+
suiteSelectors,
|
|
6
5
|
fileNames,
|
|
7
6
|
collectSuites,
|
|
8
7
|
normalizePathSeparators
|
|
9
8
|
) {
|
|
10
9
|
const matchedFiles = new Set();
|
|
11
10
|
for (const config of configs) {
|
|
12
|
-
const suites = collectSuites(config,
|
|
11
|
+
const suites = collectSuites(config, typeValues, suiteSelectors, [], {
|
|
12
|
+
ignoreSkipRules: true,
|
|
13
|
+
});
|
|
13
14
|
for (const suite of suites) {
|
|
14
15
|
for (const file of suite.files) {
|
|
15
16
|
matchedFiles.add(normalizePathSeparators(file));
|
|
@@ -22,11 +23,12 @@ export function findUnmatchedRequestedFiles(
|
|
|
22
23
|
);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export function isFullRunSelection(
|
|
26
|
+
export function isFullRunSelection(typeValues, suiteSelectors, fileNames, shard, serviceFilter) {
|
|
26
27
|
return (
|
|
27
|
-
(
|
|
28
|
+
(typeValues || []).length === 1 &&
|
|
29
|
+
typeValues[0] === "all" &&
|
|
30
|
+
(suiteSelectors || []).length === 0 &&
|
|
28
31
|
(fileNames || []).length === 0 &&
|
|
29
|
-
(framework || "all") === "all" &&
|
|
30
32
|
(shard || null) === null &&
|
|
31
33
|
(serviceFilter || null) === null
|
|
32
34
|
);
|
|
@@ -5,9 +5,8 @@ describe("runner selection", () => {
|
|
|
5
5
|
it("finds unmatched requested files", () => {
|
|
6
6
|
const unmatched = findUnmatchedRequestedFiles(
|
|
7
7
|
[{ name: "api" }],
|
|
8
|
-
"int",
|
|
8
|
+
["int"],
|
|
9
9
|
[],
|
|
10
|
-
"all",
|
|
11
10
|
["tests/a.int.testkit.ts", "tests/missing.int.testkit.ts"],
|
|
12
11
|
() => [{ files: ["tests/a.int.testkit.ts"] }],
|
|
13
12
|
(value) => value
|
|
@@ -17,9 +16,9 @@ describe("runner selection", () => {
|
|
|
17
16
|
});
|
|
18
17
|
|
|
19
18
|
it("detects a full run selection", () => {
|
|
20
|
-
expect(isFullRunSelection([], [],
|
|
21
|
-
expect(isFullRunSelection(["
|
|
22
|
-
expect(isFullRunSelection([], [
|
|
23
|
-
expect(isFullRunSelection([], [],
|
|
19
|
+
expect(isFullRunSelection(["all"], [], [], null, null)).toBe(true);
|
|
20
|
+
expect(isFullRunSelection(["all"], [{ raw: "auth" }], [], null, null)).toBe(false);
|
|
21
|
+
expect(isFullRunSelection(["all"], [], ["a"], null, null)).toBe(false);
|
|
22
|
+
expect(isFullRunSelection(["int"], [], [], null, null)).toBe(false);
|
|
24
23
|
});
|
|
25
24
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const USER_TYPES = new Set(["int", "e2e", "dal", "load", "pw", "all"]);
|
|
2
|
+
|
|
3
|
+
export function normalizeTypeValues(values = []) {
|
|
4
|
+
const expanded = [];
|
|
5
|
+
for (const rawValue of values) {
|
|
6
|
+
if (rawValue == null) continue;
|
|
7
|
+
for (const part of String(rawValue).split(",")) {
|
|
8
|
+
const value = part.trim();
|
|
9
|
+
if (!value) continue;
|
|
10
|
+
if (!USER_TYPES.has(value)) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Unknown type "${value}". Expected one of: int, e2e, dal, load, pw, all.`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
expanded.push(value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (expanded.length === 0) {
|
|
20
|
+
return ["all"];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const deduped = [...new Set(expanded)];
|
|
24
|
+
if (deduped.includes("all") && deduped.length > 1) {
|
|
25
|
+
throw new Error(`"--type all" cannot be combined with other types.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const order = ["int", "e2e", "dal", "load", "pw", "all"];
|
|
29
|
+
return deduped.sort((left, right) => order.indexOf(left) - order.indexOf(right));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isAllTypeSelection(typeValues = []) {
|
|
33
|
+
return typeValues.length === 0 || (typeValues.length === 1 && typeValues[0] === "all");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseSuiteSelectors(values = []) {
|
|
37
|
+
const selectors = [];
|
|
38
|
+
|
|
39
|
+
for (const rawValue of values) {
|
|
40
|
+
if (rawValue == null) continue;
|
|
41
|
+
for (const part of String(rawValue).split(",")) {
|
|
42
|
+
const value = part.trim();
|
|
43
|
+
if (!value) continue;
|
|
44
|
+
|
|
45
|
+
const typeMatch = value.match(/^([a-z]+):(.*)$/);
|
|
46
|
+
if (!typeMatch) {
|
|
47
|
+
selectors.push({ kind: "plain", name: value, raw: value });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const type = typeMatch[1];
|
|
52
|
+
const name = typeMatch[2].trim();
|
|
53
|
+
if (!USER_TYPES.has(type) || type === "all") {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Unknown suite selector type "${type}". Expected one of: int, e2e, dal, load, pw.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (!name) {
|
|
59
|
+
throw new Error(`Invalid suite selector "${value}". Expected "<type>:<suite-name>".`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
selectors.push({
|
|
63
|
+
kind: "typed",
|
|
64
|
+
type,
|
|
65
|
+
name,
|
|
66
|
+
raw: `${type}:${name}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return selectors;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function suiteSelectionType(type, framework) {
|
|
75
|
+
if ((framework || "k6") === "playwright") return "pw";
|
|
76
|
+
if (type === "integration") return "int";
|
|
77
|
+
return type;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function matchesSelectedTypes(selectionType, typeValues) {
|
|
81
|
+
return isAllTypeSelection(typeValues) || typeValues.includes(selectionType);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function matchesSuiteSelectors(selectionType, suiteName, selectors = []) {
|
|
85
|
+
if (!selectors || selectors.length === 0) return true;
|
|
86
|
+
|
|
87
|
+
return selectors.some((selector) => {
|
|
88
|
+
if (selector.kind === "plain") return selector.name === suiteName;
|
|
89
|
+
return selector.type === selectionType && selector.name === suiteName;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isAllTypeSelection,
|
|
4
|
+
matchesSelectedTypes,
|
|
5
|
+
matchesSuiteSelectors,
|
|
6
|
+
normalizeTypeValues,
|
|
7
|
+
parseSuiteSelectors,
|
|
8
|
+
suiteSelectionType,
|
|
9
|
+
} from "./suite-selection.mjs";
|
|
10
|
+
|
|
11
|
+
describe("runner suite selection", () => {
|
|
12
|
+
it("normalizes selected type values", () => {
|
|
13
|
+
expect(normalizeTypeValues([])).toEqual(["all"]);
|
|
14
|
+
expect(normalizeTypeValues(["int,e2e", "dal"])).toEqual(["int", "e2e", "dal"]);
|
|
15
|
+
expect(() => normalizeTypeValues(["all", "int"])).toThrow("cannot be combined");
|
|
16
|
+
expect(() => normalizeTypeValues(["jest"])).toThrow("Unknown type");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("parses suite selectors", () => {
|
|
20
|
+
expect(parseSuiteSelectors(["auth,dal:queries"])).toEqual([
|
|
21
|
+
{ kind: "plain", name: "auth", raw: "auth" },
|
|
22
|
+
{ kind: "typed", type: "dal", name: "queries", raw: "dal:queries" },
|
|
23
|
+
]);
|
|
24
|
+
expect(() => parseSuiteSelectors(["all:auth"])).toThrow("Unknown suite selector type");
|
|
25
|
+
expect(() => parseSuiteSelectors(["int:"])).toThrow("Invalid suite selector");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("matches suite types and selectors", () => {
|
|
29
|
+
expect(isAllTypeSelection(["all"])).toBe(true);
|
|
30
|
+
expect(matchesSelectedTypes("int", ["int", "dal"])).toBe(true);
|
|
31
|
+
expect(matchesSelectedTypes("pw", ["int", "dal"])).toBe(false);
|
|
32
|
+
expect(matchesSuiteSelectors("int", "auth", parseSuiteSelectors(["auth"]))).toBe(true);
|
|
33
|
+
expect(matchesSuiteSelectors("e2e", "auth", parseSuiteSelectors(["int:auth"]))).toBe(false);
|
|
34
|
+
expect(matchesSuiteSelectors("int", "auth", parseSuiteSelectors(["int:auth"]))).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("maps discovered suites to user-facing selection types", () => {
|
|
38
|
+
expect(suiteSelectionType("integration", "k6")).toBe("int");
|
|
39
|
+
expect(suiteSelectionType("e2e", "playwright")).toBe("pw");
|
|
40
|
+
expect(suiteSelectionType("dal", "k6")).toBe("dal");
|
|
41
|
+
});
|
|
42
|
+
});
|
package/lib/setup/index.d.ts
CHANGED
|
@@ -18,6 +18,21 @@ export interface LifecycleConfig {
|
|
|
18
18
|
testkitCwd?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export interface SkipFileRule {
|
|
22
|
+
path: string;
|
|
23
|
+
reason: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SkipSuiteRule {
|
|
27
|
+
selector: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SkipConfig {
|
|
32
|
+
files?: SkipFileRule[];
|
|
33
|
+
suites?: SkipSuiteRule[];
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
export interface ServiceConfig {
|
|
22
37
|
database?: LocalDatabaseConfig;
|
|
23
38
|
databaseFrom?: string;
|
|
@@ -38,6 +53,7 @@ export interface ServiceConfig {
|
|
|
38
53
|
};
|
|
39
54
|
migrate?: LifecycleConfig;
|
|
40
55
|
seed?: LifecycleConfig;
|
|
56
|
+
skip?: SkipConfig;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
export interface TestkitSetup {
|