@devicecloud.dev/dcd 4.1.2 → 4.1.4
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/dist/commands/cloud.d.ts +26 -34
- package/dist/commands/cloud.js +151 -513
- package/dist/config/flags/api.flags.d.ts +7 -0
- package/dist/config/flags/api.flags.js +19 -0
- package/dist/config/flags/binary.flags.d.ts +8 -0
- package/dist/config/flags/binary.flags.js +20 -0
- package/dist/config/flags/device.flags.d.ts +14 -0
- package/dist/config/flags/device.flags.js +46 -0
- package/dist/config/flags/environment.flags.d.ts +11 -0
- package/dist/config/flags/environment.flags.js +37 -0
- package/dist/config/flags/execution.flags.d.ts +13 -0
- package/dist/config/flags/execution.flags.js +50 -0
- package/dist/config/flags/output.flags.d.ts +18 -0
- package/dist/config/flags/output.flags.js +61 -0
- package/dist/constants.d.ts +28 -24
- package/dist/constants.js +21 -206
- package/dist/gateways/api-gateway.d.ts +3 -3
- package/dist/gateways/api-gateway.js +1 -1
- package/dist/methods.d.ts +0 -4
- package/dist/methods.js +15 -80
- package/dist/services/device-validation.service.d.ts +31 -0
- package/dist/services/device-validation.service.js +74 -0
- package/dist/{plan.d.ts → services/execution-plan.service.d.ts} +10 -2
- package/dist/{plan.js → services/execution-plan.service.js} +12 -11
- package/dist/{planMethods.js → services/execution-plan.utils.js} +0 -1
- package/dist/services/metadata-extractor.service.d.ts +46 -0
- package/dist/services/metadata-extractor.service.js +138 -0
- package/dist/services/moropo.service.d.ts +25 -0
- package/dist/services/moropo.service.js +118 -0
- package/dist/services/report-download.service.d.ts +43 -0
- package/dist/services/report-download.service.js +113 -0
- package/dist/services/results-polling.service.d.ts +51 -0
- package/dist/services/results-polling.service.js +224 -0
- package/dist/services/test-submission.service.d.ts +47 -0
- package/dist/services/test-submission.service.js +122 -0
- package/dist/services/version.service.d.ts +32 -0
- package/dist/services/version.service.js +82 -0
- package/dist/types/{schema.types.d.ts → generated/schema.types.d.ts} +349 -349
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +24 -0
- package/dist/utils/compatibility.d.ts +5 -0
- package/oclif.manifest.json +195 -209
- package/package.json +2 -9
- /package/dist/{planMethods.d.ts → services/execution-plan.utils.d.ts} +0 -0
- /package/dist/types/{device.types.d.ts → domain/device.types.d.ts} +0 -0
- /package/dist/types/{device.types.js → domain/device.types.js} +0 -0
- /package/dist/types/{schema.types.js → generated/schema.types.js} +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MoropoService = void 0;
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const StreamZip = require("node-stream-zip");
|
|
9
|
+
/**
|
|
10
|
+
* Service for downloading and extracting Moropo tests from the Moropo API
|
|
11
|
+
*/
|
|
12
|
+
class MoropoService {
|
|
13
|
+
MOROPO_API_URL = 'https://api.moropo.com/tests';
|
|
14
|
+
/**
|
|
15
|
+
* Download and extract Moropo tests from the API
|
|
16
|
+
* @param options Download configuration options
|
|
17
|
+
* @returns Path to the extracted Moropo tests directory
|
|
18
|
+
*/
|
|
19
|
+
async downloadAndExtract(options) {
|
|
20
|
+
const { apiKey, branchName = 'main', debug = false, quiet = false, json = false, logger } = options;
|
|
21
|
+
this.logDebug(debug, logger, 'DEBUG: Moropo v1 API key detected, downloading tests from Moropo API');
|
|
22
|
+
this.logDebug(debug, logger, `DEBUG: Using branch name: ${branchName}`);
|
|
23
|
+
try {
|
|
24
|
+
if (!quiet && !json) {
|
|
25
|
+
core_1.ux.action.start('Downloading Moropo tests', 'Initializing', {
|
|
26
|
+
stdout: true,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const response = await fetch(this.MOROPO_API_URL, {
|
|
30
|
+
headers: {
|
|
31
|
+
accept: 'application/zip',
|
|
32
|
+
'x-app-api-key': apiKey,
|
|
33
|
+
'x-branch-name': branchName,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`Failed to download Moropo tests: ${response.statusText}`);
|
|
38
|
+
}
|
|
39
|
+
const moropoDir = path.join(os.tmpdir(), `moropo-tests-${Date.now()}`);
|
|
40
|
+
this.logDebug(debug, logger, `DEBUG: Extracting Moropo tests to: ${moropoDir}`);
|
|
41
|
+
// Create moropo directory if it doesn't exist
|
|
42
|
+
if (!fs.existsSync(moropoDir)) {
|
|
43
|
+
fs.mkdirSync(moropoDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
// Download zip file
|
|
46
|
+
const zipPath = path.join(moropoDir, 'moropo-tests.zip');
|
|
47
|
+
await this.downloadZipFile(response, zipPath, { quiet, json });
|
|
48
|
+
this.showProgress(quiet, json, 'Extracting tests...');
|
|
49
|
+
// Extract zip file
|
|
50
|
+
await this.extractZipFile(zipPath, moropoDir);
|
|
51
|
+
if (!quiet && !json) {
|
|
52
|
+
core_1.ux.action.stop('completed');
|
|
53
|
+
}
|
|
54
|
+
this.logDebug(debug, logger, 'DEBUG: Successfully extracted Moropo tests');
|
|
55
|
+
// Create config.yaml file
|
|
56
|
+
this.createConfigFile(moropoDir);
|
|
57
|
+
this.logDebug(debug, logger, 'DEBUG: Created config.yaml file');
|
|
58
|
+
return moropoDir;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (!quiet && !json) {
|
|
62
|
+
core_1.ux.action.stop('failed');
|
|
63
|
+
}
|
|
64
|
+
this.logDebug(debug, logger, `DEBUG: Error downloading/extracting Moropo tests: ${error}`);
|
|
65
|
+
throw new Error(`Failed to download/extract Moropo tests: ${error}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
createConfigFile(moropoDir) {
|
|
69
|
+
const configPath = path.join(moropoDir, 'config.yaml');
|
|
70
|
+
fs.writeFileSync(configPath, 'flows:\n- ./**/*.yaml\n- ./*.yaml\n');
|
|
71
|
+
}
|
|
72
|
+
async downloadZipFile(response, zipPath, options) {
|
|
73
|
+
const { quiet, json } = options;
|
|
74
|
+
const contentLength = response.headers.get('content-length');
|
|
75
|
+
const totalSize = contentLength ? Number.parseInt(contentLength, 10) : 0;
|
|
76
|
+
let downloadedSize = 0;
|
|
77
|
+
const fileStream = fs.createWriteStream(zipPath);
|
|
78
|
+
const reader = response.body?.getReader();
|
|
79
|
+
if (!reader) {
|
|
80
|
+
throw new Error('Failed to get response reader');
|
|
81
|
+
}
|
|
82
|
+
let readerResult = await reader.read();
|
|
83
|
+
while (!readerResult.done) {
|
|
84
|
+
const { value } = readerResult;
|
|
85
|
+
downloadedSize += value.length;
|
|
86
|
+
if (!quiet && !json && totalSize) {
|
|
87
|
+
const progress = Math.round((downloadedSize / totalSize) * 100);
|
|
88
|
+
core_1.ux.action.status = `Downloading: ${progress}%`;
|
|
89
|
+
}
|
|
90
|
+
fileStream.write(value);
|
|
91
|
+
readerResult = await reader.read();
|
|
92
|
+
}
|
|
93
|
+
fileStream.end();
|
|
94
|
+
await new Promise((resolve) => {
|
|
95
|
+
fileStream.on('finish', () => {
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async extractZipFile(zipPath, extractPath) {
|
|
101
|
+
// eslint-disable-next-line new-cap
|
|
102
|
+
const zip = new StreamZip.async({ file: zipPath });
|
|
103
|
+
await zip.extract(null, extractPath);
|
|
104
|
+
await zip.close();
|
|
105
|
+
fs.unlinkSync(zipPath);
|
|
106
|
+
}
|
|
107
|
+
logDebug(debug, logger, message) {
|
|
108
|
+
if (debug && logger) {
|
|
109
|
+
logger(message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
showProgress(quiet, json, message) {
|
|
113
|
+
if (!quiet && !json) {
|
|
114
|
+
core_1.ux.action.status = message;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
exports.MoropoService = MoropoService;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface DownloadOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
debug?: boolean;
|
|
5
|
+
logger?: (message: string) => void;
|
|
6
|
+
uploadId: string;
|
|
7
|
+
warnLogger?: (message: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface ArtifactsDownloadOptions extends DownloadOptions {
|
|
10
|
+
artifactsPath?: string;
|
|
11
|
+
downloadType: 'ALL' | 'FAILED';
|
|
12
|
+
}
|
|
13
|
+
export interface ReportDownloadOptions extends DownloadOptions {
|
|
14
|
+
allurePath?: string;
|
|
15
|
+
htmlPath?: string;
|
|
16
|
+
junitPath?: string;
|
|
17
|
+
reportType: 'allure' | 'html' | 'junit';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Service for downloading test artifacts and reports
|
|
21
|
+
*/
|
|
22
|
+
export declare class ReportDownloadService {
|
|
23
|
+
/**
|
|
24
|
+
* Download test artifacts as a zip file
|
|
25
|
+
* @param options Download configuration
|
|
26
|
+
* @returns Promise that resolves when download is complete
|
|
27
|
+
*/
|
|
28
|
+
downloadArtifacts(options: ArtifactsDownloadOptions): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Handle downloading reports based on the report type specified
|
|
31
|
+
* @param options Report download configuration
|
|
32
|
+
* @returns Promise that resolves when download is complete
|
|
33
|
+
*/
|
|
34
|
+
downloadReports(options: ReportDownloadOptions): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Download a specific report type
|
|
37
|
+
* @param type Report type to download
|
|
38
|
+
* @param filePath Path where report should be saved
|
|
39
|
+
* @param options Download configuration
|
|
40
|
+
* @returns Promise that resolves when download is complete
|
|
41
|
+
*/
|
|
42
|
+
private downloadReport;
|
|
43
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReportDownloadService = void 0;
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const api_gateway_1 = require("../gateways/api-gateway");
|
|
6
|
+
/**
|
|
7
|
+
* Service for downloading test artifacts and reports
|
|
8
|
+
*/
|
|
9
|
+
class ReportDownloadService {
|
|
10
|
+
/**
|
|
11
|
+
* Download test artifacts as a zip file
|
|
12
|
+
* @param options Download configuration
|
|
13
|
+
* @returns Promise that resolves when download is complete
|
|
14
|
+
*/
|
|
15
|
+
async downloadArtifacts(options) {
|
|
16
|
+
const { apiUrl, apiKey, uploadId, downloadType, artifactsPath = './artifacts.zip', debug = false, logger, warnLogger, } = options;
|
|
17
|
+
try {
|
|
18
|
+
if (debug && logger) {
|
|
19
|
+
logger(`DEBUG: Downloading artifacts: ${downloadType}`);
|
|
20
|
+
}
|
|
21
|
+
await api_gateway_1.ApiGateway.downloadArtifactsZip(apiUrl, apiKey, uploadId, downloadType, artifactsPath);
|
|
22
|
+
if (logger) {
|
|
23
|
+
logger('\n');
|
|
24
|
+
logger(`Test artifacts have been downloaded to ${artifactsPath}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (debug && logger) {
|
|
29
|
+
logger(`DEBUG: Error downloading artifacts: ${error}`);
|
|
30
|
+
}
|
|
31
|
+
if (warnLogger) {
|
|
32
|
+
warnLogger('Failed to download artifacts');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Handle downloading reports based on the report type specified
|
|
38
|
+
* @param options Report download configuration
|
|
39
|
+
* @returns Promise that resolves when download is complete
|
|
40
|
+
*/
|
|
41
|
+
async downloadReports(options) {
|
|
42
|
+
const { reportType, junitPath, allurePath, htmlPath, warnLogger, ...downloadOptions } = options;
|
|
43
|
+
switch (reportType) {
|
|
44
|
+
case 'junit': {
|
|
45
|
+
const reportPath = path.resolve(process.cwd(), junitPath || 'report.xml');
|
|
46
|
+
await this.downloadReport('junit', reportPath, {
|
|
47
|
+
...downloadOptions,
|
|
48
|
+
warnLogger,
|
|
49
|
+
});
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'allure': {
|
|
53
|
+
const reportPath = path.resolve(process.cwd(), allurePath || 'report.html');
|
|
54
|
+
await this.downloadReport('allure', reportPath, {
|
|
55
|
+
...downloadOptions,
|
|
56
|
+
warnLogger,
|
|
57
|
+
});
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case 'html': {
|
|
61
|
+
const htmlReportPath = path.resolve(process.cwd(), htmlPath || 'report.html');
|
|
62
|
+
await this.downloadReport('html', htmlReportPath, {
|
|
63
|
+
...downloadOptions,
|
|
64
|
+
warnLogger,
|
|
65
|
+
});
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
default: {
|
|
69
|
+
if (warnLogger) {
|
|
70
|
+
warnLogger(`Unknown report type: ${reportType}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Download a specific report type
|
|
77
|
+
* @param type Report type to download
|
|
78
|
+
* @param filePath Path where report should be saved
|
|
79
|
+
* @param options Download configuration
|
|
80
|
+
* @returns Promise that resolves when download is complete
|
|
81
|
+
*/
|
|
82
|
+
async downloadReport(type, filePath, options) {
|
|
83
|
+
const { apiUrl, apiKey, uploadId, debug = false, logger, warnLogger } = options;
|
|
84
|
+
try {
|
|
85
|
+
if (debug && logger) {
|
|
86
|
+
logger(`DEBUG: Downloading ${type.toUpperCase()} report`);
|
|
87
|
+
}
|
|
88
|
+
await api_gateway_1.ApiGateway.downloadReportGeneric(apiUrl, apiKey, uploadId, type, filePath);
|
|
89
|
+
if (logger) {
|
|
90
|
+
logger(`${type.toUpperCase()} test report has been downloaded to ${filePath}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (debug && logger) {
|
|
95
|
+
logger(`DEBUG: Error downloading ${type.toUpperCase()} report: ${error}`);
|
|
96
|
+
}
|
|
97
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
98
|
+
if (warnLogger) {
|
|
99
|
+
warnLogger(`Failed to download ${type.toUpperCase()} report: ${errorMessage}`);
|
|
100
|
+
if (errorMessage.includes('404')) {
|
|
101
|
+
warnLogger(`No ${type.toUpperCase()} reports found for this upload. Make sure your tests generated results.`);
|
|
102
|
+
}
|
|
103
|
+
else if (errorMessage.includes('EACCES') || errorMessage.includes('EPERM')) {
|
|
104
|
+
warnLogger('Permission denied. Check write permissions for the current directory.');
|
|
105
|
+
}
|
|
106
|
+
else if (errorMessage.includes('ENOENT')) {
|
|
107
|
+
warnLogger('Directory does not exist. Make sure you have write access to the current directory.');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.ReportDownloadService = ReportDownloadService;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { paths } from '../types/generated/schema.types';
|
|
2
|
+
type TestResult = paths['/results/{uploadId}']['get']['responses']['200']['content']['application/json']['results'][number];
|
|
3
|
+
/**
|
|
4
|
+
* Custom error for run failures that includes the polling result
|
|
5
|
+
*/
|
|
6
|
+
export declare class RunFailedError extends Error {
|
|
7
|
+
result: PollingResult;
|
|
8
|
+
constructor(result: PollingResult);
|
|
9
|
+
}
|
|
10
|
+
export interface PollingOptions {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
apiUrl: string;
|
|
13
|
+
consoleUrl: string;
|
|
14
|
+
debug?: boolean;
|
|
15
|
+
json?: boolean;
|
|
16
|
+
logger?: (message: string) => void;
|
|
17
|
+
quiet?: boolean;
|
|
18
|
+
uploadId: string;
|
|
19
|
+
}
|
|
20
|
+
export interface PollingResult {
|
|
21
|
+
consoleUrl: string;
|
|
22
|
+
status: 'FAILED' | 'PASSED';
|
|
23
|
+
tests: Array<{
|
|
24
|
+
durationSeconds: null | number;
|
|
25
|
+
failReason?: string;
|
|
26
|
+
name: string;
|
|
27
|
+
status: string;
|
|
28
|
+
}>;
|
|
29
|
+
uploadId: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Service for polling test results from the API
|
|
33
|
+
*/
|
|
34
|
+
export declare class ResultsPollingService {
|
|
35
|
+
private readonly MAX_SEQUENTIAL_FAILURES;
|
|
36
|
+
private readonly POLL_INTERVAL_MS;
|
|
37
|
+
/**
|
|
38
|
+
* Poll for test results until all tests complete
|
|
39
|
+
* @param results Initial test results from submission
|
|
40
|
+
* @param options Polling configuration
|
|
41
|
+
* @returns Promise that resolves with final test results or rejects if tests fail
|
|
42
|
+
*/
|
|
43
|
+
pollUntilComplete(results: TestResult[], options: PollingOptions): Promise<PollingResult>;
|
|
44
|
+
private buildPollingResult;
|
|
45
|
+
private calculateStatusSummary;
|
|
46
|
+
private displayFinalResults;
|
|
47
|
+
private filterLatestResults;
|
|
48
|
+
private handlePollingError;
|
|
49
|
+
private updateDisplayStatus;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ResultsPollingService = exports.RunFailedError = void 0;
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const cli_ux_1 = require("@oclif/core/lib/cli-ux");
|
|
6
|
+
const api_gateway_1 = require("../gateways/api-gateway");
|
|
7
|
+
const methods_1 = require("../methods");
|
|
8
|
+
const connectivity_1 = require("../utils/connectivity");
|
|
9
|
+
/**
|
|
10
|
+
* Custom error for run failures that includes the polling result
|
|
11
|
+
*/
|
|
12
|
+
class RunFailedError extends Error {
|
|
13
|
+
result;
|
|
14
|
+
constructor(result) {
|
|
15
|
+
super('RUN_FAILED');
|
|
16
|
+
this.result = result;
|
|
17
|
+
this.name = 'RunFailedError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.RunFailedError = RunFailedError;
|
|
21
|
+
/**
|
|
22
|
+
* Service for polling test results from the API
|
|
23
|
+
*/
|
|
24
|
+
class ResultsPollingService {
|
|
25
|
+
MAX_SEQUENTIAL_FAILURES = 10;
|
|
26
|
+
POLL_INTERVAL_MS = 5000;
|
|
27
|
+
/**
|
|
28
|
+
* Poll for test results until all tests complete
|
|
29
|
+
* @param results Initial test results from submission
|
|
30
|
+
* @param options Polling configuration
|
|
31
|
+
* @returns Promise that resolves with final test results or rejects if tests fail
|
|
32
|
+
*/
|
|
33
|
+
async pollUntilComplete(results, options) {
|
|
34
|
+
const { apiUrl, apiKey, uploadId, consoleUrl, quiet = false, json = false, debug = false, logger } = options;
|
|
35
|
+
if (!json) {
|
|
36
|
+
core_1.ux.action.start('Waiting for results', 'Initializing', {
|
|
37
|
+
stdout: true,
|
|
38
|
+
});
|
|
39
|
+
if (logger) {
|
|
40
|
+
logger('\nYou can safely close this terminal and the tests will continue\n');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let sequentialPollFailures = 0;
|
|
44
|
+
let previousSummary = '';
|
|
45
|
+
if (debug && logger) {
|
|
46
|
+
logger(`DEBUG: Starting polling loop for results`);
|
|
47
|
+
}
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const intervalId = setInterval(async () => {
|
|
50
|
+
try {
|
|
51
|
+
if (debug && logger) {
|
|
52
|
+
logger(`DEBUG: Polling for results: ${uploadId}`);
|
|
53
|
+
}
|
|
54
|
+
const { results: updatedResults } = await api_gateway_1.ApiGateway.getResultsForUpload(apiUrl, apiKey, uploadId);
|
|
55
|
+
if (!updatedResults) {
|
|
56
|
+
throw new Error('no results');
|
|
57
|
+
}
|
|
58
|
+
if (debug && logger) {
|
|
59
|
+
logger(`DEBUG: Poll received ${updatedResults.length} results`);
|
|
60
|
+
for (const result of updatedResults) {
|
|
61
|
+
logger(`DEBUG: Result status: ${result.test_file_name} - ${result.status}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const { summary } = this.calculateStatusSummary(updatedResults);
|
|
65
|
+
previousSummary = this.updateDisplayStatus(updatedResults, quiet, json, summary, previousSummary);
|
|
66
|
+
const allComplete = updatedResults.every((result) => !['PENDING', 'RUNNING'].includes(result.status));
|
|
67
|
+
if (allComplete) {
|
|
68
|
+
if (debug && logger) {
|
|
69
|
+
logger(`DEBUG: All tests completed, stopping poll`);
|
|
70
|
+
}
|
|
71
|
+
this.displayFinalResults(updatedResults, consoleUrl, json, logger);
|
|
72
|
+
clearInterval(intervalId);
|
|
73
|
+
const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl);
|
|
74
|
+
if (output.status === 'FAILED') {
|
|
75
|
+
if (debug && logger) {
|
|
76
|
+
logger(`DEBUG: Some tests failed, returning failed status`);
|
|
77
|
+
}
|
|
78
|
+
reject(new RunFailedError(output));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
if (debug && logger) {
|
|
82
|
+
logger(`DEBUG: All tests passed, returning success status`);
|
|
83
|
+
}
|
|
84
|
+
sequentialPollFailures = 0;
|
|
85
|
+
resolve(output);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
sequentialPollFailures++;
|
|
91
|
+
try {
|
|
92
|
+
await this.handlePollingError(error, sequentialPollFailures, debug, logger);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
clearInterval(intervalId);
|
|
96
|
+
reject(error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}, this.POLL_INTERVAL_MS);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
buildPollingResult(results, uploadId, consoleUrl) {
|
|
103
|
+
const resultsWithoutEarlierTries = this.filterLatestResults(results);
|
|
104
|
+
return {
|
|
105
|
+
consoleUrl,
|
|
106
|
+
status: resultsWithoutEarlierTries.some((result) => result.status === 'FAILED')
|
|
107
|
+
? 'FAILED'
|
|
108
|
+
: 'PASSED',
|
|
109
|
+
tests: resultsWithoutEarlierTries.map((r) => ({
|
|
110
|
+
durationSeconds: r.duration_seconds,
|
|
111
|
+
failReason: r.status === 'FAILED' ? r.fail_reason || 'No reason provided' : undefined,
|
|
112
|
+
name: r.test_file_name,
|
|
113
|
+
status: r.status,
|
|
114
|
+
})),
|
|
115
|
+
uploadId,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
calculateStatusSummary(results) {
|
|
119
|
+
const statusCounts = {};
|
|
120
|
+
for (const result of results) {
|
|
121
|
+
statusCounts[result.status] = (statusCounts[result.status] || 0) + 1;
|
|
122
|
+
}
|
|
123
|
+
const passed = statusCounts.PASSED || 0;
|
|
124
|
+
const failed = statusCounts.FAILED || 0;
|
|
125
|
+
const pending = statusCounts.PENDING || 0;
|
|
126
|
+
const running = statusCounts.RUNNING || 0;
|
|
127
|
+
const total = results.length;
|
|
128
|
+
const completed = passed + failed;
|
|
129
|
+
const summary = `${completed}/${total} | ✓ ${passed} | ✗ ${failed} | ▶ ${running} | ⏸ ${pending}`;
|
|
130
|
+
return { completed, failed, passed, pending, running, summary, total };
|
|
131
|
+
}
|
|
132
|
+
displayFinalResults(results, consoleUrl, json, logger) {
|
|
133
|
+
if (json) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
core_1.ux.action.stop('completed');
|
|
137
|
+
if (logger) {
|
|
138
|
+
logger('\n');
|
|
139
|
+
}
|
|
140
|
+
const hasFailedTests = results.some((result) => result.status === 'FAILED');
|
|
141
|
+
(0, cli_ux_1.table)(results, {
|
|
142
|
+
duration: {
|
|
143
|
+
get: (row) => row.duration_seconds
|
|
144
|
+
? (0, methods_1.formatDurationSeconds)(Number(row.duration_seconds))
|
|
145
|
+
: '-',
|
|
146
|
+
},
|
|
147
|
+
status: { get: (row) => row.status },
|
|
148
|
+
test: {
|
|
149
|
+
get: (row) => `${row.test_file_name} ${row.retry_of ? '(retry)' : ''}`,
|
|
150
|
+
},
|
|
151
|
+
...(hasFailedTests && {
|
|
152
|
+
// eslint-disable-next-line camelcase
|
|
153
|
+
fail_reason: {
|
|
154
|
+
get: (row) => row.status === 'FAILED' && row.fail_reason ? row.fail_reason : '',
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
}, { printLine: logger });
|
|
158
|
+
if (logger) {
|
|
159
|
+
logger('\n');
|
|
160
|
+
logger('Run completed, you can access the results at:');
|
|
161
|
+
logger(consoleUrl);
|
|
162
|
+
logger('\n');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
filterLatestResults(results) {
|
|
166
|
+
return results.filter((result) => {
|
|
167
|
+
const originalTryId = result.retry_of || result.id;
|
|
168
|
+
const tries = results.filter((r) => r.retry_of === originalTryId || r.id === originalTryId);
|
|
169
|
+
return result.id === Math.max(...tries.map((t) => t.id));
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async handlePollingError(error, sequentialPollFailures, debug, logger) {
|
|
173
|
+
if (debug && logger) {
|
|
174
|
+
logger(`DEBUG: Error polling for results: ${error}`);
|
|
175
|
+
logger(`DEBUG: Sequential poll failures: ${sequentialPollFailures}`);
|
|
176
|
+
}
|
|
177
|
+
if (sequentialPollFailures > this.MAX_SEQUENTIAL_FAILURES) {
|
|
178
|
+
if (debug && logger) {
|
|
179
|
+
logger('DEBUG: Checking internet connectivity...');
|
|
180
|
+
}
|
|
181
|
+
const connectivityCheck = await (0, connectivity_1.checkInternetConnectivity)();
|
|
182
|
+
if (debug && logger) {
|
|
183
|
+
logger(`DEBUG: ${connectivityCheck.message}`);
|
|
184
|
+
for (const result of connectivityCheck.endpointResults) {
|
|
185
|
+
if (result.success) {
|
|
186
|
+
logger(`DEBUG: ✓ ${result.endpoint} - ${result.statusCode} (${result.latencyMs}ms)`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
logger(`DEBUG: ✗ ${result.endpoint} - ${result.error} (${result.latencyMs}ms)`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!connectivityCheck.connected) {
|
|
194
|
+
const endpointDetails = connectivityCheck.endpointResults
|
|
195
|
+
.map((r) => ` - ${r.endpoint}: ${r.error} (${r.latencyMs}ms)`)
|
|
196
|
+
.join('\n');
|
|
197
|
+
throw new Error(`Unable to fetch results after ${this.MAX_SEQUENTIAL_FAILURES} attempts.\n\nInternet connectivity check failed - all test endpoints unreachable:\n${endpointDetails}\n\nPlease verify your network connection and DNS resolution.`);
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`unable to fetch results after ${this.MAX_SEQUENTIAL_FAILURES} attempts`);
|
|
200
|
+
}
|
|
201
|
+
if (logger) {
|
|
202
|
+
logger('unable to fetch results, trying again...');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
updateDisplayStatus(results, quiet, json, summary, previousSummary) {
|
|
206
|
+
if (json) {
|
|
207
|
+
return previousSummary;
|
|
208
|
+
}
|
|
209
|
+
if (quiet) {
|
|
210
|
+
if (summary !== previousSummary) {
|
|
211
|
+
core_1.ux.action.status = summary;
|
|
212
|
+
return summary;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
core_1.ux.action.status = '\nStatus Test\n─────────── ───────────────';
|
|
217
|
+
for (const { retry_of: isRetry, status, test_file_name: test, } of results) {
|
|
218
|
+
core_1.ux.action.status += `\n${status.padEnd(10, ' ')} ${test} ${isRetry ? '(retry)' : ''}`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return previousSummary;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
exports.ResultsPollingService = ResultsPollingService;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { IExecutionPlan } from './execution-plan.service';
|
|
2
|
+
export interface TestSubmissionConfig {
|
|
3
|
+
androidApiLevel?: string;
|
|
4
|
+
androidDevice?: string;
|
|
5
|
+
appBinaryId: string;
|
|
6
|
+
cliVersion: string;
|
|
7
|
+
commonRoot: string;
|
|
8
|
+
continueOnFailure?: boolean;
|
|
9
|
+
debug?: boolean;
|
|
10
|
+
deviceLocale?: string;
|
|
11
|
+
env?: string[];
|
|
12
|
+
executionPlan: IExecutionPlan;
|
|
13
|
+
flowFile: string;
|
|
14
|
+
googlePlay?: boolean;
|
|
15
|
+
iOSDevice?: string;
|
|
16
|
+
iOSVersion?: string;
|
|
17
|
+
logger?: (message: string) => void;
|
|
18
|
+
maestroVersion: string;
|
|
19
|
+
metadata?: string[];
|
|
20
|
+
mitmHost?: string;
|
|
21
|
+
mitmPath?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
orientation?: string;
|
|
24
|
+
raw?: unknown;
|
|
25
|
+
report?: string;
|
|
26
|
+
retry?: number;
|
|
27
|
+
runnerType?: string;
|
|
28
|
+
showCrosshairs?: boolean;
|
|
29
|
+
skipChromeOnboarding?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Service for building test submission form data
|
|
33
|
+
*/
|
|
34
|
+
export declare class TestSubmissionService {
|
|
35
|
+
/**
|
|
36
|
+
* Build FormData for test submission
|
|
37
|
+
* @param config Test submission configuration
|
|
38
|
+
* @returns FormData ready to be submitted to the API
|
|
39
|
+
*/
|
|
40
|
+
buildTestFormData(config: TestSubmissionConfig): Promise<FormData>;
|
|
41
|
+
private logDebug;
|
|
42
|
+
private normalizeFilePath;
|
|
43
|
+
private normalizePathMap;
|
|
44
|
+
private normalizePaths;
|
|
45
|
+
private parseKeyValuePairs;
|
|
46
|
+
private setOptionalFields;
|
|
47
|
+
}
|