appium-xcuitest-driver 10.13.2 → 10.13.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.
@@ -1,10 +1,13 @@
1
1
  import _ from 'lodash';
2
- import path from 'path';
2
+ import path from 'node:path';
3
3
  import {fs, zip, logger, util, tempDir} from 'appium/support';
4
4
  import {SubProcess, exec} from 'teen_process';
5
5
  import {encodeBase64OrUpload} from '../utils';
6
6
  import {waitForCondition} from 'asyncbox';
7
7
  import B from 'bluebird';
8
+ import type {XCUITestDriver} from '../driver';
9
+ import type {ActiveAppInfo} from './types';
10
+ import type {Method} from 'axios';
8
11
 
9
12
  const PERF_RECORD_FEAT_NAME = 'perf_record';
10
13
  const PERF_RECORD_SECURITY_MESSAGE =
@@ -34,16 +37,16 @@ const XCRUN = 'xcrun';
34
37
  *
35
38
  * Read [Recording, Pausing, and Stopping Traces](https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/Recording,Pausing,andStoppingTraces.html) for more details.
36
39
  *
37
- * @param {number} timeout - The maximum count of milliseconds to record the profiling information.
38
- * @param {string} profileName - The name of existing performance profile to apply. Can also contain the full path to the chosen template on the server file system. Note: not all profiles are supported on mobile devices.
39
- * @param {number|'current'} [pid] - The ID of the process to measure the performance for. Set it to `current` in order to measure the performance of the process, which belongs to the currently active application. All processes running on the device are measured if `pid` is unset (the default setting).
40
- * @this {XCUITestDriver}
40
+ * @param timeout - The maximum count of milliseconds to record the profiling information.
41
+ * @param profileName - The name of existing performance profile to apply. Can also contain the full path to the chosen template on the server file system. Note: not all profiles are supported on mobile devices.
42
+ * @param pid - The ID of the process to measure the performance for. Set it to `current` in order to measure the performance of the process, which belongs to the currently active application. All processes running on the device are measured if `pid` is unset (the default setting).
41
43
  */
42
44
  export async function mobileStartPerfRecord(
45
+ this: XCUITestDriver,
43
46
  timeout = DEFAULT_TIMEOUT_MS,
44
47
  profileName = DEFAULT_PROFILE_NAME,
45
- pid,
46
- ) {
48
+ pid?: number | 'current',
49
+ ): Promise<void> {
47
50
  if (!this.isFeatureEnabled(PERF_RECORD_FEAT_NAME) && !this.isRealDevice()) {
48
51
  throw this.log.errorWithException(PERF_RECORD_SECURITY_MESSAGE);
49
52
  }
@@ -62,21 +65,19 @@ export async function mobileStartPerfRecord(
62
65
  }
63
66
  }
64
67
 
65
- let realPid;
68
+ let realPid: number | undefined;
66
69
  if (pid) {
67
70
  if (_.toLower(String(pid)) === DEFAULT_PID) {
68
- const appInfo = /** @type {import('./types').ActiveAppInfo} */ (
69
- await this.proxyCommand('/wda/activeAppInfo', 'GET')
70
- );
71
+ const appInfo = (await this.proxyCommand('/wda/activeAppInfo', 'GET')) as ActiveAppInfo;
71
72
  realPid = appInfo.pid;
72
73
  } else {
73
- realPid = pid;
74
+ realPid = pid as number;
74
75
  }
75
76
  }
76
77
  const recorder = new PerfRecorder(await tempDir.openDir(), this.device.udid, {
77
78
  timeout: parseInt(String(timeout), 10),
78
79
  profileName,
79
- pid: parseInt(String(realPid), 10),
80
+ pid: realPid,
80
81
  });
81
82
  await recorder.start();
82
83
  this._perfRecorders = [...(this._perfRecorders || []), recorder];
