@testrigor/testrigor-cli 0.0.42-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/bin/dev +18 -0
- package/bin/dev.cmd +3 -0
- package/bin/run +5 -0
- package/bin/run.cmd +3 -0
- package/dist/command-base.d.ts +27 -0
- package/dist/command-base.js +110 -0
- package/dist/commands/authenticate.d.ts +6 -0
- package/dist/commands/authenticate.js +59 -0
- package/dist/commands/test-suite/config.d.ts +14 -0
- package/dist/commands/test-suite/config.js +97 -0
- package/dist/commands/test-suite/run.d.ts +46 -0
- package/dist/commands/test-suite/run.js +728 -0
- package/dist/config/environments.d.ts +7 -0
- package/dist/config/environments.js +10 -0
- package/dist/flags/chrome-extension.d.ts +4 -0
- package/dist/flags/chrome-extension.js +45 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.js +2 -0
- package/dist/util/config.d.ts +33 -0
- package/dist/util/config.js +87 -0
- package/dist/util/parser.d.ts +4 -0
- package/dist/util/parser.js +83 -0
- package/dist/util/request-builder.d.ts +11 -0
- package/dist/util/request-builder.js +49 -0
- package/dist/util/trtc.d.ts +1 -0
- package/dist/util/trtc.js +109 -0
- package/dist/util/tunnel.d.ts +13 -0
- package/dist/util/tunnel.js +102 -0
- package/dist/util/utils.d.ts +16 -0
- package/dist/util/utils.js +97 -0
- package/oclif.manifest.json +265 -0
- package/package.json +98 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@oclif/core");
|
|
4
|
+
const Fs = require("@supercharge/fs");
|
|
5
|
+
const FormData = require("form-data");
|
|
6
|
+
const listr2_1 = require("listr2");
|
|
7
|
+
const node_fetch_commonjs_1 = require("node-fetch-commonjs");
|
|
8
|
+
const command_base_1 = require("../../command-base");
|
|
9
|
+
const environments_1 = require("../../config/environments");
|
|
10
|
+
const chrome_extension_1 = require("../../flags/chrome-extension");
|
|
11
|
+
const parser_1 = require("../../util/parser");
|
|
12
|
+
const request_builder_1 = require("../../util/request-builder");
|
|
13
|
+
const tunnel_1 = require("../../util/tunnel");
|
|
14
|
+
const utils_1 = require("../../util/utils");
|
|
15
|
+
const sleep = async (milliseconds) => {
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
17
|
+
};
|
|
18
|
+
const TEST_SUITE_STATUS = {
|
|
19
|
+
FINISHED: "Run finished",
|
|
20
|
+
FAILED: "Run failed",
|
|
21
|
+
RUNNING: "Run is still in progress",
|
|
22
|
+
CANCELED: "Run canceled",
|
|
23
|
+
UNKNOWN: "Unknown status",
|
|
24
|
+
};
|
|
25
|
+
const JUNIT_REPORT_STATUS = {
|
|
26
|
+
READY: "JUnit report is ready",
|
|
27
|
+
FAILED: "JUnit report generation is failed",
|
|
28
|
+
RUNNING: "JUnit report is still in progress",
|
|
29
|
+
};
|
|
30
|
+
class Run extends command_base_1.CommandBase {
|
|
31
|
+
initializeUrls(flags) {
|
|
32
|
+
const baseUrlWeb = flags["base-url-web"];
|
|
33
|
+
const baseUrlApi = flags["base-url-api"];
|
|
34
|
+
// Validation: if one is set, both must be set
|
|
35
|
+
if ((baseUrlWeb && !baseUrlApi) || (!baseUrlWeb && baseUrlApi)) {
|
|
36
|
+
core_1.ux.error("Both --base-url-web and --base-url-api must be set if one is provided");
|
|
37
|
+
}
|
|
38
|
+
// If both flags are set, validate and use them
|
|
39
|
+
if (baseUrlWeb && baseUrlApi) {
|
|
40
|
+
if (!(0, utils_1.isValidUrl)(baseUrlWeb)) {
|
|
41
|
+
core_1.ux.error(`Invalid URL for --base-url-web: ${baseUrlWeb}`);
|
|
42
|
+
}
|
|
43
|
+
if (!(0, utils_1.isValidUrl)(baseUrlApi)) {
|
|
44
|
+
core_1.ux.error(`Invalid URL for --base-url-api: ${baseUrlApi}`);
|
|
45
|
+
}
|
|
46
|
+
// Remove trailing slashes
|
|
47
|
+
const cleanBaseUrlWeb = baseUrlWeb.replace(/\/$/, "");
|
|
48
|
+
const cleanBaseUrlApi = baseUrlApi.replace(/\/$/, "");
|
|
49
|
+
this.TESTRIGOR_URL = cleanBaseUrlWeb;
|
|
50
|
+
this.TESTRIGOR_API_URL = `${cleanBaseUrlWeb}/api/v1`;
|
|
51
|
+
this.TESTRIGOR_API2_URL = `${cleanBaseUrlApi}/api/v1`;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// If neither flag is set, use defaults from config
|
|
55
|
+
const prodConfig = environments_1.environments.prod;
|
|
56
|
+
const baseWebUrl = prodConfig.TESTRIGOR_WEB_URL;
|
|
57
|
+
const baseApiUrl = prodConfig.TESTRIGOR_API_URL;
|
|
58
|
+
this.TESTRIGOR_URL = baseWebUrl;
|
|
59
|
+
this.TESTRIGOR_API_URL = `${baseWebUrl}/api/v1`;
|
|
60
|
+
this.TESTRIGOR_API2_URL = `${baseApiUrl}/api/v1`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async run() {
|
|
64
|
+
const flags = this.flags;
|
|
65
|
+
const args = this.args;
|
|
66
|
+
this.initializeUrls(flags);
|
|
67
|
+
const token = await this.resolveToken();
|
|
68
|
+
const testSuiteId = await this.resolveTestSuiteId(args.testSuiteId);
|
|
69
|
+
const minPort = flags["min-port"];
|
|
70
|
+
const maxPort = flags["max-port"];
|
|
71
|
+
const url = flags["url"];
|
|
72
|
+
const filePath = flags["file-path"];
|
|
73
|
+
const variablesPath = flags["variables-path"];
|
|
74
|
+
const testCasesPath = flags["test-cases-path"];
|
|
75
|
+
const rulesPath = flags["rules-path"];
|
|
76
|
+
const settingsPath = flags["settings-path"];
|
|
77
|
+
const testCaseUuid = flags["test-case-uuid"];
|
|
78
|
+
const isAutoCreateAIBasedRulesEnabled = flags["auto-create-ai-rules"];
|
|
79
|
+
const explicitMutations = flags["explicit-mutations"];
|
|
80
|
+
const labels = flags["labels"];
|
|
81
|
+
const excludedLabels = flags["excluded-labels"];
|
|
82
|
+
const chromeExtensions = flags["chrome-extension"];
|
|
83
|
+
const context = {
|
|
84
|
+
configuration: {
|
|
85
|
+
forceCancelPreviousTesting: true,
|
|
86
|
+
baselineMutations: [],
|
|
87
|
+
settings: {},
|
|
88
|
+
},
|
|
89
|
+
tunnel: new tunnel_1.default(),
|
|
90
|
+
};
|
|
91
|
+
if (labels) {
|
|
92
|
+
context.configuration.labels = labels;
|
|
93
|
+
}
|
|
94
|
+
if (excludedLabels) {
|
|
95
|
+
context.configuration.excludedLabels = excludedLabels;
|
|
96
|
+
}
|
|
97
|
+
if (minPort > maxPort) {
|
|
98
|
+
core_1.ux.error("--minPort cannot be greater than --maxPort");
|
|
99
|
+
}
|
|
100
|
+
if (testCaseUuid) {
|
|
101
|
+
context.configuration.testCaseUuids = [testCaseUuid];
|
|
102
|
+
}
|
|
103
|
+
if (isAutoCreateAIBasedRulesEnabled) {
|
|
104
|
+
context.configuration.isAutoCreateAIBasedRulesEnabled =
|
|
105
|
+
isAutoCreateAIBasedRulesEnabled;
|
|
106
|
+
}
|
|
107
|
+
if (explicitMutations) {
|
|
108
|
+
context.configuration.explicitMutations = explicitMutations;
|
|
109
|
+
}
|
|
110
|
+
if (variablesPath) {
|
|
111
|
+
try {
|
|
112
|
+
const file = await Fs.content(variablesPath);
|
|
113
|
+
context.configuration.storedValues = JSON.parse(file.toString());
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
core_1.ux.error("--variables-path must be a valid JSON path");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (testCasesPath) {
|
|
120
|
+
try {
|
|
121
|
+
const testCases = await (0, parser_1.parseTestCases)(testCasesPath);
|
|
122
|
+
if (testCases.length) {
|
|
123
|
+
context.configuration.baselineMutations = testCases;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
core_1.ux.error("--test-cases-path must be a valid JSON path");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (rulesPath) {
|
|
131
|
+
try {
|
|
132
|
+
const rules = await (0, parser_1.parseRules)(rulesPath);
|
|
133
|
+
if (rules.length) {
|
|
134
|
+
context.configuration.ruleMutations = rules;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
core_1.ux.error("--rules-path must be a valid JSON path");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (settingsPath) {
|
|
142
|
+
try {
|
|
143
|
+
const settings = await (0, parser_1.parseSettings)(settingsPath);
|
|
144
|
+
if (settings) {
|
|
145
|
+
context.configuration.settings = settings;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
core_1.ux.error("--settings-path must be a valid JSON/YAML path");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (filePath) {
|
|
153
|
+
if (url) {
|
|
154
|
+
core_1.ux.info("WARNING: File path will be ignored because --url is used");
|
|
155
|
+
delete flags["file-path"];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const fileExists = await Fs.isFile(filePath);
|
|
159
|
+
if (!fileExists || !(0, utils_1.isValidFileExtensionToUpload)(filePath)) {
|
|
160
|
+
core_1.ux.error("--file-path must be one of the valid format types: [zip, exe, msi, apk, aab, ipa, app]");
|
|
161
|
+
}
|
|
162
|
+
const fileName = Fs.basename(filePath);
|
|
163
|
+
const fileSize = Fs.statSync(filePath).size;
|
|
164
|
+
context.fileProperties = { fileName, fileSize };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (chromeExtensions?.length) {
|
|
168
|
+
context.chromeExtensions = chromeExtensions;
|
|
169
|
+
}
|
|
170
|
+
super.setTunnel(context.tunnel);
|
|
171
|
+
const tasks = [
|
|
172
|
+
{
|
|
173
|
+
enabled: () => flags.localhost,
|
|
174
|
+
task: (ctx, task) => {
|
|
175
|
+
if (flags.verbose) {
|
|
176
|
+
task.title = "Initializing tunnel";
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
task.title = "Initializing";
|
|
180
|
+
}
|
|
181
|
+
return task.newListr([
|
|
182
|
+
{
|
|
183
|
+
task: async (ctx, task) => {
|
|
184
|
+
if (flags.verbose) {
|
|
185
|
+
task.title = "Downloading binary";
|
|
186
|
+
}
|
|
187
|
+
await ctx.tunnel.initialize();
|
|
188
|
+
},
|
|
189
|
+
rendererOptions: {
|
|
190
|
+
timer: listr2_1.PRESET_TIMER,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
task: async (ctx, task) => {
|
|
195
|
+
if (flags.verbose) {
|
|
196
|
+
task.title = "Starting tunnel";
|
|
197
|
+
}
|
|
198
|
+
await ctx.tunnel.start(minPort, maxPort);
|
|
199
|
+
},
|
|
200
|
+
exitOnError: true,
|
|
201
|
+
rendererOptions: {
|
|
202
|
+
timer: listr2_1.PRESET_TIMER,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
enabled: (ctx) => ctx.tunnel.isRunning(),
|
|
207
|
+
task: (ctx, task) => {
|
|
208
|
+
if (flags.verbose) {
|
|
209
|
+
task.title = `Tunnel started at port ${ctx.tunnel.getPort()}`;
|
|
210
|
+
}
|
|
211
|
+
ctx.configuration.settings = {
|
|
212
|
+
...ctx.configuration.settings,
|
|
213
|
+
httpProxyString: `tunnel.testrigor.com:${ctx.tunnel.getPort()}`,
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
], { concurrent: false, rendererOptions: { collapseSubtasks: false } });
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
title: "Running test suite",
|
|
222
|
+
task: async (ctx, task) => task.newListr([
|
|
223
|
+
{
|
|
224
|
+
// Checking last run id
|
|
225
|
+
task: async (ctx) => {
|
|
226
|
+
const statusResponse = await this.fetchStatus(testSuiteId, token, flags.branch, ctx.configuration.labels, ctx.configuration.excludedLabels);
|
|
227
|
+
context.lastRunId = (0, utils_1.extractRunIdFromDetailsUrl)(statusResponse.detailsUrl);
|
|
228
|
+
},
|
|
229
|
+
exitOnError: false,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
enabled: () => !flags["file-path"] && !flags["chrome-extension"],
|
|
233
|
+
task: async (ctx) => {
|
|
234
|
+
if (flags.url) {
|
|
235
|
+
ctx.configuration.url = flags.url;
|
|
236
|
+
}
|
|
237
|
+
if (flags.branch && flags.commit) {
|
|
238
|
+
ctx.configuration.branch = {
|
|
239
|
+
name: flags.branch,
|
|
240
|
+
commit: flags.commit,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const response = await this.callRetest(testSuiteId, ctx.configuration, token);
|
|
244
|
+
ctx.response = (await response.json());
|
|
245
|
+
if (!response.ok) {
|
|
246
|
+
const errorMessage = JSON.stringify(ctx.response);
|
|
247
|
+
throw new Error(`Test suite failed to start: ${ctx.response?.msg ||
|
|
248
|
+
ctx.response?.errName ||
|
|
249
|
+
errorMessage}`);
|
|
250
|
+
}
|
|
251
|
+
if (ctx.response?.taskId) {
|
|
252
|
+
task.title = `Running test suite [ ${this.TESTRIGOR_URL}/test-suites/${testSuiteId}/runs/${ctx.response?.taskId} ]`;
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
exitOnError: false,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
title: "Uploading Chrome extension(s)",
|
|
259
|
+
enabled: () => !!context.chromeExtensions?.length,
|
|
260
|
+
task: async (ctx) => {
|
|
261
|
+
if (flags.url) {
|
|
262
|
+
ctx.configuration.url = flags.url;
|
|
263
|
+
}
|
|
264
|
+
if (flags.branch && flags.commit) {
|
|
265
|
+
ctx.configuration.branch = {
|
|
266
|
+
name: flags.branch,
|
|
267
|
+
commit: flags.commit,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const { form, cleanup } = (0, request_builder_1.buildChromeExtensionFormData)(ctx.chromeExtensions);
|
|
271
|
+
form.append("data", JSON.stringify(ctx.configuration));
|
|
272
|
+
const controller = new AbortController();
|
|
273
|
+
const timeoutId = setTimeout(() => controller.abort(), utils_1.UPLOAD_TIMEOUT_MS);
|
|
274
|
+
try {
|
|
275
|
+
// Prepare headers
|
|
276
|
+
const requestHeaders = {
|
|
277
|
+
...form.getHeaders(),
|
|
278
|
+
Accept: "application/json",
|
|
279
|
+
"auth-token": token,
|
|
280
|
+
};
|
|
281
|
+
const response = await (0, node_fetch_commonjs_1.default)(`${this.TESTRIGOR_API2_URL}/apps/${testSuiteId}/retest`, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
body: form,
|
|
284
|
+
headers: requestHeaders,
|
|
285
|
+
signal: controller.signal,
|
|
286
|
+
});
|
|
287
|
+
const retestApi2Response = (await response.json());
|
|
288
|
+
if (!response.ok || retestApi2Response.status >= 300) {
|
|
289
|
+
throw new Error(`Chrome extension upload failed: ${retestApi2Response?.message ||
|
|
290
|
+
retestApi2Response?.errors}`);
|
|
291
|
+
}
|
|
292
|
+
ctx.response = {
|
|
293
|
+
result: "ok",
|
|
294
|
+
msg: retestApi2Response.message,
|
|
295
|
+
taskId: retestApi2Response.data?.taskId,
|
|
296
|
+
queueId: retestApi2Response.data?.queueId,
|
|
297
|
+
};
|
|
298
|
+
if (ctx.response?.taskId) {
|
|
299
|
+
task.title = `Running test suite [ ${this.TESTRIGOR_URL}/test-suites/${testSuiteId}/runs/${ctx.response?.taskId} ]`;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (error.name === "AbortError") {
|
|
304
|
+
throw new Error("Chrome extension upload timed out. Please check your network connection.");
|
|
305
|
+
}
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
clearTimeout(timeoutId);
|
|
310
|
+
cleanup();
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
exitOnError: false,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
title: "Uploading file",
|
|
317
|
+
enabled: () => context.fileProperties &&
|
|
318
|
+
context.fileProperties.fileSize < utils_1.MAX_FILE_SIZE_100_MB,
|
|
319
|
+
task: async (ctx) => {
|
|
320
|
+
if (flags.branch && flags.commit) {
|
|
321
|
+
ctx.configuration.branch = {
|
|
322
|
+
name: flags.branch,
|
|
323
|
+
commit: flags.commit,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
if (!flags["file-path"]) {
|
|
327
|
+
throw new Error("file-path is not provided");
|
|
328
|
+
}
|
|
329
|
+
const configuration = ctx.configuration;
|
|
330
|
+
const filePath = flags["file-path"];
|
|
331
|
+
const form = new FormData();
|
|
332
|
+
form.append("file", Fs.createReadStream(filePath));
|
|
333
|
+
form.append("data", JSON.stringify(configuration));
|
|
334
|
+
const response = await (0, node_fetch_commonjs_1.default)(`${this.TESTRIGOR_API2_URL}/apps/${testSuiteId}/retest`, {
|
|
335
|
+
method: "POST",
|
|
336
|
+
body: form,
|
|
337
|
+
headers: {
|
|
338
|
+
...form.getHeaders(),
|
|
339
|
+
Accept: "application/json",
|
|
340
|
+
"auth-token": token,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
const retestApi2Response = (await response.json());
|
|
344
|
+
if (!response.ok || retestApi2Response.status >= 300) {
|
|
345
|
+
throw new Error(`Test suite failed to start: ${retestApi2Response?.message ||
|
|
346
|
+
retestApi2Response?.errors}`);
|
|
347
|
+
}
|
|
348
|
+
ctx.response = {
|
|
349
|
+
result: "ok",
|
|
350
|
+
msg: retestApi2Response.message,
|
|
351
|
+
taskId: retestApi2Response.data?.taskId,
|
|
352
|
+
queueId: retestApi2Response.data?.queueId,
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
exitOnError: false,
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
title: "Uploading file (> 100 MB)",
|
|
359
|
+
enabled: () => context.fileProperties &&
|
|
360
|
+
context.fileProperties.fileSize >= utils_1.MAX_FILE_SIZE_100_MB,
|
|
361
|
+
task: async (ctx) => {
|
|
362
|
+
if (flags.branch && flags.commit) {
|
|
363
|
+
ctx.configuration.branch = {
|
|
364
|
+
name: flags.branch,
|
|
365
|
+
commit: flags.commit,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (!flags["file-path"]) {
|
|
369
|
+
throw new Error("file-path is not provided");
|
|
370
|
+
}
|
|
371
|
+
if (!ctx.fileProperties) {
|
|
372
|
+
throw new Error(`Error getting details of file provided by --file-path ${flags["file-path"]}`);
|
|
373
|
+
}
|
|
374
|
+
const configuration = ctx.configuration;
|
|
375
|
+
const filePath = flags["file-path"];
|
|
376
|
+
const response = await this.uploadFile(task, testSuiteId, configuration, filePath, token, ctx.fileProperties);
|
|
377
|
+
ctx.response = (await response.json());
|
|
378
|
+
if (!response.ok) {
|
|
379
|
+
throw new Error(`Test suite failed to start: ${ctx.response?.msg || ctx.response?.errName}`);
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
exitOnError: false,
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
title: "Waiting for test suite run creation",
|
|
386
|
+
enabled: (ctx) => ctx.response?.queueId,
|
|
387
|
+
task: async (ctx, currentTask) => {
|
|
388
|
+
const detailsUrl = await this.waitForTestSuiteToBeCreated(testSuiteId, token, ctx.lastRunId, flags.branch, ctx.configuration.labels, ctx.configuration.excludedLabels);
|
|
389
|
+
task.title = `Running test suite [ ${detailsUrl} ]`;
|
|
390
|
+
currentTask.title = "Test suite run created successfully";
|
|
391
|
+
},
|
|
392
|
+
exitOnError: false,
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
title: "Checking status",
|
|
396
|
+
enabled: (ctx) => ctx.response?.result === "ok" && !flags.async,
|
|
397
|
+
task: async (ctx, task) => task.newListr([
|
|
398
|
+
{
|
|
399
|
+
task: async (ctx, statusTask) => {
|
|
400
|
+
await this.waitForTestSuiteToFinish(testSuiteId, token, statusTask, flags.branch, ctx.configuration.labels, ctx.configuration.excludedLabels);
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
]),
|
|
404
|
+
exitOnError: false,
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
title: "Generating JUnit report",
|
|
408
|
+
enabled: (ctx) => ctx.response?.result === "ok" &&
|
|
409
|
+
!flags.async &&
|
|
410
|
+
flags["junit-report-save-path"] &&
|
|
411
|
+
ctx.response?.taskId,
|
|
412
|
+
task: async (ctx, task) => task.newListr([
|
|
413
|
+
{
|
|
414
|
+
task: async (ctx, statusTask) => {
|
|
415
|
+
await this.startAndWaitForJunitReportToFinish(testSuiteId, String(ctx.response?.taskId), String(flags["junit-report-save-path"]), token, statusTask);
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
]),
|
|
419
|
+
exitOnError: false,
|
|
420
|
+
},
|
|
421
|
+
], {
|
|
422
|
+
concurrent: false,
|
|
423
|
+
rendererOptions: { collapseSubtasks: false, timer: listr2_1.PRESET_TIMER },
|
|
424
|
+
fallbackRendererCondition: true,
|
|
425
|
+
fallbackRendererOptions: { timer: listr2_1.PRESET_TIMER },
|
|
426
|
+
}),
|
|
427
|
+
exitOnError: false,
|
|
428
|
+
rendererOptions: {
|
|
429
|
+
timer: listr2_1.PRESET_TIMER,
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
enabled: (ctx) => ctx.tunnel.isRunning(),
|
|
434
|
+
task: async (ctx, task) => {
|
|
435
|
+
if (flags.verbose) {
|
|
436
|
+
task.title = "Stopping tunnel";
|
|
437
|
+
}
|
|
438
|
+
await ctx.tunnel.stop();
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
];
|
|
442
|
+
const runner = new listr2_1.Listr(tasks, {
|
|
443
|
+
ctx: context,
|
|
444
|
+
concurrent: false,
|
|
445
|
+
collectErrors: "minimal",
|
|
446
|
+
renderer: (0, utils_1.rendererToUse)(),
|
|
447
|
+
});
|
|
448
|
+
await runner.run().catch(() => this.exit(1));
|
|
449
|
+
if (runner.errors?.length > 0) {
|
|
450
|
+
core_1.ux.error(runner.errors[0].message, { exit: 1 });
|
|
451
|
+
}
|
|
452
|
+
this.exit(0);
|
|
453
|
+
}
|
|
454
|
+
async fetchStatus(testSuiteId, token, branch, labels, excludedLabels) {
|
|
455
|
+
const statusApiEndpoint = this.getStatusApiEndpoint(testSuiteId, branch, labels, excludedLabels);
|
|
456
|
+
const response = await (0, node_fetch_commonjs_1.default)(statusApiEndpoint, {
|
|
457
|
+
method: "GET",
|
|
458
|
+
headers: {
|
|
459
|
+
Accept: "application/json",
|
|
460
|
+
"auth-token": token,
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
if (!response.ok) {
|
|
464
|
+
return {};
|
|
465
|
+
}
|
|
466
|
+
return response.json();
|
|
467
|
+
}
|
|
468
|
+
getStatusApiEndpoint(testSuiteId, branch, labels, excludedLabels) {
|
|
469
|
+
const apiEndpoint = `${this.TESTRIGOR_API_URL}/apps/${testSuiteId}/status`;
|
|
470
|
+
const queryParams = new URLSearchParams();
|
|
471
|
+
if (branch) {
|
|
472
|
+
queryParams.append("branchName", branch);
|
|
473
|
+
}
|
|
474
|
+
if (labels && labels.length > 0) {
|
|
475
|
+
queryParams.append("labels", labels.join(","));
|
|
476
|
+
}
|
|
477
|
+
if (excludedLabels && excludedLabels.length > 0) {
|
|
478
|
+
queryParams.append("excludedLabels", excludedLabels.join(","));
|
|
479
|
+
}
|
|
480
|
+
const queryString = queryParams.toString();
|
|
481
|
+
return queryString ? `${apiEndpoint}?${queryString}` : apiEndpoint;
|
|
482
|
+
}
|
|
483
|
+
async callRetest(testSuiteId, configuration, token) {
|
|
484
|
+
return (0, node_fetch_commonjs_1.default)(`${this.TESTRIGOR_API_URL}/apps/${testSuiteId}/retest`, {
|
|
485
|
+
method: "POST",
|
|
486
|
+
body: JSON.stringify(configuration),
|
|
487
|
+
headers: {
|
|
488
|
+
"Content-Type": "application/json",
|
|
489
|
+
Accept: "application/json",
|
|
490
|
+
"auth-token": token,
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
async uploadFile(task, testSuiteId, configuration, filePath, token, fileProperties) {
|
|
495
|
+
const body = {
|
|
496
|
+
fileName: fileProperties.fileName,
|
|
497
|
+
};
|
|
498
|
+
const preSignedUrl = await (0, node_fetch_commonjs_1.default)(`${this.TESTRIGOR_API2_URL}/apps/${testSuiteId}/retest/pre-signed-url`, {
|
|
499
|
+
method: "POST",
|
|
500
|
+
body: JSON.stringify(body),
|
|
501
|
+
headers: {
|
|
502
|
+
"Content-Type": "application/json",
|
|
503
|
+
Accept: "application/json",
|
|
504
|
+
"auth-token": token,
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
const preSignedUrlResponse = (await preSignedUrl.json());
|
|
508
|
+
if (!preSignedUrl.ok ||
|
|
509
|
+
preSignedUrlResponse.status !== 200 ||
|
|
510
|
+
!preSignedUrlResponse.data) {
|
|
511
|
+
throw new Error(`Failed to prepare upload: ${preSignedUrlResponse.message}`);
|
|
512
|
+
}
|
|
513
|
+
const preSignedUrlData = preSignedUrlResponse.data;
|
|
514
|
+
const fileToBeUploaded = Fs.createReadStream(filePath);
|
|
515
|
+
const uploadFileResponse = await (0, node_fetch_commonjs_1.default)(preSignedUrlData.url, {
|
|
516
|
+
method: "PUT",
|
|
517
|
+
body: fileToBeUploaded,
|
|
518
|
+
headers: {
|
|
519
|
+
"Content-length": `${fileProperties.fileSize}`,
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
if (!uploadFileResponse.ok) {
|
|
523
|
+
throw new Error(`Failed to upload file: ${uploadFileResponse.statusText}`);
|
|
524
|
+
}
|
|
525
|
+
configuration.fileUrl = (0, utils_1.extractUrlWithoutQueryParams)(preSignedUrlData.url);
|
|
526
|
+
return this.callRetest(testSuiteId, configuration, token);
|
|
527
|
+
}
|
|
528
|
+
async waitForTestSuiteToBeCreated(testSuiteId, token, lastRunId, branch, labels, excludedLabels) {
|
|
529
|
+
for (;;) {
|
|
530
|
+
const statusResponse = await this.fetchStatus(testSuiteId, token, branch, labels, excludedLabels);
|
|
531
|
+
const currentRunId = (0, utils_1.extractRunIdFromDetailsUrl)(statusResponse.detailsUrl);
|
|
532
|
+
if (currentRunId !== lastRunId) {
|
|
533
|
+
return `${this.TESTRIGOR_URL}/suites/${testSuiteId}/runs/${currentRunId}`;
|
|
534
|
+
}
|
|
535
|
+
await sleep(10000);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async waitForTestSuiteToFinish(testSuiteId, token, task, branch, labels, excludedLabels) {
|
|
539
|
+
const statusApiEndpoint = this.getStatusApiEndpoint(testSuiteId, branch, labels, excludedLabels);
|
|
540
|
+
let hasReceivedValidStatus = false;
|
|
541
|
+
for (;;) {
|
|
542
|
+
// eslint-disable-next-line no-await-in-loop
|
|
543
|
+
let response = await this.fetchTestSuiteStatus(statusApiEndpoint, token);
|
|
544
|
+
if (response.status === 200) {
|
|
545
|
+
hasReceivedValidStatus = true;
|
|
546
|
+
task.title = TEST_SUITE_STATUS.FINISHED;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
if (response.status === 230) {
|
|
550
|
+
hasReceivedValidStatus = true;
|
|
551
|
+
throw new Error(TEST_SUITE_STATUS.FAILED);
|
|
552
|
+
}
|
|
553
|
+
if (response.status === 229) {
|
|
554
|
+
hasReceivedValidStatus = true;
|
|
555
|
+
throw new Error(TEST_SUITE_STATUS.CANCELED);
|
|
556
|
+
}
|
|
557
|
+
//RUNNING OR NEW
|
|
558
|
+
if (response.status === 228 || response.status === 227) {
|
|
559
|
+
hasReceivedValidStatus = true;
|
|
560
|
+
task.title = TEST_SUITE_STATUS.RUNNING;
|
|
561
|
+
if ((0, utils_1.isRunningOnCi)()) {
|
|
562
|
+
task.output = TEST_SUITE_STATUS.RUNNING;
|
|
563
|
+
}
|
|
564
|
+
await sleep(10000);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
// Unknown status handling
|
|
568
|
+
if (hasReceivedValidStatus) {
|
|
569
|
+
// Retry with exponential backoff after valid status was seen
|
|
570
|
+
response = await this.retryForValidStatus(statusApiEndpoint, token, response.status, task);
|
|
571
|
+
// Continue the loop to process the (hopefully valid) response
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
// First occurrence of unknown status - fail immediately
|
|
576
|
+
throw new Error(`Unknown status: ${response.status}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async fetchTestSuiteStatus(statusApiEndpoint, token) {
|
|
581
|
+
return await (0, node_fetch_commonjs_1.default)(statusApiEndpoint, {
|
|
582
|
+
method: "GET",
|
|
583
|
+
headers: {
|
|
584
|
+
Accept: "application/json",
|
|
585
|
+
"auth-token": token,
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
async retryForValidStatus(statusApiEndpoint, token, initialStatus, task) {
|
|
590
|
+
// Retry delays: 1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120 seconds
|
|
591
|
+
const retryDelays = [1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
|
|
592
|
+
for (const delaySeconds of retryDelays) {
|
|
593
|
+
if ((0, utils_1.isRunningOnCi)()) {
|
|
594
|
+
task.output = `Unknown status ${initialStatus}, retrying in ${delaySeconds}s...`;
|
|
595
|
+
}
|
|
596
|
+
// eslint-disable-next-line no-await-in-loop
|
|
597
|
+
await sleep(delaySeconds * 1000);
|
|
598
|
+
// eslint-disable-next-line no-await-in-loop
|
|
599
|
+
const retryResponse = await this.fetchTestSuiteStatus(statusApiEndpoint, token);
|
|
600
|
+
// Check if we got a valid status back
|
|
601
|
+
if ([200, 227, 228, 229, 230].includes(retryResponse.status)) {
|
|
602
|
+
if ((0, utils_1.isRunningOnCi)()) {
|
|
603
|
+
task.output = `Recovered with status ${retryResponse.status}`;
|
|
604
|
+
}
|
|
605
|
+
return retryResponse;
|
|
606
|
+
}
|
|
607
|
+
// Update status for next retry
|
|
608
|
+
initialStatus = retryResponse.status;
|
|
609
|
+
}
|
|
610
|
+
// If we exhausted all retries, throw error
|
|
611
|
+
throw new Error(`Unknown status: ${initialStatus} (failed after ${retryDelays.length} retries)`);
|
|
612
|
+
}
|
|
613
|
+
async startAndWaitForJunitReportToFinish(testSuiteId, runId, savePath, token, task) {
|
|
614
|
+
const url = `${this.TESTRIGOR_API2_URL}/apps/${testSuiteId}/runs/${runId}/junit_report`;
|
|
615
|
+
for (;;) {
|
|
616
|
+
// eslint-disable-next-line no-await-in-loop
|
|
617
|
+
const response = await (0, node_fetch_commonjs_1.default)(url, {
|
|
618
|
+
method: "GET",
|
|
619
|
+
headers: {
|
|
620
|
+
Accept: "application/json",
|
|
621
|
+
"auth-token": token,
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
if (response.status === 200) {
|
|
625
|
+
const contentType = response.headers.get("content-type");
|
|
626
|
+
if (contentType !== "application/xml") {
|
|
627
|
+
throw new Error(`Unexpected contentType of junit report: ${contentType}`);
|
|
628
|
+
}
|
|
629
|
+
const xmlData = await response.text();
|
|
630
|
+
await Fs.writeFile(savePath, xmlData);
|
|
631
|
+
task.title = JUNIT_REPORT_STATUS.READY;
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
else if (response.status !== 228) {
|
|
635
|
+
throw new Error(`JUnit report generation failed with status: ${response.status}`);
|
|
636
|
+
}
|
|
637
|
+
task.title = JUNIT_REPORT_STATUS.RUNNING;
|
|
638
|
+
if ((0, utils_1.isRunningOnCi)()) {
|
|
639
|
+
task.output = JUNIT_REPORT_STATUS.RUNNING;
|
|
640
|
+
}
|
|
641
|
+
// eslint-disable-next-line no-await-in-loop
|
|
642
|
+
await sleep(10000);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
exports.default = Run;
|
|
647
|
+
Run.description = "Run all tests of a Test Suite";
|
|
648
|
+
Run.examples = [];
|
|
649
|
+
Run.flags = {
|
|
650
|
+
async: core_1.Flags.boolean({
|
|
651
|
+
char: "w",
|
|
652
|
+
description: "Don't wait for the test suite to finish",
|
|
653
|
+
aliases: ["nowait"],
|
|
654
|
+
default: false,
|
|
655
|
+
}),
|
|
656
|
+
branch: core_1.Flags.string({
|
|
657
|
+
description: "The branch name",
|
|
658
|
+
env: "TR_BRANCH",
|
|
659
|
+
dependsOn: ["commit"],
|
|
660
|
+
}),
|
|
661
|
+
commit: core_1.Flags.string({
|
|
662
|
+
description: "The commit",
|
|
663
|
+
env: "TR_COMMIT",
|
|
664
|
+
dependsOn: ["branch"],
|
|
665
|
+
}),
|
|
666
|
+
localhost: core_1.Flags.boolean({
|
|
667
|
+
description: "Localhost run",
|
|
668
|
+
aliases: ["tunnel"],
|
|
669
|
+
dependsOn: ["url"],
|
|
670
|
+
}),
|
|
671
|
+
url: core_1.Flags.string({
|
|
672
|
+
description: "The url",
|
|
673
|
+
}),
|
|
674
|
+
"file-path": core_1.Flags.string({
|
|
675
|
+
description: "Path to the file stored locally. It will upload the new file to be used. File type should be according to the file type accepted for the Test Suite type of testing. If the parameter --url is used, this parameter will be ignored.",
|
|
676
|
+
}),
|
|
677
|
+
"junit-report-save-path": core_1.Flags.string({
|
|
678
|
+
description: "JUnit report save path",
|
|
679
|
+
}),
|
|
680
|
+
"variables-path": core_1.Flags.string({
|
|
681
|
+
description: "Variables path",
|
|
682
|
+
}),
|
|
683
|
+
"test-cases-path": core_1.Flags.string({
|
|
684
|
+
description: "Test cases files",
|
|
685
|
+
}),
|
|
686
|
+
"rules-path": core_1.Flags.string({
|
|
687
|
+
description: "Rules path",
|
|
688
|
+
}),
|
|
689
|
+
"settings-path": core_1.Flags.string({
|
|
690
|
+
description: "Settings path",
|
|
691
|
+
}),
|
|
692
|
+
"test-case-uuid": core_1.Flags.string({
|
|
693
|
+
description: "Test case uuid",
|
|
694
|
+
dependsOn: ["url"],
|
|
695
|
+
}),
|
|
696
|
+
"auto-create-ai-rules": core_1.Flags.boolean({
|
|
697
|
+
description: "Enable auto create AI based rules",
|
|
698
|
+
default: false,
|
|
699
|
+
}),
|
|
700
|
+
"explicit-mutations": core_1.Flags.boolean({
|
|
701
|
+
description: 'In cases where you are always using mutations, such as storing test cases in your own repository, and you always send us the set of test cases you have through our API you should send "--explicit-mutations"',
|
|
702
|
+
default: false,
|
|
703
|
+
}),
|
|
704
|
+
labels: core_1.Flags.string({
|
|
705
|
+
description: "Run tests of specific labels",
|
|
706
|
+
multiple: true,
|
|
707
|
+
}),
|
|
708
|
+
"excluded-labels": core_1.Flags.string({
|
|
709
|
+
description: "Not run tests of specific labels",
|
|
710
|
+
multiple: true,
|
|
711
|
+
}),
|
|
712
|
+
"base-url-web": core_1.Flags.string({
|
|
713
|
+
description: "Base URL for web interface (e.g., https://app.testrigor.com)",
|
|
714
|
+
}),
|
|
715
|
+
"base-url-api": core_1.Flags.string({
|
|
716
|
+
description: "Base URL for API2 (e.g., https://api2.testrigor.com)",
|
|
717
|
+
}),
|
|
718
|
+
"chrome-extension": (0, chrome_extension_1.ChromeExtensionFlag)({
|
|
719
|
+
exclusive: ["file-path"],
|
|
720
|
+
}),
|
|
721
|
+
};
|
|
722
|
+
Run.args = {
|
|
723
|
+
testSuiteId: core_1.Args.string({
|
|
724
|
+
name: "testSuiteId",
|
|
725
|
+
description: "The test suite id (optional if set in config)",
|
|
726
|
+
required: false,
|
|
727
|
+
}),
|
|
728
|
+
};
|