@testrigor/testrigor-cli 1.0.1 → 2.0.1
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 -432
- package/dist/commands/authenticate.d.ts +14 -0
- package/dist/commands/authenticate.js +86 -25
- package/dist/commands/test-suite/run.js +240 -222
- package/dist/commands/test-suite/validate-syntax.js +1 -3
- package/dist/types.d.ts +0 -11
- package/dist/util/appium.js +67 -13
- package/dist/util/certificate-uploader.js +1 -2
- package/dist/util/request-builder.d.ts +2 -8
- package/dist/util/request-builder.js +10 -33
- package/dist/util/trtc.js +4 -4
- package/dist/util/variable-file-uploader.js +4 -10
- package/npm-shrinkwrap.json +11056 -0
- package/oclif.manifest.json +253 -180
- package/package.json +19 -16
package/dist/util/appium.js
CHANGED
|
@@ -15,21 +15,25 @@ const util = tslib_1.__importStar(require("util"));
|
|
|
15
15
|
const path = tslib_1.__importStar(require("path"));
|
|
16
16
|
const os = tslib_1.__importStar(require("os"));
|
|
17
17
|
const fs_1 = tslib_1.__importDefault(require("@supercharge/fs"));
|
|
18
|
+
const utils_1 = require("./utils");
|
|
18
19
|
const execFile = util.promisify(childProcess.execFile);
|
|
19
20
|
const PROJECT_ROOT = path.resolve(__dirname, "..", "..");
|
|
20
|
-
const APPIUM_HOME = path.join(PROJECT_ROOT, ".appium");
|
|
21
21
|
const DEFAULT_PORT = 4723;
|
|
22
22
|
exports.DEFAULT_PORT = DEFAULT_PORT;
|
|
23
23
|
const DEFAULT_BASE_PATH = "/wd/hub";
|
|
24
24
|
exports.DEFAULT_BASE_PATH = DEFAULT_BASE_PATH;
|
|
25
|
-
const PID_DIR =
|
|
25
|
+
const PID_DIR = (0, utils_1.getHomeDir)();
|
|
26
26
|
exports.PID_DIR = PID_DIR;
|
|
27
27
|
const PID_FILE = path.join(PID_DIR, "appium.pid");
|
|
28
28
|
exports.PID_FILE = PID_FILE;
|
|
29
29
|
const LOG_FILE = path.join(os.tmpdir(), "testrigor-appium.log");
|
|
30
|
+
// Appium writes its drivers here (hundreds of MB). It must live under the
|
|
31
|
+
// user's home, not the install directory: a global install is often
|
|
32
|
+
// root-owned, and `npm update -g` would wipe the drivers on every upgrade.
|
|
33
|
+
const APPIUM_HOME = process.env.APPIUM_HOME || path.join((0, utils_1.getHomeDir)(), "appium-home");
|
|
30
34
|
const REQUIRED_DRIVERS = [
|
|
31
35
|
{ name: "uiautomator2", version: "4.2.3" },
|
|
32
|
-
{ name: "xcuitest", version: "
|
|
36
|
+
{ name: "xcuitest", version: "10.43.1" },
|
|
33
37
|
];
|
|
34
38
|
exports.REQUIRED_DRIVERS = REQUIRED_DRIVERS;
|
|
35
39
|
function getAppiumBin() {
|
|
@@ -42,16 +46,34 @@ function getAppiumEnv() {
|
|
|
42
46
|
CHROMELABS_URL: "https://googlechromelabs.github.io",
|
|
43
47
|
};
|
|
44
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Appium refuses to start unless APPIUM_HOME already exists, so create it
|
|
51
|
+
* before any invocation and surface a permission problem as itself rather
|
|
52
|
+
* than as a generic "appium is broken".
|
|
53
|
+
*/
|
|
54
|
+
async function ensureAppiumHome() {
|
|
55
|
+
try {
|
|
56
|
+
await fs_1.default.ensureDir(APPIUM_HOME);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw new Error(`Cannot create the mobile server directory at ${APPIUM_HOME}: ` +
|
|
60
|
+
`${error instanceof Error ? error.message : String(error)}\n` +
|
|
61
|
+
"Check that you own that directory, or point APPIUM_HOME at a writable one.");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
45
64
|
async function checkAppiumInstalled() {
|
|
46
65
|
const appiumBin = getAppiumBin();
|
|
47
66
|
if (!(await fs_1.default.exists(appiumBin))) {
|
|
48
|
-
throw new Error(`Appium binary not found at ${appiumBin}.
|
|
67
|
+
throw new Error(`Appium binary not found at ${appiumBin}. ` +
|
|
68
|
+
"Reinstall the CLI with: npm install -g @testrigor/testrigor-cli");
|
|
49
69
|
}
|
|
70
|
+
await ensureAppiumHome();
|
|
50
71
|
try {
|
|
51
72
|
await execFile(appiumBin, ["--version"], { env: getAppiumEnv() });
|
|
52
73
|
}
|
|
53
|
-
catch {
|
|
54
|
-
|
|
74
|
+
catch (error) {
|
|
75
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
76
|
+
throw new Error(`Appium binary at ${appiumBin} failed to run (APPIUM_HOME=${APPIUM_HOME}).\n${detail}`);
|
|
55
77
|
}
|
|
56
78
|
}
|
|
57
79
|
async function getDriverStatus(driverName, requiredVersion) {
|
|
@@ -92,10 +114,7 @@ async function ensureDriver(driverName, requiredVersion) {
|
|
|
92
114
|
}
|
|
93
115
|
catch (error) {
|
|
94
116
|
const raw = error instanceof Error ? error.message : String(error);
|
|
95
|
-
return raw
|
|
96
|
-
.split("\n")[0]
|
|
97
|
-
.replace(/^Command failed:.*?Error:\s*/, "")
|
|
98
|
-
.trim();
|
|
117
|
+
return raw.replace(/^Command failed:.*?Error:\s*/, "").trim();
|
|
99
118
|
}
|
|
100
119
|
}
|
|
101
120
|
function isProcessRunning(pid) {
|
|
@@ -123,7 +142,36 @@ async function getRunningAppiumPid() {
|
|
|
123
142
|
await fs_1.default.removeFile(PID_FILE);
|
|
124
143
|
return null;
|
|
125
144
|
}
|
|
126
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Last lines of the Appium log, so a server that dies on startup reports the
|
|
147
|
+
* reason instead of just a health-check timeout. The log is shared across
|
|
148
|
+
* invocations, so only bytes written after `fromOffset` are considered.
|
|
149
|
+
*/
|
|
150
|
+
function readLogTail(fromOffset, lines = 20) {
|
|
151
|
+
let fd;
|
|
152
|
+
try {
|
|
153
|
+
fd = fs.openSync(LOG_FILE, "r");
|
|
154
|
+
const size = fs.fstatSync(fd).size;
|
|
155
|
+
if (size <= fromOffset)
|
|
156
|
+
return "";
|
|
157
|
+
const maxTailBytes = 64 * 1024;
|
|
158
|
+
const start = Math.max(fromOffset, size - maxTailBytes);
|
|
159
|
+
const buffer = Buffer.alloc(size - start);
|
|
160
|
+
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
161
|
+
const content = buffer.toString("utf8").trimEnd();
|
|
162
|
+
if (!content)
|
|
163
|
+
return "";
|
|
164
|
+
return content.split("\n").slice(-lines).join("\n");
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
if (fd !== undefined)
|
|
171
|
+
fs.closeSync(fd);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function healthCheck(port, logOffset, retries = 15, interval = 2000) {
|
|
127
175
|
const url = `http://127.0.0.1:${port}/wd/hub/status`;
|
|
128
176
|
let lastError = "";
|
|
129
177
|
for (let i = 0; i < retries; i++) {
|
|
@@ -139,7 +187,11 @@ async function healthCheck(port, retries = 15, interval = 2000) {
|
|
|
139
187
|
}
|
|
140
188
|
await new Promise((r) => setTimeout(r, interval));
|
|
141
189
|
}
|
|
142
|
-
|
|
190
|
+
const tail = readLogTail(logOffset);
|
|
191
|
+
throw new Error(`Mobile server not ready after ${retries} attempts: ${lastError}` +
|
|
192
|
+
(tail
|
|
193
|
+
? `\nLast lines of ${LOG_FILE}:\n${tail}`
|
|
194
|
+
: `\nSee ${LOG_FILE} for details.`));
|
|
143
195
|
}
|
|
144
196
|
async function startAppium(port = DEFAULT_PORT, logLevel = "debug") {
|
|
145
197
|
const existingPid = await getRunningAppiumPid();
|
|
@@ -147,7 +199,9 @@ async function startAppium(port = DEFAULT_PORT, logLevel = "debug") {
|
|
|
147
199
|
return { pid: existingPid, alreadyRunning: true };
|
|
148
200
|
}
|
|
149
201
|
const appiumBin = getAppiumBin();
|
|
202
|
+
await ensureAppiumHome();
|
|
150
203
|
const logStream = fs.openSync(LOG_FILE, "a");
|
|
204
|
+
const logOffset = fs.fstatSync(logStream).size;
|
|
151
205
|
const appiumProcess = childProcess.spawn(appiumBin, [
|
|
152
206
|
"--port",
|
|
153
207
|
String(port),
|
|
@@ -170,7 +224,7 @@ async function startAppium(port = DEFAULT_PORT, logLevel = "debug") {
|
|
|
170
224
|
await fs_1.default.ensureDir(PID_DIR);
|
|
171
225
|
await fs_1.default.writeFile(PID_FILE, String(pid));
|
|
172
226
|
try {
|
|
173
|
-
await healthCheck(port);
|
|
227
|
+
await healthCheck(port, logOffset);
|
|
174
228
|
}
|
|
175
229
|
catch (err) {
|
|
176
230
|
killSafe(pid);
|
|
@@ -4,12 +4,11 @@ exports.uploadClientCertificate = uploadClientCertificate;
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
6
|
const path = tslib_1.__importStar(require("path"));
|
|
7
|
-
const node_fetch_commonjs_1 = tslib_1.__importDefault(require("node-fetch-commonjs"));
|
|
8
7
|
const type_guards_1 = require("./type-guards");
|
|
9
8
|
async function uploadClientCertificate(api2BaseUrl, testSuiteId, token, localFilePath, password) {
|
|
10
9
|
const fileName = path.basename(localFilePath);
|
|
11
10
|
const certBase64 = fs.readFileSync(localFilePath).toString("base64");
|
|
12
|
-
const response = await (
|
|
11
|
+
const response = await fetch(`${api2BaseUrl}/apps/${testSuiteId}/test_data/global_variables/client_certificate`, {
|
|
13
12
|
method: "POST",
|
|
14
13
|
body: JSON.stringify({ fileName, certBase64, password }),
|
|
15
14
|
headers: {
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import FormData from "form-data";
|
|
2
1
|
import { ChromeExtensionInput } from "../types";
|
|
3
|
-
export interface ChromeExtensionFormDataResult {
|
|
4
|
-
form: FormData;
|
|
5
|
-
cleanup: () => void;
|
|
6
|
-
}
|
|
7
2
|
/**
|
|
8
|
-
*
|
|
9
|
-
* Returns both the FormData and a cleanup function to close streams.
|
|
3
|
+
* Builds the multipart body for the chrome extension overrides of a retest.
|
|
10
4
|
*/
|
|
11
|
-
export declare function buildChromeExtensionFormData(chromeExtensions: ChromeExtensionInput[]):
|
|
5
|
+
export declare function buildChromeExtensionFormData(chromeExtensions: ChromeExtensionInput[]): Promise<FormData>;
|
|
@@ -2,48 +2,25 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildChromeExtensionFormData = buildChromeExtensionFormData;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
-
const
|
|
6
|
-
const fs_1 = tslib_1.__importDefault(require("@supercharge/fs"));
|
|
5
|
+
const fs_1 = require("fs");
|
|
7
6
|
const path = tslib_1.__importStar(require("path"));
|
|
8
|
-
function
|
|
7
|
+
async function readChromeExtension(input) {
|
|
9
8
|
try {
|
|
10
|
-
return
|
|
11
|
-
name: input.name,
|
|
12
|
-
file: fs_1.default.createReadStream(input.file),
|
|
13
|
-
};
|
|
9
|
+
return await (0, fs_1.openAsBlob)(input.file);
|
|
14
10
|
}
|
|
15
11
|
catch (error) {
|
|
16
12
|
throw new Error(`Failed to read chrome extension file "${input.file}": ${error instanceof Error ? error.message : String(error)}`);
|
|
17
13
|
}
|
|
18
14
|
}
|
|
19
|
-
function toChromeExtensionOverridesRequest(extensionInputs) {
|
|
20
|
-
const chromeExtensions = extensionInputs.map(toChromeExtensionOverrideItem);
|
|
21
|
-
const streams = chromeExtensions.map((item) => item.file);
|
|
22
|
-
return {
|
|
23
|
-
request: { chromeExtensions },
|
|
24
|
-
streams,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
15
|
/**
|
|
28
|
-
*
|
|
29
|
-
* Returns both the FormData and a cleanup function to close streams.
|
|
16
|
+
* Builds the multipart body for the chrome extension overrides of a retest.
|
|
30
17
|
*/
|
|
31
|
-
function buildChromeExtensionFormData(chromeExtensions) {
|
|
32
|
-
const
|
|
33
|
-
const form = new
|
|
34
|
-
|
|
18
|
+
async function buildChromeExtensionFormData(chromeExtensions) {
|
|
19
|
+
const files = await Promise.all(chromeExtensions.map(readChromeExtension));
|
|
20
|
+
const form = new FormData();
|
|
21
|
+
chromeExtensions.forEach((ext, index) => {
|
|
35
22
|
form.append(`chromeExtensions[${index}].name`, ext.name);
|
|
36
|
-
|
|
37
|
-
form.append(`chromeExtensions[${index}].file`, ext.file, {
|
|
38
|
-
filename: fileName,
|
|
39
|
-
});
|
|
23
|
+
form.append(`chromeExtensions[${index}].file`, files[index], path.basename(ext.file));
|
|
40
24
|
});
|
|
41
|
-
|
|
42
|
-
streams.forEach((stream) => {
|
|
43
|
-
if (!stream.destroyed) {
|
|
44
|
-
stream.destroy();
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
};
|
|
48
|
-
return { form, cleanup };
|
|
25
|
+
return form;
|
|
49
26
|
}
|
package/dist/util/trtc.js
CHANGED
|
@@ -6,9 +6,9 @@ exports.download = download;
|
|
|
6
6
|
const tslib_1 = require("tslib");
|
|
7
7
|
const fs_1 = tslib_1.__importDefault(require("@supercharge/fs"));
|
|
8
8
|
const fs = tslib_1.__importStar(require("fs"));
|
|
9
|
-
const node_fetch_commonjs_1 = tslib_1.__importDefault(require("node-fetch-commonjs"));
|
|
10
9
|
const os = tslib_1.__importStar(require("os"));
|
|
11
10
|
const path = tslib_1.__importStar(require("path"));
|
|
11
|
+
const stream_1 = require("stream");
|
|
12
12
|
const tar = tslib_1.__importStar(require("tar-stream"));
|
|
13
13
|
const unzip = tslib_1.__importStar(require("unzip-stream"));
|
|
14
14
|
const zlib = tslib_1.__importStar(require("zlib"));
|
|
@@ -155,14 +155,14 @@ async function downloadOnce(dir) {
|
|
|
155
155
|
};
|
|
156
156
|
armStallTimer();
|
|
157
157
|
try {
|
|
158
|
-
const response = await (
|
|
158
|
+
const response = await fetch(getUrl(), { signal: controller.signal });
|
|
159
159
|
if (!response.ok) {
|
|
160
160
|
throw new Error(`unexpected response ${response.statusText}`);
|
|
161
161
|
}
|
|
162
|
-
|
|
163
|
-
if (stream == null) {
|
|
162
|
+
if (response.body == null) {
|
|
164
163
|
throw new Error("unexpected null response body");
|
|
165
164
|
}
|
|
165
|
+
const stream = stream_1.Readable.fromWeb(response.body);
|
|
166
166
|
const extraction = os.platform() === WIN32_PLATFORM
|
|
167
167
|
? extractZip(dir, stream)
|
|
168
168
|
: extractTar(dir, stream);
|
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.uploadVariableFile = uploadVariableFile;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
-
const fs_1 =
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
6
|
const path = tslib_1.__importStar(require("path"));
|
|
7
|
-
const node_fetch_commonjs_1 = tslib_1.__importDefault(require("node-fetch-commonjs"));
|
|
8
7
|
const utils_1 = require("./utils");
|
|
9
8
|
const type_guards_1 = require("./type-guards");
|
|
10
9
|
async function uploadVariableFile(api2BaseUrl, testSuiteId, token, localFilePath) {
|
|
11
10
|
const fileName = path.basename(localFilePath);
|
|
12
|
-
const
|
|
13
|
-
const presignedResp = await (0, node_fetch_commonjs_1.default)(`${api2BaseUrl}/apps/${testSuiteId}/test_data/global_variables/file/pre_signed_url`, {
|
|
11
|
+
const presignedResp = await fetch(`${api2BaseUrl}/apps/${testSuiteId}/test_data/global_variables/file/pre_signed_url`, {
|
|
14
12
|
method: "POST",
|
|
15
13
|
body: JSON.stringify({ fileName }),
|
|
16
14
|
headers: {
|
|
@@ -28,13 +26,9 @@ async function uploadVariableFile(api2BaseUrl, testSuiteId, token, localFilePath
|
|
|
28
26
|
!presignedJson.data) {
|
|
29
27
|
throw new Error(`Failed to request presigned URL for variable file '${fileName}': ${presignedJson.message}`);
|
|
30
28
|
}
|
|
31
|
-
const
|
|
32
|
-
const putResp = await (0, node_fetch_commonjs_1.default)(presignedJson.data.url, {
|
|
29
|
+
const putResp = await fetch(presignedJson.data.url, {
|
|
33
30
|
method: "PUT",
|
|
34
|
-
body:
|
|
35
|
-
headers: {
|
|
36
|
-
"Content-length": `${fileSize}`,
|
|
37
|
-
},
|
|
31
|
+
body: await (0, fs_1.openAsBlob)(localFilePath),
|
|
38
32
|
});
|
|
39
33
|
if (!putResp.ok) {
|
|
40
34
|
throw new Error(`Failed to upload variable file '${fileName}' to S3: ${putResp.statusText}`);
|