@@ -89,30 +90,29 @@ export async function mobileStartPerfRecord(
89
90
  *
90
91
  * The resulting file in `.trace` format can be either returned directly as base64-encoded zip archive or uploaded to a remote location (such files can be pretty large). Afterwards it is possible to unarchive and open such files with Xcode Dev Tools.
91
92
  *
92
- * @param {string} [remotePath] - The path to the remote location, where the resulting zipped `.trace` file should be uploaded. The following protocols are supported: `http`, `https`, `ftp`. Null or empty string value (the default setting) means the content of resulting file should be zipped, encoded as Base64 and passed as the endpoint response value. An exception will be thrown if the generated file is too big to fit into the available process memory.
93
- * @param {string} [user] - The name of the user for the remote authentication. Only works if `remotePath` is provided.
94
- * @param {string} [pass] - The password for the remote authentication. Only works if `remotePath` is provided.
95
- * @param {import('axios').Method} [method] - The http multipart upload method name. Only works if `remotePath` is provided. Defaults to `PUT`
96
- * @param {string} profileName - The name of existing performance profile to stop the recording for. Multiple recorders for different profile names could be executed at the same time.
97
- * @param {Record<string,any>} [headers] - Additional headers mapping for multipart http(s) uploads
98
- * @param {string} [fileFieldName] - The name of the form field, where the file content BLOB should be stored for http(s) uploads. Defaults to `file`
99
- * @param {Record<string,any>|([string, any])[]} [formFields] - Additional form fields for multipart http(s) uploads
100
- *
101
- * @returns {Promise<string>} The resulting file in `.trace` format. This file can either be returned directly as base64-encoded `.zip` archive or uploaded to a remote location (note that such files may be large), _depending on the `remotePath` argument value._ Thereafter, the file may be unarchived and opened with Xcode Developer Tools.
93
+ * @param remotePath - The path to the remote location, where the resulting zipped `.trace` file should be uploaded. The following protocols are supported: `http`, `https`, `ftp`. Null or empty string value (the default setting) means the content of resulting file should be zipped, encoded as Base64 and passed as the endpoint response value. An exception will be thrown if the generated file is too big to fit into the available process memory.
94
+ * @param user - The name of the user for the remote authentication. Only works if `remotePath` is provided.
95
+ * @param pass - The password for the remote authentication. Only works if `remotePath` is provided.
96
+ * @param method - The http multipart upload method name. Only works if `remotePath` is provided. Defaults to `PUT`
97
+ * @param profileName - The name of existing performance profile to stop the recording for. Multiple recorders for different profile names could be executed at the same time.
98
+ * @param headers - Additional headers mapping for multipart http(s) uploads
99
+ * @param fileFieldName - The name of the form field, where the file content BLOB should be stored for http(s) uploads. Defaults to `file`
100
+ * @param formFields - Additional form fields for multipart http(s) uploads
101
+ * @returns The resulting file in `.trace` format. This file can either be returned directly as base64-encoded `.zip` archive or uploaded to a remote location (note that such files may be large), _depending on the `remotePath` argument value._ Thereafter, the file may be unarchived and opened with Xcode Developer Tools.
102
102
  * @throws {Error} If no performance recording with given profile name/device udid combination
103
103
  * has been started before or the resulting .trace file has not been generated properly.
104
- * @this {XCUITestDriver}
105
104
  */
106
105
  export async function mobileStopPerfRecord(
107
- remotePath,
108
- user,
109
- pass,
110
- method,
106
+ this: XCUITestDriver,
107
+ remotePath?: string,
108
+ user?: string,
109
+ pass?: string,
110
+ method?: Method,
111
111
  profileName = DEFAULT_PROFILE_NAME,
112
- headers,
113
- fileFieldName,
114
- formFields,
115
- ) {
112
+ headers?: Record<string, any>,
113
+ fileFieldName?: string,
114
+ formFields?: Record<string, any> | [string, any][],
115
+ ): Promise<string> {
116
116
  if (!this.isFeatureEnabled(PERF_RECORD_FEAT_NAME) && !this.isRealDevice()) {
117
117
  throw this.log.errorWithException(PERF_RECORD_SECURITY_MESSAGE);
118
118
  }
@@ -131,7 +131,12 @@ export async function mobileStopPerfRecord(
131
131
  }
132
132
 
133
133
  const recorder = _.first(recorders);
134
- const resultPath = await /** @type {PerfRecorder} */ (recorder).stop();
134
+ if (!recorder) {
135
+ throw this.log.errorWithException(
136
+ `No recorder found for performance profile '${profileName}' and device ${this.device.udid}`,
137
+ );
138
+ }
139
+ const resultPath = await recorder.stop();
135
140
  if (!(await fs.exists(resultPath))) {
136
141
  throw this.log.errorWithException(
137
142
  `There is no ${DEFAULT_EXT} file found for performance profile '${profileName}' ` +
@@ -152,35 +157,18 @@ export async function mobileStopPerfRecord(
152
157
  return result;
153
158
  }
154
159
 
155
-
156
- async function requireXctrace() {
157
- const xcrunPath = await requireXcrun();
158
- try {
159
- await exec(xcrunPath, [XCTRACE, 'help']);
160
- } catch (e) {
161
- throw new Error(
162
- `${XCTRACE} is not available for the active Xcode version. ` +
163
- `Please make sure XCode is up to date. Original error: ${e.stderr || e.message}`,
164
- );
165
- }
166
- return xcrunPath;
167
- }
168
-
169
- async function requireInstruments() {
170
- try {
171
- return await fs.which(INSTRUMENTS);
172
- } catch {
173
- throw new Error(
174
- `${INSTRUMENTS} has not been found in PATH. ` +
175
- `Please make sure XCode development tools are installed`,
176
- );
177
- }
178
- }
179
-
180
160
  export class PerfRecorder {
181
- /** @type {import('teen_process').SubProcess|null} */
182
- _process;
183
- constructor(reportRoot, udid, opts = {}) {
161
+ private _process: SubProcess | null;
162
+ private _zippedReportPath: string;
163
+ private readonly _timeout: number;
164
+ private readonly _profileName: string;
165
+ private readonly _reportPath: string;
166
+ private readonly _pid: number | undefined;
167
+ private readonly _udid: string;
168
+ private readonly _logger: any;
169
+ private _archivePromise: Promise<string> | null;
170
+
171
+ constructor(reportRoot: string, udid: string, opts: PerfRecorderOptions = {}) {
184
172
  this._process = null;
185
173
  this._zippedReportPath = '';
186
174
  this._timeout = opts.timeout && opts.timeout > 0 ? opts.timeout : DEFAULT_TIMEOUT_MS;
@@ -197,15 +185,11 @@ export class PerfRecorder {
197
185
  this._archivePromise = null;
198
186
  }
199
187
 
200
- get profileName() {
188
+ get profileName(): string {
201
189
  return this._profileName;
202
190
  }
203
191
 
204
- async getOriginalReportPath() {
205
- return (await fs.exists(this._reportPath)) ? this._reportPath : '';
206
- }
207
-
208
- async getZippedReportPath() {
192
+ async getZippedReportPath(): Promise<string> {
209
193
  // This is to prevent possible race conditions, because the archive operation
210
194
  // could be pretty time-intensive
211
195
  if (!this._archivePromise) {
@@ -229,55 +213,21 @@ export class PerfRecorder {
229
213
  return await this._archivePromise;
230
214
  }
231
215
 
232
- isRunning() {
216
+ isRunning(): boolean {
233
217
  return !!this._process?.isRunning;
234
218
  }
235
219
 
236
- async _enforceTermination() {
237
- if (this._process && this.isRunning()) {
238
- this._logger.debug('Force-stopping the currently running perf recording');
239
- try {
240
- await this._process.stop('SIGKILL');
241
- } catch {}
242
- }
243
- this._process = null;
244
- const performCleanup = async () => {
245
- try {
246
- await B.all(
247
- [this._zippedReportPath, path.dirname(this._reportPath)]
248
- .filter(Boolean)
249
- .map((x) => fs.rimraf(x)),
250
- );
251
- } catch (e) {
252
- this._logger.warn(e.message);
253
- }
254
- };
255
- if (this._archivePromise) {
256
- (async () => {
257
- try {
258
- await this._archivePromise;
259
- } catch {
260
- } finally {
261
- await performCleanup();
262
- this._archivePromise = null;
263
- }
264
- })();
265
- }
266
- await performCleanup();
267
- return '';
268
- }
269
-
270
- async start() {
271
- let binaryPath;
220
+ async start(): Promise<void> {
221
+ let binaryPath: string;
272
222
  try {
273
223
  binaryPath = await requireXctrace();
274
- } catch (e) {
224
+ } catch (e: any) {
275
225
  this._logger.debug(e.message);
276
226
  this._logger.warn(`Defaulting to ${INSTRUMENTS} usage`);
277
227
  binaryPath = await requireInstruments();
278
228
  }
279
229
 
280
- const args = [];
230
+ const args: string[] = [];
281
231
  const toolName = path.basename(binaryPath) === XCRUN ? XCTRACE : INSTRUMENTS;
282
232
  if (toolName === XCTRACE) {
283
233
  args.push(
@@ -318,16 +268,18 @@ export class PerfRecorder {
318
268
  this._archivePromise = null;
319
269
  this._logger.debug(`Starting performance recording: ${util.quote(fullCmd)}`);
320
270
  for (const streamName of ['stdout', 'stderr']) {
321
- this._process.on(`line-${streamName}`, (line) => this._logger.debug(`[${toolName}] ${line}`));
271
+ this._process.on(`line-${streamName}`, (line: string) =>
272
+ this._logger.debug(`[${toolName}] ${line}`),
273
+ );
322
274
  }
323
- this._process.once('exit', async (code, signal) => {
275
+ this._process.once('exit', async (code: number | null, signal: string | null) => {
324
276
  this._process = null;
325
277
  if (code === 0) {
326
278
  this._logger.debug('Performance recording exited without errors');
327
279
  try {
328
280
  // cache zipped report
329
281
  await this.getZippedReportPath();
330
- } catch (e) {
282
+ } catch (e: any) {
331
283
  this._logger.warn(e);
332
284
  }
333
285
  } else {
@@ -366,7 +318,7 @@ export class PerfRecorder {
366
318
  this._logger.info(`The performance recording has started. Will timeout in ${this._timeout}ms`);
367
319
  }
368
320
 
369
- async stop(force = false) {
321
+ async stop(force = false): Promise<string> {
370
322
  if (force) {
371
323
  return await this._enforceTermination();
372
324
  }
@@ -385,9 +337,74 @@ export class PerfRecorder {
385
337
  }
386
338
  return await this.getZippedReportPath();
387
339
  }
340
+
341
+ private async getOriginalReportPath(): Promise<string> {
342
+ return (await fs.exists(this._reportPath)) ? this._reportPath : '';
343
+ }
344
+
345
+ private async _enforceTermination(): Promise<string> {
346
+ if (this._process && this.isRunning()) {
347
+ this._logger.debug('Force-stopping the currently running perf recording');
348
+ try {
349
+ await this._process.stop('SIGKILL');
350
+ } catch {
351
+ // Ignore errors
352
+ }
353
+ }
354
+ this._process = null;
355
+ const performCleanup = async () => {
356
+ try {
357
+ await B.all(
358
+ [this._zippedReportPath, path.dirname(this._reportPath)]
359
+ .filter(Boolean)
360
+ .map((x) => fs.rimraf(x)),
361
+ );
362
+ } catch (e: any) {
363
+ this._logger.warn(e.message);
364
+ }
365
+ };
366
+ if (this._archivePromise) {
367
+ (async () => {
368
+ try {
369
+ await this._archivePromise;
370
+ } catch {
371
+ // Ignore errors
372
+ } finally {
373
+ await performCleanup();
374
+ this._archivePromise = null;
375
+ }
376
+ })();
377
+ }
378
+ await performCleanup();
379
+ return '';
380
+ }
381
+ }
382
+
383
+ async function requireXctrace(): Promise<string> {
384
+ const xcrunPath = await requireXcrun();
385
+ try {
386
+ await exec(xcrunPath, [XCTRACE, 'help']);
387
+ } catch (e: any) {
388
+ throw new Error(
389
+ `${XCTRACE} is not available for the active Xcode version. ` +
390
+ `Please make sure XCode is up to date. Original error: ${e.stderr || e.message}`,
391
+ );
392
+ }
393
+ return xcrunPath;
388
394
  }
389
395
 
390
- async function requireXcrun() {
396
+ async function requireInstruments(): Promise<string> {
397
+ try {
398
+ return await fs.which(INSTRUMENTS);
399
+ } catch {
400
+ throw new Error(
401
+ `${INSTRUMENTS} has not been found in PATH. ` +
402
+ `Please make sure XCode development tools are installed`,
403
+ );
404
+ }
405
+ }
406
+
407
+ async function requireXcrun(): Promise<string> {
391
408
  try {
392
409
  return await fs.which(XCRUN);
393
410
  } catch {
@@ -398,6 +415,8 @@ async function requireXcrun() {
398
415
  }
399
416
  }
400
417
 
401
- /**
402
- * @typedef {import('../driver').XCUITestDriver} XCUITestDriver
403
- */
418
+ interface PerfRecorderOptions {
419
+ timeout?: number;
420
+ profileName?: string;
421
+ pid?: number;
422
+ }