testomatio-reporter-cli 2.21.0 → 2.22.0
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/package.json +1 -1
- package/src/bin/cli.js +52 -38
- package/src/fetch/report.js +23 -2
- package/src/fetch/testruns.js +119 -38
- package/src/pipe/markdown.js +24 -1
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
downloadArtifacts,
|
|
29
29
|
defaultArtifactsDir,
|
|
30
30
|
TESTRUNS_DEFAULT_LIMIT,
|
|
31
|
-
TESTRUNS_MAX_LIMIT,
|
|
32
31
|
TESTRUN_STATUSES,
|
|
33
32
|
} from '../fetch/testruns.js';
|
|
34
33
|
import { renderRunReport } from '../fetch/report.js';
|
|
@@ -149,7 +148,7 @@ const fetchCommand = program
|
|
|
149
148
|
.command('fetch')
|
|
150
149
|
.description('Fetch runs from Testomat.io API v2')
|
|
151
150
|
.argument('[url]', 'URL of a run or of a (filtered) runs page in Testomat.io')
|
|
152
|
-
.option('--project <slug>', 'Project slug (or set TESTOMATIO_PROJECT)')
|
|
151
|
+
.option('--project <slug>', 'Project slug for listing runs (or set TESTOMATIO_PROJECT)')
|
|
153
152
|
.option('--run <id>', 'Fetch a single run by its id')
|
|
154
153
|
.option('--title <text>', 'Filter by run title')
|
|
155
154
|
.option('--tql <query>', 'Filter using Testomat Query Language')
|
|
@@ -180,29 +179,33 @@ const fetchCommand = program
|
|
|
180
179
|
}
|
|
181
180
|
}
|
|
182
181
|
|
|
183
|
-
const project = opts.project || page.project || process.env.TESTOMATIO_PROJECT;
|
|
184
|
-
if (!project) {
|
|
185
|
-
log.error(pc.red('⚠️ --project (or TESTOMATIO_PROJECT) is required'));
|
|
186
|
-
process.exit(1);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
182
|
const baseUrl = page.baseUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
190
183
|
const runId = opts.run || page.runId;
|
|
191
|
-
let url;
|
|
192
184
|
|
|
193
185
|
if (runId) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
186
|
+
const fetcher = new RunFetcher({ baseUrl, project: page.project, runId, apiKey });
|
|
187
|
+
const run = await fetcher.fetchRun().catch(exitOnFetchError);
|
|
188
|
+
console.log(formatFetchRunsOutput({ data: run }, opts.format));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const project = opts.project || page.project || process.env.TESTOMATIO_PROJECT;
|
|
193
|
+
if (!project) {
|
|
194
|
+
log.error(
|
|
195
|
+
pc.red('⚠️ --project (or TESTOMATIO_PROJECT) is required to list runs, unless --run or a URL is passed'),
|
|
196
|
+
);
|
|
197
|
+
process.exit(1);
|
|
204
198
|
}
|
|
205
199
|
|
|
200
|
+
const url = new URL(`/api/v2/${project}/runs`, baseUrl);
|
|
201
|
+
for (const [key, value] of page.params || []) url.searchParams.append(key, value);
|
|
202
|
+
if (opts.title) url.searchParams.set('search', opts.title);
|
|
203
|
+
if (opts.tql) url.searchParams.set('tql', opts.tql);
|
|
204
|
+
if (opts.rungroup) url.searchParams.set('groupId', opts.rungroup);
|
|
205
|
+
const requestedLimit = opts.latest ? 1 : parseInt(opts.limit, 10) || FETCH_RUNS_DEFAULT_LIMIT;
|
|
206
|
+
const limit = Math.min(Math.max(requestedLimit, 1), FETCH_RUNS_MAX_LIMIT);
|
|
207
|
+
url.searchParams.set('per_page', String(limit));
|
|
208
|
+
|
|
206
209
|
const response = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
207
210
|
const body = await response.json();
|
|
208
211
|
|
|
@@ -214,17 +217,16 @@ const fetchCommand = program
|
|
|
214
217
|
console.log(formatFetchRunsOutput(body, opts.format));
|
|
215
218
|
});
|
|
216
219
|
|
|
217
|
-
// fetch parses the options it shares with its subcommands (--
|
|
220
|
+
// fetch parses the options it shares with its subcommands (--format, --limit) wherever they are placed,
|
|
218
221
|
// so subcommands declare them only for help and read them with optsWithGlobals()
|
|
219
222
|
|
|
220
223
|
/**
|
|
221
224
|
* Build a RunFetcher for the run argument of fetch status|count|report, or exit with an error.
|
|
222
225
|
*
|
|
223
226
|
* @param {string} runArg
|
|
224
|
-
* @param {{project?: string}} opts
|
|
225
227
|
* @returns {RunFetcher}
|
|
226
228
|
*/
|
|
227
|
-
function createRunFetcher(runArg
|
|
229
|
+
function createRunFetcher(runArg) {
|
|
228
230
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
229
231
|
if (!apiKey) {
|
|
230
232
|
log.error(pc.red('⚠️ TESTOMATIO API key required'));
|
|
@@ -232,7 +234,7 @@ function createRunFetcher(runArg, opts) {
|
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
try {
|
|
235
|
-
return new RunFetcher({ ...resolveRunTarget(runArg
|
|
237
|
+
return new RunFetcher({ ...resolveRunTarget(runArg), apiKey });
|
|
236
238
|
} catch (err) {
|
|
237
239
|
log.error(pc.red(`⚠️ ${err.message}`));
|
|
238
240
|
process.exit(1);
|
|
@@ -268,11 +270,10 @@ fetchCommand
|
|
|
268
270
|
.command('status')
|
|
269
271
|
.description('Show a run with the number of its testruns by status')
|
|
270
272
|
.argument('<run>', RUN_ARGUMENT_DESCRIPTION)
|
|
271
|
-
.option('--project <slug>', 'Project slug (or set TESTOMATIO_PROJECT)')
|
|
272
273
|
.option('--format <format>', 'Machine-readable output: run status and counts as JSON (--format json)')
|
|
273
274
|
.action(async (runArg, _opts, command) => {
|
|
274
275
|
const opts = command.optsWithGlobals();
|
|
275
|
-
const fetcher = createRunFetcher(runArg
|
|
276
|
+
const fetcher = createRunFetcher(runArg);
|
|
276
277
|
|
|
277
278
|
const [run, summary] = await Promise.all([fetcher.fetchRun(), fetcher.countTestrunsByStatus()]).catch(
|
|
278
279
|
exitOnFetchError,
|
|
@@ -294,12 +295,11 @@ fetchCommand
|
|
|
294
295
|
.command('count')
|
|
295
296
|
.description('Print the number of testruns of a run, to pick --limit and filters for fetch report')
|
|
296
297
|
.argument('<run>', RUN_ARGUMENT_DESCRIPTION)
|
|
297
|
-
.option('--project <slug>', 'Project slug (or set TESTOMATIO_PROJECT)')
|
|
298
298
|
.option('--only <statuses>', STATUS_FILTER_DESCRIPTION)
|
|
299
299
|
.option('--only-failed', 'Only failed testruns (shorthand for --only failed)')
|
|
300
300
|
.action(async (runArg, _opts, command) => {
|
|
301
301
|
const opts = command.optsWithGlobals();
|
|
302
|
-
const fetcher = createRunFetcher(runArg
|
|
302
|
+
const fetcher = createRunFetcher(runArg);
|
|
303
303
|
const statuses = getStatusFilter(opts);
|
|
304
304
|
|
|
305
305
|
const total = await fetcher.countTestruns(statuses).catch(exitOnFetchError);
|
|
@@ -310,22 +310,19 @@ fetchCommand
|
|
|
310
310
|
.command('report')
|
|
311
311
|
.description('Print a run with its testruns as a Markdown report')
|
|
312
312
|
.argument('<run>', RUN_ARGUMENT_DESCRIPTION)
|
|
313
|
-
.option('--project <slug>', 'Project slug (or set TESTOMATIO_PROJECT)')
|
|
314
313
|
.option('--only <statuses>', STATUS_FILTER_DESCRIPTION)
|
|
315
314
|
.option('--only-failed', 'Only failed testruns (shorthand for --only failed)')
|
|
316
315
|
.option('--stack', 'Include stack traces')
|
|
317
316
|
.option('--artifacts', 'Download artifacts to --out and link them from the report')
|
|
318
317
|
.option('--out <dir>', 'Directory for downloaded artifacts (default: ./<run-title>-<run-id>)')
|
|
319
|
-
.option(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
)
|
|
318
|
+
.option('--limit <number>', `Max number of testruns, failed first (default ${TESTRUNS_DEFAULT_LIMIT})`)
|
|
319
|
+
.option('--all', 'All matching testruns, fetched in chunks of 100 (overrides --limit)')
|
|
320
|
+
.option('--no-defects', 'Skip defects of the run and of its tests')
|
|
323
321
|
.action(async (runArg, _opts, command) => {
|
|
324
322
|
const opts = command.optsWithGlobals();
|
|
325
|
-
const fetcher = createRunFetcher(runArg
|
|
323
|
+
const fetcher = createRunFetcher(runArg);
|
|
326
324
|
const statuses = getStatusFilter(opts);
|
|
327
|
-
const
|
|
328
|
-
if (capped) log.warn(pc.yellow(`⚠️ --limit is capped at ${TESTRUNS_MAX_LIMIT}`));
|
|
325
|
+
const limit = parseTestrunsLimit(opts);
|
|
329
326
|
|
|
330
327
|
const run = await fetcher.fetchRun().catch(exitOnFetchError);
|
|
331
328
|
|
|
@@ -337,16 +334,25 @@ fetchCommand
|
|
|
337
334
|
}
|
|
338
335
|
}
|
|
339
336
|
|
|
340
|
-
|
|
337
|
+
let runDefectsRequest;
|
|
338
|
+
if (opts.defects) {
|
|
339
|
+
runDefectsRequest = fetcher.listRunDefects().catch(err => {
|
|
340
|
+
log.warn(pc.yellow(`⚠️ Failed to fetch defects of the run: ${err.message}`));
|
|
341
|
+
return null;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const [summary, list, runDefects] = await Promise.all([
|
|
341
346
|
fetcher.countTestrunsByStatus(),
|
|
342
347
|
fetcher.listTestruns({ statuses, limit }),
|
|
348
|
+
runDefectsRequest,
|
|
343
349
|
]).catch(exitOnFetchError);
|
|
344
350
|
|
|
345
351
|
let { testruns } = list;
|
|
346
352
|
if (list.total > testruns.length) {
|
|
347
353
|
log.warn(
|
|
348
354
|
pc.yellow(
|
|
349
|
-
`⚠️ Showing ${testruns.length} of ${list.total} testruns. Use --limit or --only-failed
|
|
355
|
+
`⚠️ Showing ${testruns.length} of ${list.total} testruns. Use --all, --limit or --only-failed.`,
|
|
350
356
|
),
|
|
351
357
|
);
|
|
352
358
|
}
|
|
@@ -358,6 +364,14 @@ fetchCommand
|
|
|
358
364
|
if (!opts.stack) testruns = testruns.map(({ stack, ...testrun }) => testrun);
|
|
359
365
|
}
|
|
360
366
|
|
|
367
|
+
if (opts.defects) {
|
|
368
|
+
testruns = await fetcher.addTestDefects(testruns, (testId, err) => {
|
|
369
|
+
log.warn(pc.yellow(`⚠️ Failed to fetch defects of test ${testId}: ${err.message}`));
|
|
370
|
+
});
|
|
371
|
+
} else {
|
|
372
|
+
testruns = testruns.map(({ defects, ...testrun }) => testrun);
|
|
373
|
+
}
|
|
374
|
+
|
|
361
375
|
if (opts.artifacts) {
|
|
362
376
|
const outDir = opts.out || defaultArtifactsDir(run, fetcher.runId);
|
|
363
377
|
const artifactsCount = testruns.reduce((sum, testrun) => sum + (testrun.artifacts?.length || 0), 0);
|
|
@@ -370,7 +384,7 @@ fetchCommand
|
|
|
370
384
|
}
|
|
371
385
|
|
|
372
386
|
process.stdout.write(
|
|
373
|
-
renderRunReport({ run, runUrl: fetcher.runUrl, summary, testruns, matched: list.total, statuses }),
|
|
387
|
+
renderRunReport({ run, runUrl: fetcher.runUrl, summary, testruns, matched: list.total, statuses, runDefects }),
|
|
374
388
|
);
|
|
375
389
|
});
|
|
376
390
|
|
package/src/fetch/report.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderTests } from '../pipe/markdown.js';
|
|
1
|
+
import { renderTests, defectLink } from '../pipe/markdown.js';
|
|
2
2
|
|
|
3
3
|
const TITLE_SEPARATOR = ' ⇒ ';
|
|
4
4
|
|
|
@@ -28,9 +28,27 @@ function toReportTest(testrun) {
|
|
|
28
28
|
message: testrun.message,
|
|
29
29
|
stack: testrun.stack,
|
|
30
30
|
artifacts: testrun.artifacts,
|
|
31
|
+
defects: testrun.defects,
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @param {{defects: object[], total: number}} runDefects - Defects of GET /api/v2/:project/defects?run_id=
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function renderRunDefects({ defects, total }) {
|
|
40
|
+
const lines = ['## Defects', ''];
|
|
41
|
+
if (total > defects.length) lines.push(`${defects.length} of ${total} shown`, '');
|
|
42
|
+
lines.push('| Defect | Title | Status | Severity |', '| ------ | ----- | ------ | -------- |');
|
|
43
|
+
for (const defect of defects) {
|
|
44
|
+
const cells = [defectLink(defect), defect.title, defect.status, defect.severity].map(cell =>
|
|
45
|
+
String(cell || '').replace(/\|/g, '\\|'),
|
|
46
|
+
);
|
|
47
|
+
lines.push(`| ${cells.join(' | ')} |`);
|
|
48
|
+
}
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
/**
|
|
35
53
|
* Render a fetched run with its testruns as a Markdown report.
|
|
36
54
|
*
|
|
@@ -41,9 +59,10 @@ function toReportTest(testrun) {
|
|
|
41
59
|
* @param {object[]} params.testruns
|
|
42
60
|
* @param {number} params.matched - Number of testruns matching the filter
|
|
43
61
|
* @param {string[]} params.statuses - Status filter, empty for all testruns
|
|
62
|
+
* @param {{defects: object[], total: number} | null} [params.runDefects] - Defects of the run
|
|
44
63
|
* @returns {string}
|
|
45
64
|
*/
|
|
46
|
-
function renderRunReport({ run, runUrl, summary, testruns, matched, statuses }) {
|
|
65
|
+
function renderRunReport({ run, runUrl, summary, testruns, matched, statuses, runDefects }) {
|
|
47
66
|
const status = String(run.status || 'unknown');
|
|
48
67
|
const sections = [`# ${run.title || run.id} — ${status.toUpperCase()}`];
|
|
49
68
|
|
|
@@ -78,6 +97,8 @@ function renderRunReport({ run, runUrl, summary, testruns, matched, statuses })
|
|
|
78
97
|
].join('\n'),
|
|
79
98
|
);
|
|
80
99
|
|
|
100
|
+
if (runDefects?.defects.length) sections.push(renderRunDefects(runDefects));
|
|
101
|
+
|
|
81
102
|
sections.push(renderTests(testruns.map(toReportTest)));
|
|
82
103
|
|
|
83
104
|
return `${sections.join('\n\n')}\n`;
|
package/src/fetch/testruns.js
CHANGED
|
@@ -6,29 +6,26 @@ import { S3Uploader } from '../uploader.js';
|
|
|
6
6
|
const debug = createDebugMessages('@testomatio/reporter:fetch');
|
|
7
7
|
|
|
8
8
|
const TESTRUNS_DEFAULT_LIMIT = 50;
|
|
9
|
-
const
|
|
9
|
+
const TESTRUNS_PAGE_SIZE = 100;
|
|
10
10
|
const TESTRUN_STATUSES = ['passed', 'failed', 'skipped', 'pending'];
|
|
11
11
|
const PARALLEL_REQUESTS = 5;
|
|
12
|
+
const FINISHED_RUN_STATUSES = ['passed', 'failed', 'terminated', 'finished'];
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Resolve the run argument of `fetch status|count|report`: a run id or a run URL.
|
|
15
|
-
* A URL also sets the server and the project.
|
|
16
|
+
* A URL also sets the server and the project; for a run id the project is resolved from the run by RunFetcher.
|
|
16
17
|
*
|
|
17
18
|
* @param {string} runArg
|
|
18
|
-
* @
|
|
19
|
-
* @returns {{baseUrl: string, project: string, runId: string}}
|
|
19
|
+
* @returns {{baseUrl: string, project?: string, runId: string}}
|
|
20
20
|
*/
|
|
21
|
-
function resolveRunTarget(runArg
|
|
21
|
+
function resolveRunTarget(runArg) {
|
|
22
22
|
if (/^https?:\/\//i.test(runArg)) {
|
|
23
23
|
const page = parseRunsUrl(runArg);
|
|
24
24
|
if (!page?.runId) throw new Error(`Not a Testomat.io run URL: ${runArg}`);
|
|
25
|
-
return { baseUrl: page.baseUrl, project:
|
|
25
|
+
return { baseUrl: page.baseUrl, project: page.project, runId: page.runId };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
if (!project) throw new Error('--project (or TESTOMATIO_PROJECT) is required');
|
|
30
|
-
|
|
31
|
-
return { baseUrl: process.env.TESTOMATIO_URL || 'https://app.testomat.io', project, runId: runArg };
|
|
28
|
+
return { baseUrl: process.env.TESTOMATIO_URL || 'https://app.testomat.io', runId: runArg };
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
/**
|
|
@@ -53,12 +50,12 @@ function parseStatuses(opts) {
|
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
/**
|
|
56
|
-
* @param {string}
|
|
57
|
-
* @returns {
|
|
53
|
+
* @param {{limit?: string, all?: boolean}} opts - Values of the CLI --limit and --all options.
|
|
54
|
+
* @returns {number} Infinity for --all
|
|
58
55
|
*/
|
|
59
|
-
function parseTestrunsLimit(
|
|
60
|
-
|
|
61
|
-
return
|
|
56
|
+
function parseTestrunsLimit(opts) {
|
|
57
|
+
if (opts.all) return Infinity;
|
|
58
|
+
return Math.max(parseInt(opts.limit, 10) || TESTRUNS_DEFAULT_LIMIT, 1);
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
/**
|
|
@@ -88,13 +85,45 @@ async function mapWithLimit(items, limit, fn) {
|
|
|
88
85
|
*/
|
|
89
86
|
class RunFetcher {
|
|
90
87
|
/**
|
|
91
|
-
* @param {{baseUrl: string, project
|
|
88
|
+
* @param {{baseUrl: string, project?: string, runId: string, apiKey: string}} params
|
|
92
89
|
*/
|
|
93
90
|
constructor({ baseUrl, project, runId, apiKey }) {
|
|
94
91
|
this.baseUrl = baseUrl;
|
|
95
92
|
this.project = project;
|
|
96
93
|
this.runId = runId;
|
|
97
94
|
this.apiKey = apiKey;
|
|
95
|
+
/** @type {Promise<any>|null} */
|
|
96
|
+
this.reporterRun = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Empty update of the run in the reporter API: it returns the project of the token and shared S3 credentials.
|
|
101
|
+
*
|
|
102
|
+
* @returns {Promise<{project?: {id: string}, artifacts?: object}>}
|
|
103
|
+
*/
|
|
104
|
+
updateReporterRun() {
|
|
105
|
+
this.reporterRun ||= (async () => {
|
|
106
|
+
const url = new URL(`/api/reporter/${this.runId}`, this.baseUrl);
|
|
107
|
+
debug('PUT', url.toString());
|
|
108
|
+
const response = await fetch(url, {
|
|
109
|
+
method: 'PUT',
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
body: JSON.stringify({ api_key: this.apiKey }),
|
|
112
|
+
});
|
|
113
|
+
const body = await response.json().catch(() => ({}));
|
|
114
|
+
if (response.status === 404) throw new Error(`Run ${this.runId} not found`);
|
|
115
|
+
if (!response.ok) throw new Error(`${response.status} ${body.message || body.error || response.statusText}`);
|
|
116
|
+
return body;
|
|
117
|
+
})();
|
|
118
|
+
return this.reporterRun;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async resolveProject() {
|
|
122
|
+
if (this.project) return this.project;
|
|
123
|
+
const body = await this.updateReporterRun();
|
|
124
|
+
if (!body.project?.id) throw new Error(`Project of run ${this.runId} is unknown`);
|
|
125
|
+
this.project = body.project.id;
|
|
126
|
+
return this.project;
|
|
98
127
|
}
|
|
99
128
|
|
|
100
129
|
get runUrl() {
|
|
@@ -102,6 +131,7 @@ class RunFetcher {
|
|
|
102
131
|
}
|
|
103
132
|
|
|
104
133
|
async get(apiPath, params = []) {
|
|
134
|
+
await this.resolveProject();
|
|
105
135
|
const url = new URL(`/api/v2/${this.project}${apiPath}`, this.baseUrl);
|
|
106
136
|
for (const [key, value] of params) url.searchParams.append(key, value);
|
|
107
137
|
debug('GET', url.toString());
|
|
@@ -148,19 +178,31 @@ class RunFetcher {
|
|
|
148
178
|
}
|
|
149
179
|
|
|
150
180
|
/**
|
|
151
|
-
* Testruns with failures first; the list has no stack and no artifacts.
|
|
181
|
+
* Testruns with failures first, read in pages of TESTRUNS_PAGE_SIZE; the list has no stack and no artifacts.
|
|
152
182
|
*
|
|
153
|
-
* @param {{statuses?: string[], limit: number}} params
|
|
183
|
+
* @param {{statuses?: string[], limit: number}} params - limit is Infinity to read all testruns
|
|
154
184
|
* @returns {Promise<{testruns: object[], total: number}>}
|
|
155
185
|
*/
|
|
156
186
|
async listTestruns({ statuses = [], limit }) {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
187
|
+
const perPage = Math.min(limit, TESTRUNS_PAGE_SIZE);
|
|
188
|
+
const getPage = async page => {
|
|
189
|
+
const body = await this.get('/testruns', [
|
|
190
|
+
...this.testrunParams(statuses),
|
|
191
|
+
['sort', 'failure'],
|
|
192
|
+
['order', 'desc'],
|
|
193
|
+
['per_page', String(perPage)],
|
|
194
|
+
['page', String(page)],
|
|
195
|
+
]);
|
|
196
|
+
return { testruns: body.data || [], total: body.meta?.total || 0 };
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const first = await getPage(1);
|
|
200
|
+
const pagesCount = Math.ceil(Math.min(limit, first.total) / perPage);
|
|
201
|
+
const nextPages = Array.from({ length: Math.max(pagesCount - 1, 0) }, (_, index) => index + 2);
|
|
202
|
+
const rest = await mapWithLimit(nextPages, PARALLEL_REQUESTS, getPage);
|
|
203
|
+
|
|
204
|
+
const testruns = [first, ...rest].flatMap(page => page.testruns).slice(0, limit);
|
|
205
|
+
return { testruns, total: first.total };
|
|
164
206
|
}
|
|
165
207
|
|
|
166
208
|
/**
|
|
@@ -181,31 +223,70 @@ class RunFetcher {
|
|
|
181
223
|
});
|
|
182
224
|
}
|
|
183
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Defects linked to the testruns of the run; one page of TESTRUNS_PAGE_SIZE.
|
|
228
|
+
*
|
|
229
|
+
* @returns {Promise<{defects: object[], total: number}>}
|
|
230
|
+
*/
|
|
231
|
+
async listRunDefects() {
|
|
232
|
+
const body = await this.get('/defects', [
|
|
233
|
+
['run_id', this.runId],
|
|
234
|
+
['per_page', String(TESTRUNS_PAGE_SIZE)],
|
|
235
|
+
]);
|
|
236
|
+
return { defects: body.data || [], total: body.meta?.total || 0 };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Add the defects linked to the test case of each testrun to the testrun's own defects.
|
|
241
|
+
* A test that fails to load keeps only the testrun defects.
|
|
242
|
+
*
|
|
243
|
+
* @param {object[]} testruns
|
|
244
|
+
* @param {(testId: string, err: Error) => void} onError
|
|
245
|
+
*/
|
|
246
|
+
async addTestDefects(testruns, onError) {
|
|
247
|
+
const testIds = [...new Set(testruns.map(testrun => testrun.testId).filter(Boolean))];
|
|
248
|
+
const testDefects = await mapWithLimit(testIds, PARALLEL_REQUESTS, async testId => {
|
|
249
|
+
try {
|
|
250
|
+
const body = await this.get('/defects', [
|
|
251
|
+
['test_id', testId],
|
|
252
|
+
['per_page', String(TESTRUNS_PAGE_SIZE)],
|
|
253
|
+
]);
|
|
254
|
+
return body.data || [];
|
|
255
|
+
} catch (err) {
|
|
256
|
+
onError(testId, err);
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
const defectsByTest = new Map(testIds.map((testId, index) => [testId, testDefects[index]]));
|
|
261
|
+
|
|
262
|
+
return testruns.map(testrun => {
|
|
263
|
+
const defects = [...(testrun.defects || []), ...(defectsByTest.get(testrun.testId) || [])];
|
|
264
|
+
const unique = [...new Map(defects.map(defect => [defect.id, defect])).values()];
|
|
265
|
+
return { ...testrun, defects: unique };
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
184
269
|
/**
|
|
185
270
|
* Shared S3 credentials of the project, returned by an empty update of the run.
|
|
186
|
-
*
|
|
271
|
+
* Unless the run was already updated to resolve its project, skipped for scheduled runs (the update launches them)
|
|
272
|
+
* and unfinished mixed runs (it marks automated tests as started).
|
|
187
273
|
*
|
|
188
274
|
* @param {{status?: string, kind?: string}} run
|
|
189
275
|
* @returns {Promise<boolean>} whether credentials were applied
|
|
190
276
|
*/
|
|
191
277
|
async applySharedS3Credentials(run) {
|
|
192
|
-
|
|
278
|
+
const isUnfinishedMixed = run.kind === 'mixed' && !FINISHED_RUN_STATUSES.includes(run.status);
|
|
279
|
+
if (!this.reporterRun && (run.status === 'scheduled' || isUnfinishedMixed)) {
|
|
193
280
|
debug(`Shared S3 credentials are not requested for a ${run.status} ${run.kind} run`);
|
|
194
281
|
return false;
|
|
195
282
|
}
|
|
196
283
|
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
headers: { 'Content-Type': 'application/json' },
|
|
201
|
-
body: JSON.stringify({ api_key: this.apiKey }),
|
|
284
|
+
const body = await this.updateReporterRun().catch(err => {
|
|
285
|
+
debug('Failed to get shared S3 credentials:', err.message);
|
|
286
|
+
return { artifacts: null };
|
|
202
287
|
});
|
|
203
|
-
if (!response.ok) {
|
|
204
|
-
debug('Failed to get shared S3 credentials:', response.status);
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
288
|
|
|
208
|
-
const { artifacts } =
|
|
289
|
+
const { artifacts } = body;
|
|
209
290
|
if (!artifacts || !Object.keys(artifacts).length) return false;
|
|
210
291
|
setS3Credentials(artifacts);
|
|
211
292
|
return true;
|
|
@@ -271,7 +352,7 @@ function defaultArtifactsDir(run, runId) {
|
|
|
271
352
|
|
|
272
353
|
export {
|
|
273
354
|
TESTRUNS_DEFAULT_LIMIT,
|
|
274
|
-
|
|
355
|
+
TESTRUNS_PAGE_SIZE,
|
|
275
356
|
TESTRUN_STATUSES,
|
|
276
357
|
RunFetcher,
|
|
277
358
|
resolveRunTarget,
|
package/src/pipe/markdown.js
CHANGED
|
@@ -332,6 +332,9 @@ function renderTest(test) {
|
|
|
332
332
|
const logsBlock = renderLogs(test);
|
|
333
333
|
if (logsBlock) lines.push(logsBlock);
|
|
334
334
|
|
|
335
|
+
const defectsBlock = renderDefects(test);
|
|
336
|
+
if (defectsBlock) lines.push(defectsBlock);
|
|
337
|
+
|
|
335
338
|
const artifactsBlock = renderArtifacts(test);
|
|
336
339
|
if (artifactsBlock) lines.push(artifactsBlock);
|
|
337
340
|
|
|
@@ -497,6 +500,26 @@ function renderArtifacts(test) {
|
|
|
497
500
|
return lines.join('\n');
|
|
498
501
|
}
|
|
499
502
|
|
|
503
|
+
function renderDefects(test) {
|
|
504
|
+
if (!Array.isArray(test.defects) || !test.defects.length) return '';
|
|
505
|
+
|
|
506
|
+
const lines = ['**Defects**', ''];
|
|
507
|
+
for (const defect of test.defects) {
|
|
508
|
+
let line = `- ${defectLink(defect)}`;
|
|
509
|
+
if (defect.sourceKey && defect.title) line += ` ${mdInline(defect.title)}`;
|
|
510
|
+
const details = [defect.status, defect.severity].filter(Boolean);
|
|
511
|
+
if (details.length) line += ` (${details.join(', ')})`;
|
|
512
|
+
lines.push(line);
|
|
513
|
+
}
|
|
514
|
+
return lines.join('\n');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function defectLink(defect) {
|
|
518
|
+
const key = mdInline(defect.sourceKey || defect.title || 'defect');
|
|
519
|
+
if (!defect.url) return key;
|
|
520
|
+
return `[${key}](${defect.url})`;
|
|
521
|
+
}
|
|
522
|
+
|
|
500
523
|
function normalizeArtifact(raw) {
|
|
501
524
|
if (raw == null) return null;
|
|
502
525
|
|
|
@@ -755,6 +778,6 @@ function aggregateTestRetries(tests) {
|
|
|
755
778
|
return aggregated;
|
|
756
779
|
}
|
|
757
780
|
|
|
758
|
-
export { renderTests };
|
|
781
|
+
export { renderTests, defectLink };
|
|
759
782
|
|
|
760
783
|
export default MarkdownPipe;
|