@testingbot/cli 1.0.1 → 1.0.3
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 +84 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +55 -8
- package/dist/logger.js +4 -4
- package/dist/models/espresso_options.d.ts +9 -0
- package/dist/models/espresso_options.d.ts.map +1 -1
- package/dist/models/espresso_options.js +14 -0
- package/dist/models/maestro_options.d.ts +20 -7
- package/dist/models/maestro_options.d.ts.map +1 -1
- package/dist/models/maestro_options.js +22 -9
- package/dist/models/testingbot_error.d.ts +3 -0
- package/dist/models/testingbot_error.d.ts.map +1 -1
- package/dist/models/testingbot_error.js +5 -0
- package/dist/models/xcuitest_options.d.ts +9 -0
- package/dist/models/xcuitest_options.d.ts.map +1 -1
- package/dist/models/xcuitest_options.js +14 -0
- package/dist/providers/base_provider.d.ts +119 -0
- package/dist/providers/base_provider.d.ts.map +1 -0
- package/dist/providers/base_provider.js +296 -0
- package/dist/providers/espresso.d.ts +14 -21
- package/dist/providers/espresso.d.ts.map +1 -1
- package/dist/providers/espresso.js +50 -181
- package/dist/providers/login.d.ts +1 -0
- package/dist/providers/login.d.ts.map +1 -1
- package/dist/providers/login.js +16 -7
- package/dist/providers/maestro.d.ts +73 -21
- package/dist/providers/maestro.d.ts.map +1 -1
- package/dist/providers/maestro.js +842 -276
- package/dist/providers/xcuitest.d.ts +14 -21
- package/dist/providers/xcuitest.d.ts.map +1 -1
- package/dist/providers/xcuitest.js +50 -181
- package/dist/upload.d.ts +10 -0
- package/dist/upload.d.ts.map +1 -1
- package/dist/upload.js +46 -21
- package/dist/utils/connectivity.d.ts +26 -0
- package/dist/utils/connectivity.d.ts.map +1 -0
- package/dist/utils/connectivity.js +131 -0
- package/dist/utils/error-helpers.d.ts +26 -0
- package/dist/utils/error-helpers.d.ts.map +1 -0
- package/dist/utils/error-helpers.js +237 -0
- package/dist/utils/file-type-detector.d.ts +4 -1
- package/dist/utils/file-type-detector.d.ts.map +1 -1
- package/dist/utils/file-type-detector.js +30 -6
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +8 -3
- package/package.json +2 -2
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import XCUITestOptions from '../models/xcuitest_options';
|
|
2
2
|
import Credentials from '../models/credentials';
|
|
3
|
+
import BaseProvider from './base_provider';
|
|
4
|
+
export interface XCUITestRunEnvironment {
|
|
5
|
+
device?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
}
|
|
3
9
|
export interface XCUITestRunInfo {
|
|
4
10
|
id: number;
|
|
5
11
|
status: 'WAITING' | 'READY' | 'DONE' | 'FAILED';
|
|
@@ -8,6 +14,7 @@ export interface XCUITestRunInfo {
|
|
|
8
14
|
platformName: string;
|
|
9
15
|
version?: string;
|
|
10
16
|
};
|
|
17
|
+
environment?: XCUITestRunEnvironment;
|
|
11
18
|
success: number;
|
|
12
19
|
report?: string;
|
|
13
20
|
}
|
|
@@ -24,23 +31,13 @@ export interface XCUITestSocketMessage {
|
|
|
24
31
|
id: number;
|
|
25
32
|
payload: string;
|
|
26
33
|
}
|
|
27
|
-
export default class XCUITest {
|
|
28
|
-
|
|
29
|
-
private readonly POLL_INTERVAL_MS;
|
|
30
|
-
private readonly MAX_POLL_ATTEMPTS;
|
|
31
|
-
private credentials;
|
|
32
|
-
private options;
|
|
33
|
-
private upload;
|
|
34
|
-
private appId;
|
|
35
|
-
private activeRunIds;
|
|
36
|
-
private isShuttingDown;
|
|
37
|
-
private signalHandler;
|
|
34
|
+
export default class XCUITest extends BaseProvider<XCUITestOptions> {
|
|
35
|
+
protected readonly URL = "https://api.testingbot.com/v1/app-automate/xcuitest";
|
|
38
36
|
private socket;
|
|
39
37
|
private updateServer;
|
|
40
38
|
private updateKey;
|
|
41
39
|
constructor(credentials: Credentials, options: XCUITestOptions);
|
|
42
40
|
private validate;
|
|
43
|
-
private ensureOutputDirectory;
|
|
44
41
|
run(): Promise<XCUITestResult>;
|
|
45
42
|
private uploadApp;
|
|
46
43
|
private uploadTestApp;
|
|
@@ -48,17 +45,13 @@ export default class XCUITest {
|
|
|
48
45
|
private getStatus;
|
|
49
46
|
private waitForCompletion;
|
|
50
47
|
private displayRunStatus;
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Get the display name for a run, preferring environment.name over capabilities.deviceName
|
|
50
|
+
* This shows the actual device used when a wildcard (*) was specified
|
|
51
|
+
*/
|
|
52
|
+
private getRunDisplayName;
|
|
53
53
|
private getStatusInfo;
|
|
54
54
|
private fetchReports;
|
|
55
|
-
private sleep;
|
|
56
|
-
private extractErrorMessage;
|
|
57
|
-
private setupSignalHandlers;
|
|
58
|
-
private removeSignalHandlers;
|
|
59
|
-
private handleShutdown;
|
|
60
|
-
private stopActiveRuns;
|
|
61
|
-
private stopRun;
|
|
62
55
|
private connectToUpdateServer;
|
|
63
56
|
private disconnectFromUpdateServer;
|
|
64
57
|
private handleXCUITestData;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xcuitest.d.ts","sourceRoot":"","sources":["../../src/providers/xcuitest.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAEzD,OAAO,WAAW,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"xcuitest.d.ts","sourceRoot":"","sources":["../../src/providers/xcuitest.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAEzD,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAOhD,OAAO,YAAY,MAAM,iBAAiB,CAAC;AAE3C,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,YAAY,EAAE;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,eAAe,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,CAAC,OAAO,OAAO,QAAS,SAAQ,YAAY,CAAC,eAAe,CAAC;IACjE,SAAS,CAAC,QAAQ,CAAC,GAAG,yDACkC;IAExD,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,SAAS,CAAuB;gBAErB,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe;YAIvD,QAAQ;IAuCT,GAAG,IAAI,OAAO,CAAC,cAAc,CAAC;YA+D7B,SAAS;YAaT,aAAa;YAYb,QAAQ;YA0DR,SAAS;YA4BT,iBAAiB;IAwE/B,OAAO,CAAC,gBAAgB;IAmCxB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,aAAa;YAkBP,YAAY;IA2D1B,OAAO,CAAC,qBAAqB;IAqC7B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,mBAAmB;CAa5B"}
|
|
@@ -10,84 +10,48 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
10
10
|
const socket_io_client_1 = require("socket.io-client");
|
|
11
11
|
const testingbot_error_1 = __importDefault(require("../models/testingbot_error"));
|
|
12
12
|
const utils_1 = __importDefault(require("../utils"));
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
class XCUITest {
|
|
13
|
+
const base_provider_1 = __importDefault(require("./base_provider"));
|
|
14
|
+
class XCUITest extends base_provider_1.default {
|
|
16
15
|
URL = 'https://api.testingbot.com/v1/app-automate/xcuitest';
|
|
17
|
-
POLL_INTERVAL_MS = 5000;
|
|
18
|
-
MAX_POLL_ATTEMPTS = 720; // 1 hour max with 5s interval
|
|
19
|
-
credentials;
|
|
20
|
-
options;
|
|
21
|
-
upload;
|
|
22
|
-
appId = undefined;
|
|
23
|
-
activeRunIds = [];
|
|
24
|
-
isShuttingDown = false;
|
|
25
|
-
signalHandler = null;
|
|
26
16
|
socket = null;
|
|
27
17
|
updateServer = null;
|
|
28
18
|
updateKey = null;
|
|
29
19
|
constructor(credentials, options) {
|
|
30
|
-
|
|
31
|
-
this.options = options;
|
|
32
|
-
this.upload = new upload_1.default();
|
|
20
|
+
super(credentials, options);
|
|
33
21
|
}
|
|
34
22
|
async validate() {
|
|
35
23
|
if (this.options.app === undefined) {
|
|
36
24
|
throw new testingbot_error_1.default(`app option is required`);
|
|
37
25
|
}
|
|
38
|
-
try {
|
|
39
|
-
await node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK);
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
|
|
43
|
-
}
|
|
44
26
|
if (this.options.testApp === undefined) {
|
|
45
27
|
throw new testingbot_error_1.default(`testApp option is required`);
|
|
46
28
|
}
|
|
47
|
-
try {
|
|
48
|
-
await node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK);
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
|
|
52
|
-
}
|
|
53
29
|
// Validate report options
|
|
54
30
|
if (this.options.report && !this.options.reportOutputDir) {
|
|
55
31
|
throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
|
|
56
32
|
}
|
|
33
|
+
// Validate file access in parallel for better performance
|
|
34
|
+
const fileChecks = [
|
|
35
|
+
node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK).catch(() => {
|
|
36
|
+
throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
|
|
37
|
+
}),
|
|
38
|
+
node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK).catch(() => {
|
|
39
|
+
throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
|
|
40
|
+
}),
|
|
41
|
+
];
|
|
57
42
|
if (this.options.reportOutputDir) {
|
|
58
|
-
|
|
43
|
+
fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
|
|
59
44
|
}
|
|
45
|
+
await Promise.all(fileChecks);
|
|
60
46
|
return true;
|
|
61
47
|
}
|
|
62
|
-
async ensureOutputDirectory(dirPath) {
|
|
63
|
-
try {
|
|
64
|
-
const stat = await node_fs_1.default.promises.stat(dirPath);
|
|
65
|
-
if (!stat.isDirectory()) {
|
|
66
|
-
throw new testingbot_error_1.default(`Report output path exists but is not a directory: ${dirPath}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
if (error.code === 'ENOENT') {
|
|
71
|
-
try {
|
|
72
|
-
await node_fs_1.default.promises.mkdir(dirPath, { recursive: true });
|
|
73
|
-
}
|
|
74
|
-
catch (mkdirError) {
|
|
75
|
-
throw new testingbot_error_1.default(`Failed to create report output directory: ${dirPath}`, { cause: mkdirError });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
else if (error instanceof testingbot_error_1.default) {
|
|
79
|
-
throw error;
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
throw new testingbot_error_1.default(`Failed to access report output directory: ${dirPath}`, { cause: error });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
48
|
async run() {
|
|
87
49
|
if (!(await this.validate())) {
|
|
88
50
|
return { success: false, runs: [] };
|
|
89
51
|
}
|
|
90
52
|
try {
|
|
53
|
+
// Quick connectivity check before starting uploads
|
|
54
|
+
await this.ensureConnectivity();
|
|
91
55
|
if (!this.options.quiet) {
|
|
92
56
|
logger_1.default.info('Uploading XCUITest App');
|
|
93
57
|
}
|
|
@@ -158,9 +122,11 @@ class XCUITest {
|
|
|
158
122
|
try {
|
|
159
123
|
const capabilities = this.options.getCapabilities();
|
|
160
124
|
const xcuitestOptions = this.options.getXCUITestOptions();
|
|
125
|
+
const metadata = this.options.metadata;
|
|
161
126
|
const response = await axios_1.default.post(`${this.URL}/${this.appId}/run`, {
|
|
162
127
|
capabilities: [capabilities],
|
|
163
128
|
...(xcuitestOptions && { options: xcuitestOptions }),
|
|
129
|
+
...(metadata && { metadata }),
|
|
164
130
|
}, {
|
|
165
131
|
headers: {
|
|
166
132
|
'Content-Type': 'application/json',
|
|
@@ -170,6 +136,7 @@ class XCUITest {
|
|
|
170
136
|
username: this.credentials.userName,
|
|
171
137
|
password: this.credentials.accessKey,
|
|
172
138
|
},
|
|
139
|
+
timeout: 30000, // 30 second timeout
|
|
173
140
|
});
|
|
174
141
|
// Check for version update notification
|
|
175
142
|
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
@@ -192,28 +159,30 @@ class XCUITest {
|
|
|
192
159
|
if (error instanceof testingbot_error_1.default) {
|
|
193
160
|
throw error;
|
|
194
161
|
}
|
|
195
|
-
throw
|
|
196
|
-
cause: error,
|
|
197
|
-
});
|
|
162
|
+
throw await this.handleErrorWithDiagnostics(error, 'Running XCUITest failed');
|
|
198
163
|
}
|
|
199
164
|
}
|
|
200
165
|
async getStatus() {
|
|
201
166
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
167
|
+
return await this.withRetry('Getting XCUITest status', async () => {
|
|
168
|
+
const response = await axios_1.default.get(`${this.URL}/${this.appId}`, {
|
|
169
|
+
headers: {
|
|
170
|
+
'User-Agent': utils_1.default.getUserAgent(),
|
|
171
|
+
},
|
|
172
|
+
auth: {
|
|
173
|
+
username: this.credentials.userName,
|
|
174
|
+
password: this.credentials.accessKey,
|
|
175
|
+
},
|
|
176
|
+
timeout: 30000, // 30 second timeout
|
|
177
|
+
});
|
|
178
|
+
// Check for version update notification
|
|
179
|
+
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
180
|
+
utils_1.default.checkForUpdate(latestVersion);
|
|
181
|
+
return response.data;
|
|
210
182
|
});
|
|
211
|
-
return response.data;
|
|
212
183
|
}
|
|
213
184
|
catch (error) {
|
|
214
|
-
throw
|
|
215
|
-
cause: error,
|
|
216
|
-
});
|
|
185
|
+
throw await this.handleErrorWithDiagnostics(error, 'Failed to get XCUITest status');
|
|
217
186
|
}
|
|
218
187
|
}
|
|
219
188
|
async waitForCompletion() {
|
|
@@ -240,7 +209,7 @@ class XCUITest {
|
|
|
240
209
|
for (const run of status.runs) {
|
|
241
210
|
const statusEmoji = run.success === 1 ? '✅' : '❌';
|
|
242
211
|
const statusText = run.success === 1 ? 'Test completed successfully' : 'Test failed';
|
|
243
|
-
console.log(` ${statusEmoji} Run ${run.id} (${run
|
|
212
|
+
console.log(` ${statusEmoji} Run ${run.id} (${this.getRunDisplayName(run)}): ${statusText}`);
|
|
244
213
|
}
|
|
245
214
|
}
|
|
246
215
|
const allSucceeded = status.runs.every((run) => run.success === 1);
|
|
@@ -253,7 +222,7 @@ class XCUITest {
|
|
|
253
222
|
const failedRuns = status.runs.filter((run) => run.success !== 1);
|
|
254
223
|
logger_1.default.error(`${failedRuns.length} test run(s) failed:`);
|
|
255
224
|
for (const run of failedRuns) {
|
|
256
|
-
logger_1.default.error(` - Run ${run.id} (${run
|
|
225
|
+
logger_1.default.error(` - Run ${run.id} (${this.getRunDisplayName(run)}): ${run.report || 'No report available'}`);
|
|
257
226
|
}
|
|
258
227
|
}
|
|
259
228
|
// Fetch reports if requested
|
|
@@ -284,24 +253,20 @@ class XCUITest {
|
|
|
284
253
|
previousStatus.set(run.id, run.status);
|
|
285
254
|
const statusInfo = this.getStatusInfo(run.status);
|
|
286
255
|
if (run.status === 'WAITING' || run.status === 'READY') {
|
|
287
|
-
const message = ` ${statusInfo.emoji} Run ${run.id} (${run
|
|
256
|
+
const message = ` ${statusInfo.emoji} Run ${run.id} (${this.getRunDisplayName(run)}): ${statusInfo.text} (${elapsedStr})`;
|
|
288
257
|
process.stdout.write(`\r${message}`);
|
|
289
258
|
}
|
|
290
259
|
else if (statusChanged) {
|
|
291
|
-
console.log(` ${statusInfo.emoji} Run ${run.id} (${run
|
|
260
|
+
console.log(` ${statusInfo.emoji} Run ${run.id} (${this.getRunDisplayName(run)}): ${statusInfo.text}`);
|
|
292
261
|
}
|
|
293
262
|
}
|
|
294
263
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
const minutes = Math.floor(seconds / 60);
|
|
303
|
-
const remainingSeconds = seconds % 60;
|
|
304
|
-
return `${minutes}m ${remainingSeconds}s`;
|
|
264
|
+
/**
|
|
265
|
+
* Get the display name for a run, preferring environment.name over capabilities.deviceName
|
|
266
|
+
* This shows the actual device used when a wildcard (*) was specified
|
|
267
|
+
*/
|
|
268
|
+
getRunDisplayName(run) {
|
|
269
|
+
return run.environment?.name || run.capabilities.deviceName;
|
|
305
270
|
}
|
|
306
271
|
getStatusInfo(status) {
|
|
307
272
|
switch (status) {
|
|
@@ -340,7 +305,11 @@ class XCUITest {
|
|
|
340
305
|
password: this.credentials.accessKey,
|
|
341
306
|
},
|
|
342
307
|
responseType: reportFormat === 'html' ? 'arraybuffer' : 'text',
|
|
308
|
+
timeout: 30000, // 30 second timeout
|
|
343
309
|
});
|
|
310
|
+
// Check for version update notification
|
|
311
|
+
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
312
|
+
utils_1.default.checkForUpdate(latestVersion);
|
|
344
313
|
const reportContent = response.data;
|
|
345
314
|
if (!reportContent) {
|
|
346
315
|
logger_1.default.error(`No report content received for run ${run.id}`);
|
|
@@ -359,106 +328,6 @@ class XCUITest {
|
|
|
359
328
|
}
|
|
360
329
|
}
|
|
361
330
|
}
|
|
362
|
-
sleep(ms) {
|
|
363
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
364
|
-
}
|
|
365
|
-
extractErrorMessage(cause) {
|
|
366
|
-
if (typeof cause === 'string') {
|
|
367
|
-
return cause;
|
|
368
|
-
}
|
|
369
|
-
if (Array.isArray(cause)) {
|
|
370
|
-
return cause.join('\n');
|
|
371
|
-
}
|
|
372
|
-
if (cause && typeof cause === 'object') {
|
|
373
|
-
const axiosError = cause;
|
|
374
|
-
if (axiosError.response?.data?.errors) {
|
|
375
|
-
return axiosError.response.data.errors.join('\n');
|
|
376
|
-
}
|
|
377
|
-
if (axiosError.response?.data?.error) {
|
|
378
|
-
return axiosError.response.data.error;
|
|
379
|
-
}
|
|
380
|
-
if (axiosError.response?.data?.message) {
|
|
381
|
-
return axiosError.response.data.message;
|
|
382
|
-
}
|
|
383
|
-
if (cause instanceof Error) {
|
|
384
|
-
return cause.message;
|
|
385
|
-
}
|
|
386
|
-
const obj = cause;
|
|
387
|
-
if (obj.errors) {
|
|
388
|
-
return obj.errors.join('\n');
|
|
389
|
-
}
|
|
390
|
-
if (obj.error) {
|
|
391
|
-
return obj.error;
|
|
392
|
-
}
|
|
393
|
-
if (obj.message) {
|
|
394
|
-
return obj.message;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
setupSignalHandlers() {
|
|
400
|
-
this.signalHandler = () => {
|
|
401
|
-
this.handleShutdown();
|
|
402
|
-
};
|
|
403
|
-
platform_1.default.setupSignalHandlers(this.signalHandler);
|
|
404
|
-
}
|
|
405
|
-
removeSignalHandlers() {
|
|
406
|
-
if (this.signalHandler) {
|
|
407
|
-
platform_1.default.removeSignalHandlers(this.signalHandler);
|
|
408
|
-
this.signalHandler = null;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
handleShutdown() {
|
|
412
|
-
if (this.isShuttingDown) {
|
|
413
|
-
logger_1.default.warn('Force exiting...');
|
|
414
|
-
process.exit(1);
|
|
415
|
-
}
|
|
416
|
-
this.isShuttingDown = true;
|
|
417
|
-
this.clearLine();
|
|
418
|
-
logger_1.default.warn('Received interrupt signal, stopping test runs...');
|
|
419
|
-
this.stopActiveRuns()
|
|
420
|
-
.then(() => {
|
|
421
|
-
logger_1.default.info('All test runs have been stopped.');
|
|
422
|
-
process.exit(1);
|
|
423
|
-
})
|
|
424
|
-
.catch((error) => {
|
|
425
|
-
logger_1.default.error(`Failed to stop some test runs: ${error instanceof Error ? error.message : error}`);
|
|
426
|
-
process.exit(1);
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
async stopActiveRuns() {
|
|
430
|
-
if (!this.appId || this.activeRunIds.length === 0) {
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
const stopPromises = this.activeRunIds.map((runId) => this.stopRun(runId).catch((error) => {
|
|
434
|
-
logger_1.default.error(`Failed to stop run ${runId}: ${error instanceof Error ? error.message : error}`);
|
|
435
|
-
}));
|
|
436
|
-
await Promise.all(stopPromises);
|
|
437
|
-
}
|
|
438
|
-
async stopRun(runId) {
|
|
439
|
-
if (!this.appId) {
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
try {
|
|
443
|
-
await axios_1.default.post(`${this.URL}/${this.appId}/${runId}/stop`, {}, {
|
|
444
|
-
headers: {
|
|
445
|
-
'User-Agent': utils_1.default.getUserAgent(),
|
|
446
|
-
},
|
|
447
|
-
auth: {
|
|
448
|
-
username: this.credentials.userName,
|
|
449
|
-
password: this.credentials.accessKey,
|
|
450
|
-
},
|
|
451
|
-
});
|
|
452
|
-
if (!this.options.quiet) {
|
|
453
|
-
logger_1.default.info(` Stopped run ${runId}`);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
catch (error) {
|
|
457
|
-
throw new testingbot_error_1.default(`Failed to stop run ${runId}`, {
|
|
458
|
-
cause: error,
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
331
|
connectToUpdateServer() {
|
|
463
332
|
if (!this.updateServer || !this.updateKey || this.options.quiet) {
|
|
464
333
|
return;
|
package/dist/upload.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface UploadOptions {
|
|
|
6
6
|
credentials: Credentials;
|
|
7
7
|
contentType: ContentType;
|
|
8
8
|
showProgress?: boolean;
|
|
9
|
+
checksum?: string;
|
|
9
10
|
}
|
|
10
11
|
export interface UploadResult {
|
|
11
12
|
id: number;
|
|
@@ -13,6 +14,15 @@ export interface UploadResult {
|
|
|
13
14
|
export default class Upload {
|
|
14
15
|
upload(options: UploadOptions): Promise<UploadResult>;
|
|
15
16
|
private drawProgressBar;
|
|
17
|
+
/**
|
|
18
|
+
* Format file size in human-readable format (KB for small files, MB for larger)
|
|
19
|
+
*/
|
|
20
|
+
private formatFileSize;
|
|
16
21
|
private validateFile;
|
|
22
|
+
/**
|
|
23
|
+
* Calculate MD5 checksum of a file, returning base64-encoded result
|
|
24
|
+
* This matches ActiveStorage's checksum format
|
|
25
|
+
*/
|
|
26
|
+
calculateChecksum(filePath: string): Promise<string>;
|
|
17
27
|
}
|
|
18
28
|
//# sourceMappingURL=upload.d.ts.map
|
package/dist/upload.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAMA,OAAO,WAAW,MAAM,sBAAsB,CAAC;AAK/C,MAAM,MAAM,WAAW,GACnB,yCAAyC,GACzC,0BAA0B,GAC1B,iBAAiB,CAAC;AAEtB,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,CAAC,OAAO,OAAO,MAAM;IACZ,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IA6FlE,OAAO,CAAC,eAAe;IAmBvB;;OAEG;IACH,OAAO,CAAC,cAAc;YAUR,YAAY;IAQ1B;;;OAGG;IACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAUlE"}
|
package/dist/upload.js
CHANGED
|
@@ -4,38 +4,36 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const axios_1 = __importDefault(require("axios"));
|
|
7
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
7
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
10
|
const form_data_1 = __importDefault(require("form-data"));
|
|
10
11
|
const progress_stream_1 = __importDefault(require("progress-stream"));
|
|
11
12
|
const testingbot_error_1 = __importDefault(require("./models/testingbot_error"));
|
|
12
13
|
const utils_1 = __importDefault(require("./utils"));
|
|
14
|
+
const error_helpers_1 = require("./utils/error-helpers");
|
|
13
15
|
class Upload {
|
|
14
16
|
async upload(options) {
|
|
15
|
-
const { filePath, url, credentials, showProgress = false
|
|
17
|
+
const { filePath, url, credentials, showProgress = false } = options;
|
|
16
18
|
await this.validateFile(filePath);
|
|
17
19
|
const fileName = node_path_1.default.basename(filePath);
|
|
18
20
|
const fileStats = await node_fs_1.default.promises.stat(filePath);
|
|
19
21
|
const totalSize = fileStats.size;
|
|
20
|
-
const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
|
|
21
|
-
// Create progress tracker
|
|
22
22
|
const progressTracker = (0, progress_stream_1.default)({
|
|
23
23
|
length: totalSize,
|
|
24
24
|
time: 100, // Emit progress every 100ms
|
|
25
25
|
});
|
|
26
26
|
let lastPercent = 0;
|
|
27
27
|
if (showProgress) {
|
|
28
|
-
|
|
29
|
-
this.drawProgressBar(fileName, sizeMB, 0);
|
|
28
|
+
this.drawProgressBar(fileName, totalSize, 0);
|
|
30
29
|
progressTracker.on('progress', (prog) => {
|
|
31
30
|
const percent = Math.round(prog.percentage);
|
|
32
31
|
if (percent !== lastPercent) {
|
|
33
32
|
lastPercent = percent;
|
|
34
|
-
this.drawProgressBar(fileName,
|
|
33
|
+
this.drawProgressBar(fileName, totalSize, percent);
|
|
35
34
|
}
|
|
36
35
|
});
|
|
37
36
|
}
|
|
38
|
-
// Create file stream and pipe through progress tracker
|
|
39
37
|
const fileStream = node_fs_1.default.createReadStream(filePath);
|
|
40
38
|
const trackedStream = fileStream.pipe(progressTracker);
|
|
41
39
|
const formData = new form_data_1.default();
|
|
@@ -44,6 +42,9 @@ class Upload {
|
|
|
44
42
|
contentType: options.contentType,
|
|
45
43
|
knownLength: totalSize,
|
|
46
44
|
});
|
|
45
|
+
if (options.checksum) {
|
|
46
|
+
formData.append('checksum', options.checksum);
|
|
47
|
+
}
|
|
47
48
|
try {
|
|
48
49
|
const response = await axios_1.default.post(url, formData, {
|
|
49
50
|
headers: {
|
|
@@ -58,10 +59,13 @@ class Upload {
|
|
|
58
59
|
maxBodyLength: Infinity,
|
|
59
60
|
maxRedirects: 0, // Recommended for stream uploads to avoid buffering
|
|
60
61
|
});
|
|
62
|
+
// Check for version update notification
|
|
63
|
+
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
64
|
+
utils_1.default.checkForUpdate(latestVersion);
|
|
61
65
|
const result = response.data;
|
|
62
66
|
if (result.id) {
|
|
63
67
|
if (showProgress) {
|
|
64
|
-
this.drawProgressBar(fileName,
|
|
68
|
+
this.drawProgressBar(fileName, totalSize, 100);
|
|
65
69
|
console.log('');
|
|
66
70
|
}
|
|
67
71
|
return { id: result.id };
|
|
@@ -81,26 +85,34 @@ class Upload {
|
|
|
81
85
|
throw error;
|
|
82
86
|
}
|
|
83
87
|
if (axios_1.default.isAxiosError(error)) {
|
|
84
|
-
|
|
85
|
-
throw new testingbot_error_1.default('Invalid TestingBot credentials. Please check your API key and secret.\n' +
|
|
86
|
-
'You can update your credentials by running "testingbot login" or by using:\n' +
|
|
87
|
-
' --api-key and --api-secret options\n' +
|
|
88
|
-
' TB_KEY and TB_SECRET environment variables\n' +
|
|
89
|
-
' ~/.testingbot file with content: key:secret');
|
|
90
|
-
}
|
|
91
|
-
const message = error.response?.data?.error || error.message;
|
|
92
|
-
throw new testingbot_error_1.default(`Upload failed: ${message}`);
|
|
88
|
+
throw (0, error_helpers_1.handleAxiosError)(error, 'Upload failed');
|
|
93
89
|
}
|
|
94
|
-
throw new testingbot_error_1.default(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}
|
|
90
|
+
throw new testingbot_error_1.default(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error instanceof Error ? error : undefined });
|
|
95
91
|
}
|
|
96
92
|
}
|
|
97
|
-
drawProgressBar(fileName,
|
|
93
|
+
drawProgressBar(fileName, totalBytes, percent) {
|
|
98
94
|
const barWidth = 30;
|
|
99
95
|
const filled = Math.round((barWidth * percent) / 100);
|
|
100
96
|
const empty = barWidth - filled;
|
|
101
97
|
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
102
|
-
const
|
|
103
|
-
|
|
98
|
+
const transferredBytes = (percent / 100) * totalBytes;
|
|
99
|
+
const transferred = this.formatFileSize(transferredBytes);
|
|
100
|
+
const total = this.formatFileSize(totalBytes);
|
|
101
|
+
process.stdout.write(`\r ${fileName}: [${bar}] ${percent}% (${transferred}/${total})`);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Format file size in human-readable format (KB for small files, MB for larger)
|
|
105
|
+
*/
|
|
106
|
+
formatFileSize(bytes) {
|
|
107
|
+
if (bytes < 1024) {
|
|
108
|
+
return `${bytes} B`;
|
|
109
|
+
}
|
|
110
|
+
else if (bytes < 1024 * 1024) {
|
|
111
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
115
|
+
}
|
|
104
116
|
}
|
|
105
117
|
async validateFile(filePath) {
|
|
106
118
|
try {
|
|
@@ -110,5 +122,18 @@ class Upload {
|
|
|
110
122
|
throw new testingbot_error_1.default(`File not found or not readable: ${filePath}`);
|
|
111
123
|
}
|
|
112
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Calculate MD5 checksum of a file, returning base64-encoded result
|
|
127
|
+
* This matches ActiveStorage's checksum format
|
|
128
|
+
*/
|
|
129
|
+
async calculateChecksum(filePath) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const hash = node_crypto_1.default.createHash('md5');
|
|
132
|
+
const stream = node_fs_1.default.createReadStream(filePath);
|
|
133
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
134
|
+
stream.on('end', () => resolve(hash.digest('base64')));
|
|
135
|
+
stream.on('error', (err) => reject(err));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
113
138
|
}
|
|
114
139
|
exports.default = Upload;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for checking internet connectivity using third-party endpoints
|
|
3
|
+
*/
|
|
4
|
+
export interface EndpointResult {
|
|
5
|
+
endpoint: string;
|
|
6
|
+
success: boolean;
|
|
7
|
+
statusCode?: number;
|
|
8
|
+
latencyMs: number;
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ConnectivityCheckResult {
|
|
12
|
+
connected: boolean;
|
|
13
|
+
endpointResults: EndpointResult[];
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check if the system has internet connectivity by testing against
|
|
18
|
+
* multiple reliable third-party endpoints in parallel.
|
|
19
|
+
* Returns as soon as one endpoint succeeds, reducing latency significantly.
|
|
20
|
+
*/
|
|
21
|
+
export declare function checkInternetConnectivity(): Promise<ConnectivityCheckResult>;
|
|
22
|
+
/**
|
|
23
|
+
* Format connectivity check results for display
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatConnectivityResults(result: ConnectivityCheckResult): string;
|
|
26
|
+
//# sourceMappingURL=connectivity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connectivity.d.ts","sourceRoot":"","sources":["../../src/utils/connectivity.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;AA4DD;;;;GAIG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,uBAAuB,CAAC,CA4ClF;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,uBAAuB,GAC9B,MAAM,CAuBR"}
|