@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,6 +1,12 @@
|
|
|
1
1
|
export type Orientation = 'PORTRAIT' | 'LANDSCAPE';
|
|
2
2
|
export type ReportFormat = 'html' | 'junit';
|
|
3
3
|
export type ThrottleNetwork = '4G' | '3G' | 'Edge' | 'airplane';
|
|
4
|
+
export interface RunMetadata {
|
|
5
|
+
commitSha?: string;
|
|
6
|
+
pullRequestId?: string;
|
|
7
|
+
repoName?: string;
|
|
8
|
+
repoOwner?: string;
|
|
9
|
+
}
|
|
4
10
|
export interface CustomNetworkProfile {
|
|
5
11
|
uploadSpeed: number;
|
|
6
12
|
downloadSpeed: number;
|
|
@@ -45,6 +51,7 @@ export default class XCUITestOptions {
|
|
|
45
51
|
private _async;
|
|
46
52
|
private _report?;
|
|
47
53
|
private _reportOutputDir?;
|
|
54
|
+
private _metadata?;
|
|
48
55
|
constructor(app: string, testApp: string, device?: string, options?: {
|
|
49
56
|
version?: string;
|
|
50
57
|
realDevice?: boolean;
|
|
@@ -62,6 +69,7 @@ export default class XCUITestOptions {
|
|
|
62
69
|
async?: boolean;
|
|
63
70
|
report?: ReportFormat;
|
|
64
71
|
reportOutputDir?: string;
|
|
72
|
+
metadata?: RunMetadata;
|
|
65
73
|
});
|
|
66
74
|
get app(): string;
|
|
67
75
|
get testApp(): string;
|
|
@@ -82,6 +90,7 @@ export default class XCUITestOptions {
|
|
|
82
90
|
get async(): boolean;
|
|
83
91
|
get report(): ReportFormat | undefined;
|
|
84
92
|
get reportOutputDir(): string | undefined;
|
|
93
|
+
get metadata(): RunMetadata | undefined;
|
|
85
94
|
getCapabilities(): XCUITestCapabilities;
|
|
86
95
|
getXCUITestOptions(): XCUITestRunOptions | undefined;
|
|
87
96
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xcuitest_options.d.ts","sourceRoot":"","sources":["../../src/models/xcuitest_options.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"xcuitest_options.d.ts","sourceRoot":"","sources":["../../src/models/xcuitest_options.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const testingbot_error_1 = __importDefault(require("./testingbot_error"));
|
|
3
7
|
class XCUITestOptions {
|
|
4
8
|
_app;
|
|
5
9
|
_testApp;
|
|
@@ -25,6 +29,8 @@ class XCUITestOptions {
|
|
|
25
29
|
_async;
|
|
26
30
|
_report;
|
|
27
31
|
_reportOutputDir;
|
|
32
|
+
// Metadata
|
|
33
|
+
_metadata;
|
|
28
34
|
constructor(app, testApp, device, options) {
|
|
29
35
|
this._app = app;
|
|
30
36
|
this._testApp = testApp;
|
|
@@ -33,6 +39,10 @@ class XCUITestOptions {
|
|
|
33
39
|
this._realDevice = options?.realDevice ?? false;
|
|
34
40
|
this._tabletOnly = options?.tabletOnly ?? false;
|
|
35
41
|
this._phoneOnly = options?.phoneOnly ?? false;
|
|
42
|
+
// Validate contradictory options
|
|
43
|
+
if (this._tabletOnly && this._phoneOnly) {
|
|
44
|
+
throw new testingbot_error_1.default('Cannot specify both --phone-only and --tablet-only options');
|
|
45
|
+
}
|
|
36
46
|
this._name = options?.name;
|
|
37
47
|
this._build = options?.build;
|
|
38
48
|
this._orientation = options?.orientation;
|
|
@@ -45,6 +55,7 @@ class XCUITestOptions {
|
|
|
45
55
|
this._async = options?.async ?? false;
|
|
46
56
|
this._report = options?.report;
|
|
47
57
|
this._reportOutputDir = options?.reportOutputDir;
|
|
58
|
+
this._metadata = options?.metadata;
|
|
48
59
|
}
|
|
49
60
|
get app() {
|
|
50
61
|
return this._app;
|
|
@@ -103,6 +114,9 @@ class XCUITestOptions {
|
|
|
103
114
|
get reportOutputDir() {
|
|
104
115
|
return this._reportOutputDir;
|
|
105
116
|
}
|
|
117
|
+
get metadata() {
|
|
118
|
+
return this._metadata;
|
|
119
|
+
}
|
|
106
120
|
getCapabilities() {
|
|
107
121
|
const caps = {
|
|
108
122
|
platformName: 'iOS',
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import Credentials from '../models/credentials';
|
|
2
|
+
import TestingBotError from '../models/testingbot_error';
|
|
3
|
+
import Upload from '../upload';
|
|
4
|
+
/**
|
|
5
|
+
* Common interface for run information shared by all providers
|
|
6
|
+
*/
|
|
7
|
+
export interface BaseRunInfo {
|
|
8
|
+
id: number;
|
|
9
|
+
status: 'WAITING' | 'READY' | 'DONE' | 'FAILED';
|
|
10
|
+
capabilities: {
|
|
11
|
+
deviceName: string;
|
|
12
|
+
platformName: string;
|
|
13
|
+
version?: string;
|
|
14
|
+
};
|
|
15
|
+
success: number;
|
|
16
|
+
report?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Common interface for provider options
|
|
20
|
+
*/
|
|
21
|
+
export interface BaseProviderOptions {
|
|
22
|
+
quiet?: boolean;
|
|
23
|
+
reportOutputDir?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Abstract base class for test providers (Espresso, XCUITest, Maestro)
|
|
27
|
+
* Contains common functionality shared across all providers.
|
|
28
|
+
*/
|
|
29
|
+
export default abstract class BaseProvider<TOptions extends BaseProviderOptions> {
|
|
30
|
+
protected readonly POLL_INTERVAL_MS = 5000;
|
|
31
|
+
protected readonly MAX_POLL_ATTEMPTS = 720;
|
|
32
|
+
protected credentials: Credentials;
|
|
33
|
+
protected options: TOptions;
|
|
34
|
+
protected upload: Upload;
|
|
35
|
+
protected appId: number | undefined;
|
|
36
|
+
protected activeRunIds: number[];
|
|
37
|
+
protected isShuttingDown: boolean;
|
|
38
|
+
protected signalHandler: (() => void) | null;
|
|
39
|
+
/**
|
|
40
|
+
* The base URL for the provider's API endpoint
|
|
41
|
+
*/
|
|
42
|
+
protected abstract readonly URL: string;
|
|
43
|
+
constructor(credentials: Credentials, options: TOptions);
|
|
44
|
+
/**
|
|
45
|
+
* Ensures an output directory exists, creating it if necessary.
|
|
46
|
+
*/
|
|
47
|
+
protected ensureOutputDirectory(dirPath: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Sets up signal handlers for graceful shutdown (SIGINT, SIGTERM)
|
|
50
|
+
*/
|
|
51
|
+
protected setupSignalHandlers(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Removes signal handlers
|
|
54
|
+
*/
|
|
55
|
+
protected removeSignalHandlers(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Handles graceful shutdown when interrupt signal is received
|
|
58
|
+
*/
|
|
59
|
+
protected handleShutdown(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Stops all active test runs
|
|
62
|
+
*/
|
|
63
|
+
protected stopActiveRuns(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Stops a specific test run
|
|
66
|
+
*/
|
|
67
|
+
protected stopRun(runId: number): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Clears the current line in the terminal
|
|
70
|
+
*/
|
|
71
|
+
protected clearLine(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Sleeps for the specified number of milliseconds
|
|
74
|
+
*/
|
|
75
|
+
protected sleep(ms: number): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Maximum number of retries for transient errors
|
|
78
|
+
*/
|
|
79
|
+
protected readonly MAX_RETRIES = 3;
|
|
80
|
+
/**
|
|
81
|
+
* Base delay for exponential backoff (in milliseconds)
|
|
82
|
+
*/
|
|
83
|
+
protected readonly BASE_RETRY_DELAY_MS = 2000;
|
|
84
|
+
/**
|
|
85
|
+
* Executes an async operation with automatic retry for transient errors.
|
|
86
|
+
* Uses exponential backoff between retries.
|
|
87
|
+
*
|
|
88
|
+
* @param operation - Description of the operation (for logging)
|
|
89
|
+
* @param fn - Async function to execute
|
|
90
|
+
* @returns The result of the function
|
|
91
|
+
* @throws The last error if all retries fail
|
|
92
|
+
*/
|
|
93
|
+
protected withRetry<T>(operation: string, fn: () => Promise<T>): Promise<T>;
|
|
94
|
+
/**
|
|
95
|
+
* Extracts an error message from various error types.
|
|
96
|
+
* For Axios errors, uses enhanced error handling with diagnostics.
|
|
97
|
+
*/
|
|
98
|
+
protected extractErrorMessage(cause: unknown): string | null;
|
|
99
|
+
/**
|
|
100
|
+
* Checks internet connectivity and logs diagnostic information.
|
|
101
|
+
* Useful when network errors occur to help users troubleshoot.
|
|
102
|
+
*/
|
|
103
|
+
protected checkAndReportConnectivity(): Promise<boolean>;
|
|
104
|
+
/**
|
|
105
|
+
* Performs a quick connectivity check before starting operations.
|
|
106
|
+
* Throws an error with diagnostics if no connection is available.
|
|
107
|
+
*/
|
|
108
|
+
protected ensureConnectivity(): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Handles errors with enhanced diagnostics.
|
|
111
|
+
* For network errors, performs connectivity check.
|
|
112
|
+
*/
|
|
113
|
+
protected handleErrorWithDiagnostics(error: unknown, operation: string): Promise<TestingBotError>;
|
|
114
|
+
/**
|
|
115
|
+
* Formats elapsed time in human-readable format
|
|
116
|
+
*/
|
|
117
|
+
protected formatElapsedTime(seconds: number): string;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=base_provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base_provider.d.ts","sourceRoot":"","sources":["../../src/providers/base_provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const axios_1 = __importDefault(require("axios"));
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const testingbot_error_1 = __importDefault(require("../models/testingbot_error"));
|
|
9
|
+
const utils_1 = __importDefault(require("../utils"));
|
|
10
|
+
const upload_1 = __importDefault(require("../upload"));
|
|
11
|
+
const platform_1 = __importDefault(require("../utils/platform"));
|
|
12
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
13
|
+
const error_helpers_1 = require("../utils/error-helpers");
|
|
14
|
+
const connectivity_1 = require("../utils/connectivity");
|
|
15
|
+
/**
|
|
16
|
+
* Abstract base class for test providers (Espresso, XCUITest, Maestro)
|
|
17
|
+
* Contains common functionality shared across all providers.
|
|
18
|
+
*/
|
|
19
|
+
class BaseProvider {
|
|
20
|
+
POLL_INTERVAL_MS = 5000;
|
|
21
|
+
MAX_POLL_ATTEMPTS = 720; // 1 hour max with 5s interval
|
|
22
|
+
credentials;
|
|
23
|
+
options;
|
|
24
|
+
upload;
|
|
25
|
+
appId = undefined;
|
|
26
|
+
activeRunIds = [];
|
|
27
|
+
isShuttingDown = false;
|
|
28
|
+
signalHandler = null;
|
|
29
|
+
constructor(credentials, options) {
|
|
30
|
+
this.credentials = credentials;
|
|
31
|
+
this.options = options;
|
|
32
|
+
this.upload = new upload_1.default();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Ensures an output directory exists, creating it if necessary.
|
|
36
|
+
*/
|
|
37
|
+
async ensureOutputDirectory(dirPath) {
|
|
38
|
+
try {
|
|
39
|
+
const stat = await node_fs_1.default.promises.stat(dirPath);
|
|
40
|
+
if (!stat.isDirectory()) {
|
|
41
|
+
throw new testingbot_error_1.default(`Report output path exists but is not a directory: ${dirPath}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error instanceof Error &&
|
|
46
|
+
'code' in error &&
|
|
47
|
+
error.code === 'ENOENT') {
|
|
48
|
+
try {
|
|
49
|
+
await node_fs_1.default.promises.mkdir(dirPath, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
catch (mkdirError) {
|
|
52
|
+
throw new testingbot_error_1.default(`Failed to create report output directory: ${dirPath}`, { cause: mkdirError });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (error instanceof testingbot_error_1.default) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
throw new testingbot_error_1.default(`Failed to access report output directory: ${dirPath}`, { cause: error });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Sets up signal handlers for graceful shutdown (SIGINT, SIGTERM)
|
|
65
|
+
*/
|
|
66
|
+
setupSignalHandlers() {
|
|
67
|
+
this.signalHandler = () => {
|
|
68
|
+
this.handleShutdown();
|
|
69
|
+
};
|
|
70
|
+
platform_1.default.setupSignalHandlers(this.signalHandler);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Removes signal handlers
|
|
74
|
+
*/
|
|
75
|
+
removeSignalHandlers() {
|
|
76
|
+
if (this.signalHandler) {
|
|
77
|
+
platform_1.default.removeSignalHandlers(this.signalHandler);
|
|
78
|
+
this.signalHandler = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Handles graceful shutdown when interrupt signal is received
|
|
83
|
+
*/
|
|
84
|
+
handleShutdown() {
|
|
85
|
+
if (this.isShuttingDown) {
|
|
86
|
+
logger_1.default.warn('Force exiting...');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
this.isShuttingDown = true;
|
|
90
|
+
this.clearLine();
|
|
91
|
+
logger_1.default.info('Received interrupt signal, stopping test runs...');
|
|
92
|
+
this.stopActiveRuns()
|
|
93
|
+
.then(() => {
|
|
94
|
+
logger_1.default.info('All test runs have been stopped.');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
})
|
|
97
|
+
.catch((error) => {
|
|
98
|
+
logger_1.default.error(`Failed to stop some test runs: ${error instanceof Error ? error.message : error}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Stops all active test runs
|
|
104
|
+
*/
|
|
105
|
+
async stopActiveRuns() {
|
|
106
|
+
if (!this.appId || this.activeRunIds.length === 0) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const stopPromises = this.activeRunIds.map((runId) => this.stopRun(runId).catch((error) => {
|
|
110
|
+
logger_1.default.error(`Failed to stop run ${runId}: ${error instanceof Error ? error.message : error}`);
|
|
111
|
+
}));
|
|
112
|
+
await Promise.all(stopPromises);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Stops a specific test run
|
|
116
|
+
*/
|
|
117
|
+
async stopRun(runId) {
|
|
118
|
+
if (!this.appId) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await axios_1.default.post(`${this.URL}/${this.appId}/${runId}/stop`, {}, {
|
|
123
|
+
headers: {
|
|
124
|
+
'User-Agent': utils_1.default.getUserAgent(),
|
|
125
|
+
},
|
|
126
|
+
auth: {
|
|
127
|
+
username: this.credentials.userName,
|
|
128
|
+
password: this.credentials.accessKey,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
if (!this.options.quiet) {
|
|
132
|
+
logger_1.default.info(`Stopped run ${runId}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Ignore errors when stopping runs (may already be stopped)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Clears the current line in the terminal
|
|
141
|
+
*/
|
|
142
|
+
clearLine() {
|
|
143
|
+
platform_1.default.clearLine();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Sleeps for the specified number of milliseconds
|
|
147
|
+
*/
|
|
148
|
+
sleep(ms) {
|
|
149
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Maximum number of retries for transient errors
|
|
153
|
+
*/
|
|
154
|
+
MAX_RETRIES = 3;
|
|
155
|
+
/**
|
|
156
|
+
* Base delay for exponential backoff (in milliseconds)
|
|
157
|
+
*/
|
|
158
|
+
BASE_RETRY_DELAY_MS = 2000;
|
|
159
|
+
/**
|
|
160
|
+
* Executes an async operation with automatic retry for transient errors.
|
|
161
|
+
* Uses exponential backoff between retries.
|
|
162
|
+
*
|
|
163
|
+
* @param operation - Description of the operation (for logging)
|
|
164
|
+
* @param fn - Async function to execute
|
|
165
|
+
* @returns The result of the function
|
|
166
|
+
* @throws The last error if all retries fail
|
|
167
|
+
*/
|
|
168
|
+
async withRetry(operation, fn) {
|
|
169
|
+
let lastError;
|
|
170
|
+
for (let attempt = 0; attempt <= this.MAX_RETRIES; attempt++) {
|
|
171
|
+
try {
|
|
172
|
+
return await fn();
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
lastError = error;
|
|
176
|
+
// Check if this is a retryable error
|
|
177
|
+
const isRetryable = axios_1.default.isAxiosError(error) && (0, error_helpers_1.isRetryableError)(error);
|
|
178
|
+
// Don't retry non-retryable errors or on the last attempt
|
|
179
|
+
if (!isRetryable || attempt === this.MAX_RETRIES) {
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
// Calculate delay with exponential backoff: 2s, 4s, 8s
|
|
183
|
+
const delay = this.BASE_RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
184
|
+
if (!this.options.quiet) {
|
|
185
|
+
const statusCode = axios_1.default.isAxiosError(error)
|
|
186
|
+
? error.response?.status
|
|
187
|
+
: undefined;
|
|
188
|
+
logger_1.default.warn(`${operation} failed${statusCode ? ` (HTTP ${statusCode})` : ''}, retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${this.MAX_RETRIES})`);
|
|
189
|
+
}
|
|
190
|
+
await this.sleep(delay);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// All retries failed, throw the error
|
|
194
|
+
throw lastError;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Extracts an error message from various error types.
|
|
198
|
+
* For Axios errors, uses enhanced error handling with diagnostics.
|
|
199
|
+
*/
|
|
200
|
+
extractErrorMessage(cause) {
|
|
201
|
+
if (typeof cause === 'string') {
|
|
202
|
+
return cause;
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(cause)) {
|
|
205
|
+
return cause.join('\n');
|
|
206
|
+
}
|
|
207
|
+
if (cause && typeof cause === 'object') {
|
|
208
|
+
// Use enhanced error handling for real Axios errors
|
|
209
|
+
if (axios_1.default.isAxiosError(cause)) {
|
|
210
|
+
const enhanced = (0, error_helpers_1.handleAxiosError)(cause, 'Request failed');
|
|
211
|
+
return enhanced.message;
|
|
212
|
+
}
|
|
213
|
+
// Handle error-like objects with response property (for backwards compatibility)
|
|
214
|
+
const axiosLikeError = cause;
|
|
215
|
+
// Check for 429 status code (credits depleted)
|
|
216
|
+
if (axiosLikeError.response?.status === 429) {
|
|
217
|
+
return 'Your TestingBot credits are depleted. Please upgrade your plan at https://testingbot.com/pricing';
|
|
218
|
+
}
|
|
219
|
+
// Extract error message from response data
|
|
220
|
+
if (axiosLikeError.response?.data?.errors) {
|
|
221
|
+
return axiosLikeError.response.data.errors.join('\n');
|
|
222
|
+
}
|
|
223
|
+
if (axiosLikeError.response?.data?.error) {
|
|
224
|
+
return axiosLikeError.response.data.error;
|
|
225
|
+
}
|
|
226
|
+
if (axiosLikeError.response?.data?.message) {
|
|
227
|
+
return axiosLikeError.response.data.message;
|
|
228
|
+
}
|
|
229
|
+
if (cause instanceof Error) {
|
|
230
|
+
return cause.message;
|
|
231
|
+
}
|
|
232
|
+
const obj = cause;
|
|
233
|
+
if (obj.errors) {
|
|
234
|
+
return obj.errors.join('\n');
|
|
235
|
+
}
|
|
236
|
+
if (obj.error) {
|
|
237
|
+
return obj.error;
|
|
238
|
+
}
|
|
239
|
+
if (obj.message) {
|
|
240
|
+
return obj.message;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Checks internet connectivity and logs diagnostic information.
|
|
247
|
+
* Useful when network errors occur to help users troubleshoot.
|
|
248
|
+
*/
|
|
249
|
+
async checkAndReportConnectivity() {
|
|
250
|
+
logger_1.default.info('Checking internet connectivity...');
|
|
251
|
+
const result = await (0, connectivity_1.checkInternetConnectivity)();
|
|
252
|
+
logger_1.default.info((0, connectivity_1.formatConnectivityResults)(result));
|
|
253
|
+
return result.connected;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Performs a quick connectivity check before starting operations.
|
|
257
|
+
* Throws an error with diagnostics if no connection is available.
|
|
258
|
+
*/
|
|
259
|
+
async ensureConnectivity() {
|
|
260
|
+
const result = await (0, connectivity_1.checkInternetConnectivity)();
|
|
261
|
+
if (!result.connected) {
|
|
262
|
+
logger_1.default.error('No internet connection detected.');
|
|
263
|
+
logger_1.default.error((0, connectivity_1.formatConnectivityResults)(result));
|
|
264
|
+
throw new testingbot_error_1.default('No internet connection. Please check your network and try again.');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Handles errors with enhanced diagnostics.
|
|
269
|
+
* For network errors, performs connectivity check.
|
|
270
|
+
*/
|
|
271
|
+
async handleErrorWithDiagnostics(error, operation) {
|
|
272
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
273
|
+
// For network errors, check connectivity
|
|
274
|
+
if ((0, error_helpers_1.isNetworkError)(error)) {
|
|
275
|
+
await this.checkAndReportConnectivity();
|
|
276
|
+
}
|
|
277
|
+
return (0, error_helpers_1.handleAxiosError)(error, operation);
|
|
278
|
+
}
|
|
279
|
+
if (error instanceof testingbot_error_1.default) {
|
|
280
|
+
return error;
|
|
281
|
+
}
|
|
282
|
+
return new testingbot_error_1.default(`${operation}: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error instanceof Error ? error : undefined });
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Formats elapsed time in human-readable format
|
|
286
|
+
*/
|
|
287
|
+
formatElapsedTime(seconds) {
|
|
288
|
+
if (seconds < 60) {
|
|
289
|
+
return `${seconds}s`;
|
|
290
|
+
}
|
|
291
|
+
const minutes = Math.floor(seconds / 60);
|
|
292
|
+
const remainingSeconds = seconds % 60;
|
|
293
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
exports.default = BaseProvider;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import EspressoOptions from '../models/espresso_options';
|
|
2
2
|
import Credentials from '../models/credentials';
|
|
3
|
+
import BaseProvider from './base_provider';
|
|
4
|
+
export interface EspressoRunEnvironment {
|
|
5
|
+
device?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
}
|
|
3
9
|
export interface EspressoRunInfo {
|
|
4
10
|
id: number;
|
|
5
11
|
status: 'WAITING' | 'READY' | 'DONE' | 'FAILED';
|
|
@@ -8,6 +14,7 @@ export interface EspressoRunInfo {
|
|
|
8
14
|
platformName: string;
|
|
9
15
|
version?: string;
|
|
10
16
|
};
|
|
17
|
+
environment?: EspressoRunEnvironment;
|
|
11
18
|
success: number;
|
|
12
19
|
report?: string;
|
|
13
20
|
}
|
|
@@ -24,23 +31,13 @@ export interface EspressoSocketMessage {
|
|
|
24
31
|
id: number;
|
|
25
32
|
payload: string;
|
|
26
33
|
}
|
|
27
|
-
export default class Espresso {
|
|
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 Espresso extends BaseProvider<EspressoOptions> {
|
|
35
|
+
protected readonly URL = "https://api.testingbot.com/v1/app-automate/espresso";
|
|
38
36
|
private socket;
|
|
39
37
|
private updateServer;
|
|
40
38
|
private updateKey;
|
|
41
39
|
constructor(credentials: Credentials, options: EspressoOptions);
|
|
42
40
|
private validate;
|
|
43
|
-
private ensureOutputDirectory;
|
|
44
41
|
run(): Promise<EspressoResult>;
|
|
45
42
|
private uploadApp;
|
|
46
43
|
private uploadTestApp;
|
|
@@ -48,17 +45,13 @@ export default class Espresso {
|
|
|
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 handleEspressoData;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"espresso.d.ts","sourceRoot":"","sources":["../../src/providers/espresso.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAEzD,OAAO,WAAW,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"espresso.d.ts","sourceRoot":"","sources":["../../src/providers/espresso.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;IAwD1B,OAAO,CAAC,qBAAqB;IAqC7B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,mBAAmB;CAa5B"}
|