appium-xcuitest-driver 7.13.0 → 7.15.0
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/CHANGELOG.md +14 -0
- package/build/lib/app-infos-cache.d.ts +46 -0
- package/build/lib/app-infos-cache.d.ts.map +1 -0
- package/build/lib/app-infos-cache.js +156 -0
- package/build/lib/app-infos-cache.js.map +1 -0
- package/build/lib/app-utils.d.ts +60 -51
- package/build/lib/app-utils.d.ts.map +1 -1
- package/build/lib/app-utils.js +496 -182
- package/build/lib/app-utils.js.map +1 -1
- package/build/lib/commands/app-management.d.ts +5 -4
- package/build/lib/commands/app-management.d.ts.map +1 -1
- package/build/lib/commands/app-management.js +14 -7
- package/build/lib/commands/app-management.js.map +1 -1
- package/build/lib/commands/app-strings.d.ts +5 -2
- package/build/lib/commands/app-strings.d.ts.map +1 -1
- package/build/lib/commands/app-strings.js +6 -3
- package/build/lib/commands/app-strings.js.map +1 -1
- package/build/lib/commands/file-movement.js +1 -1
- package/build/lib/commands/file-movement.js.map +1 -1
- package/build/lib/commands/types.d.ts +1 -0
- package/build/lib/commands/types.d.ts.map +1 -1
- package/build/lib/commands/xctest-record-screen.d.ts +1 -1
- package/build/lib/desired-caps.d.ts +2 -0
- package/build/lib/desired-caps.d.ts.map +1 -1
- package/build/lib/desired-caps.js +1 -0
- package/build/lib/desired-caps.js.map +1 -1
- package/build/lib/driver.d.ts +35 -40
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +15 -99
- package/build/lib/driver.js.map +1 -1
- package/build/lib/execute-method-map.d.ts +1 -1
- package/build/lib/execute-method-map.js +1 -1
- package/build/lib/execute-method-map.js.map +1 -1
- package/build/lib/ios-fs-helpers.d.ts +30 -15
- package/build/lib/ios-fs-helpers.d.ts.map +1 -1
- package/build/lib/ios-fs-helpers.js +54 -21
- package/build/lib/ios-fs-helpers.js.map +1 -1
- package/build/lib/real-device-management.d.ts +0 -5
- package/build/lib/real-device-management.d.ts.map +1 -1
- package/build/lib/real-device-management.js +8 -5
- package/build/lib/real-device-management.js.map +1 -1
- package/build/lib/real-device.d.ts +13 -9
- package/build/lib/real-device.d.ts.map +1 -1
- package/build/lib/real-device.js +49 -75
- package/build/lib/real-device.js.map +1 -1
- package/lib/app-infos-cache.js +159 -0
- package/lib/app-utils.js +529 -193
- package/lib/commands/app-management.js +20 -9
- package/lib/commands/app-strings.js +6 -3
- package/lib/commands/file-movement.js +1 -1
- package/lib/commands/types.ts +1 -0
- package/lib/desired-caps.js +1 -0
- package/lib/driver.js +17 -120
- package/lib/execute-method-map.ts +1 -1
- package/lib/ios-fs-helpers.js +57 -23
- package/lib/real-device-management.js +7 -5
- package/lib/real-device.js +62 -88
- package/npm-shrinkwrap.json +40 -32
- package/package.json +2 -2
|
@@ -4,7 +4,11 @@ import {errors} from 'appium/driver';
|
|
|
4
4
|
import {services} from 'appium-ios-device';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import B from 'bluebird';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
SUPPORTED_EXTENSIONS,
|
|
9
|
+
onPostConfigureApp,
|
|
10
|
+
onDownloadApp,
|
|
11
|
+
} from '../app-utils';
|
|
8
12
|
|
|
9
13
|
export default {
|
|
10
14
|
/**
|
|
@@ -12,15 +16,20 @@ export default {
|
|
|
12
16
|
*
|
|
13
17
|
* Please ensure the app is built for a correct architecture and is signed with a proper developer signature (for real devices) prior to calling this.
|
|
14
18
|
* @param {string} app - See docs for `appium:app` capability
|
|
15
|
-
* @param {
|
|
16
|
-
*
|
|
17
|
-
* @param {boolean} [checkVersion] - If the application installation follows currently installed application's version status if provided.
|
|
19
|
+
* @param {number} [timeoutMs] - The maximum time to wait until app install is finished (in ms) on real devices.
|
|
20
|
+
* If not provided, then the value of `appium:appPushTimeout` capability is used. If the capability is not provided then the default is 240000ms (4 minutes).
|
|
21
|
+
* @param {boolean} [checkVersion] - If the application installation follows currently installed application's version status if provided.
|
|
22
|
+
* No checking occurs if no this option.
|
|
18
23
|
* @privateRemarks Link to capability docs
|
|
19
24
|
* @returns {Promise<void>}
|
|
20
25
|
* @this {XCUITestDriver}
|
|
21
26
|
*/
|
|
22
|
-
async mobileInstallApp(app, timeoutMs,
|
|
23
|
-
const srcAppPath = await this.helpers.configureApp(app,
|
|
27
|
+
async mobileInstallApp(app, timeoutMs, checkVersion) {
|
|
28
|
+
const srcAppPath = await this.helpers.configureApp(app, {
|
|
29
|
+
onPostProcess: onPostConfigureApp.bind(this),
|
|
30
|
+
onDownload: onDownloadApp.bind(this),
|
|
31
|
+
supportedExtensions: SUPPORTED_EXTENSIONS,
|
|
32
|
+
});
|
|
24
33
|
this.log.info(
|
|
25
34
|
`Installing '${srcAppPath}' to the ${this.isRealDevice() ? 'real device' : 'Simulator'} ` +
|
|
26
35
|
`with UDID '${this.device.udid}'`,
|
|
@@ -31,8 +40,8 @@ export default {
|
|
|
31
40
|
);
|
|
32
41
|
}
|
|
33
42
|
|
|
43
|
+
const bundleId = await this.appInfosCache.extractBundleId(srcAppPath);
|
|
34
44
|
if (checkVersion) {
|
|
35
|
-
const bundleId = await extractBundleId(srcAppPath);
|
|
36
45
|
const {install} = await this.checkAutInstallationState({
|
|
37
46
|
enforceAppInstall: false,
|
|
38
47
|
fullReset: false,
|
|
@@ -49,8 +58,10 @@ export default {
|
|
|
49
58
|
|
|
50
59
|
await this.device.installApp(
|
|
51
60
|
srcAppPath,
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
bundleId,
|
|
62
|
+
{
|
|
63
|
+
timeoutMs: timeoutMs ?? this.opts.appPushTimeout
|
|
64
|
+
},
|
|
54
65
|
);
|
|
55
66
|
this.log.info(`Installation of '${srcAppPath}' succeeded`);
|
|
56
67
|
},
|
|
@@ -4,8 +4,11 @@ export default {
|
|
|
4
4
|
/**
|
|
5
5
|
* Return the language-specific strings for an app
|
|
6
6
|
*
|
|
7
|
-
* @param {string} language - The language abbreviation to fetch app strings mapping for.
|
|
8
|
-
*
|
|
7
|
+
* @param {string} language - The language abbreviation to fetch app strings mapping for.
|
|
8
|
+
* If no language is provided then strings for the 'en language would be returned
|
|
9
|
+
* @param {string|null} [stringFile=null] - Relative path to the corresponding .strings
|
|
10
|
+
* file starting from the corresponding .lproj folder, e.g., `base/main.strings`. If omitted,
|
|
11
|
+
* then Appium will make its best guess where the file is.
|
|
9
12
|
*
|
|
10
13
|
* @returns {Promise<import('@appium/types').StringRecord<string>>} A record of localized keys to localized text
|
|
11
14
|
*
|
|
@@ -13,7 +16,7 @@ export default {
|
|
|
13
16
|
*/
|
|
14
17
|
async getStrings(language, stringFile = null) {
|
|
15
18
|
this.log.debug(`Gettings strings for language '${language}' and string file '${stringFile}'`);
|
|
16
|
-
return await parseLocalizableStrings(
|
|
19
|
+
return await parseLocalizableStrings.bind(this)(
|
|
17
20
|
Object.assign({}, this.opts, {
|
|
18
21
|
language,
|
|
19
22
|
stringFile,
|
|
@@ -160,7 +160,7 @@ async function pushFileToSimulator(device, remotePath, base64Data) {
|
|
|
160
160
|
async function pushFileToRealDevice(device, remotePath, base64Data) {
|
|
161
161
|
const {service, relativePath} = await createService(device.udid, remotePath);
|
|
162
162
|
try {
|
|
163
|
-
await pushFile(service,
|
|
163
|
+
await pushFile(service, Buffer.from(base64Data, 'base64'), relativePath);
|
|
164
164
|
} catch (e) {
|
|
165
165
|
log.debug(e.stack);
|
|
166
166
|
throw new Error(`Could not push the file to '${remotePath}'. Original error: ${e.message}`);
|
package/lib/commands/types.ts
CHANGED
package/lib/desired-caps.js
CHANGED
package/lib/driver.js
CHANGED
|
@@ -12,15 +12,10 @@ import EventEmitter from 'node:events';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import url from 'node:url';
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
IPA_EXT,
|
|
15
|
+
SUPPORTED_EXTENSIONS,
|
|
17
16
|
SAFARI_BUNDLE_ID,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
fetchSupportedAppPlatforms,
|
|
21
|
-
findApps,
|
|
22
|
-
isAppBundle,
|
|
23
|
-
isolateAppBundle,
|
|
17
|
+
onPostConfigureApp,
|
|
18
|
+
onDownloadApp,
|
|
24
19
|
verifyApplicationPlatform,
|
|
25
20
|
} from './app-utils';
|
|
26
21
|
import commands from './commands';
|
|
@@ -58,7 +53,6 @@ import {
|
|
|
58
53
|
getAndCheckXcodeVersion,
|
|
59
54
|
getDriverInfo,
|
|
60
55
|
isLocalHost,
|
|
61
|
-
isTvOs,
|
|
62
56
|
markSystemFilesForCleanup,
|
|
63
57
|
normalizeCommandTimeouts,
|
|
64
58
|
normalizePlatformVersion,
|
|
@@ -66,12 +60,11 @@ import {
|
|
|
66
60
|
removeAllSessionWebSocketHandlers,
|
|
67
61
|
translateDeviceName,
|
|
68
62
|
} from './utils';
|
|
63
|
+
import { AppInfosCache } from './app-infos-cache';
|
|
69
64
|
|
|
70
65
|
const SHUTDOWN_OTHER_FEAT_NAME = 'shutdown_other_sims';
|
|
71
66
|
const CUSTOMIZE_RESULT_BUNDLE_PATH = 'customize_result_bundle_path';
|
|
72
67
|
|
|
73
|
-
const SUPPORTED_EXTENSIONS = [IPA_EXT, APP_EXT];
|
|
74
|
-
const MAX_ARCHIVE_SCAN_DEPTH = 1;
|
|
75
68
|
const defaultServerCaps = {
|
|
76
69
|
webStorageEnabled: false,
|
|
77
70
|
locationContextEnabled: false,
|
|
@@ -315,6 +308,7 @@ export class XCUITestDriver extends BaseDriver {
|
|
|
315
308
|
}
|
|
316
309
|
this.lifecycleData = {};
|
|
317
310
|
this._audioRecorder = null;
|
|
311
|
+
this.appInfosCache = new AppInfosCache(this.log);
|
|
318
312
|
}
|
|
319
313
|
|
|
320
314
|
async onSettingsUpdate(key, value) {
|
|
@@ -553,7 +547,7 @@ export class XCUITestDriver extends BaseDriver {
|
|
|
553
547
|
await checkAppPresent(this.opts.app);
|
|
554
548
|
|
|
555
549
|
if (!this.opts.bundleId) {
|
|
556
|
-
this.opts.bundleId = await extractBundleId(this.opts.app);
|
|
550
|
+
this.opts.bundleId = await this.appInfosCache.extractBundleId(this.opts.app);
|
|
557
551
|
}
|
|
558
552
|
}
|
|
559
553
|
|
|
@@ -1065,107 +1059,12 @@ export class XCUITestDriver extends BaseDriver {
|
|
|
1065
1059
|
}
|
|
1066
1060
|
|
|
1067
1061
|
this.opts.app = await this.helpers.configureApp(this.opts.app, {
|
|
1068
|
-
onPostProcess:
|
|
1062
|
+
onPostProcess: onPostConfigureApp.bind(this),
|
|
1063
|
+
onDownload: onDownloadApp.bind(this),
|
|
1069
1064
|
supportedExtensions: SUPPORTED_EXTENSIONS,
|
|
1070
1065
|
});
|
|
1071
1066
|
}
|
|
1072
1067
|
|
|
1073
|
-
/**
|
|
1074
|
-
* Unzip the given archive and find a matching .app bundle in it
|
|
1075
|
-
*
|
|
1076
|
-
* @param {string} appPath The path to the archive.
|
|
1077
|
-
* @param {number} depth [0] the current nesting depth. App bundles whose nesting level
|
|
1078
|
-
* is greater than 1 are not supported.
|
|
1079
|
-
* @returns {Promise<string>} Full path to the first matching .app bundle..
|
|
1080
|
-
* @throws If no matching .app bundles were found in the provided archive.
|
|
1081
|
-
*/
|
|
1082
|
-
async unzipApp(appPath, depth = 0) {
|
|
1083
|
-
if (depth > MAX_ARCHIVE_SCAN_DEPTH) {
|
|
1084
|
-
throw new Error('Nesting of package bundles is not supported');
|
|
1085
|
-
}
|
|
1086
|
-
const [rootDir, matchedPaths] = await findApps(appPath, SUPPORTED_EXTENSIONS);
|
|
1087
|
-
if (_.isEmpty(matchedPaths)) {
|
|
1088
|
-
this.log.debug(`'${path.basename(appPath)}' has no bundles`);
|
|
1089
|
-
} else {
|
|
1090
|
-
this.log.debug(
|
|
1091
|
-
`Found ${util.pluralize('bundle', matchedPaths.length, true)} in ` +
|
|
1092
|
-
`'${path.basename(appPath)}': ${matchedPaths}`,
|
|
1093
|
-
);
|
|
1094
|
-
}
|
|
1095
|
-
try {
|
|
1096
|
-
for (const matchedPath of matchedPaths) {
|
|
1097
|
-
const fullPath = path.join(rootDir, matchedPath);
|
|
1098
|
-
if (await isAppBundle(fullPath)) {
|
|
1099
|
-
const supportedPlatforms = await fetchSupportedAppPlatforms(fullPath);
|
|
1100
|
-
if (this.isSimulator() && !supportedPlatforms.some((p) => _.includes(p, 'Simulator'))) {
|
|
1101
|
-
this.log.info(
|
|
1102
|
-
`'${matchedPath}' does not have Simulator devices in the list of supported platforms ` +
|
|
1103
|
-
`(${supportedPlatforms.join(',')}). Skipping it`,
|
|
1104
|
-
);
|
|
1105
|
-
continue;
|
|
1106
|
-
}
|
|
1107
|
-
if (this.isRealDevice() && !supportedPlatforms.some((p) => _.includes(p, 'OS'))) {
|
|
1108
|
-
this.log.info(
|
|
1109
|
-
`'${matchedPath}' does not have real devices in the list of supported platforms ` +
|
|
1110
|
-
`(${supportedPlatforms.join(',')}). Skipping it`,
|
|
1111
|
-
);
|
|
1112
|
-
continue;
|
|
1113
|
-
}
|
|
1114
|
-
this.log.info(
|
|
1115
|
-
`'${matchedPath}' is the resulting application bundle selected from '${appPath}'`,
|
|
1116
|
-
);
|
|
1117
|
-
return await isolateAppBundle(fullPath);
|
|
1118
|
-
} else if (_.endsWith(_.toLower(fullPath), IPA_EXT) && (await fs.stat(fullPath)).isFile()) {
|
|
1119
|
-
try {
|
|
1120
|
-
return await this.unzipApp(fullPath, depth + 1);
|
|
1121
|
-
} catch (e) {
|
|
1122
|
-
this.log.warn(`Skipping processing of '${matchedPath}': ${e.message}`);
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
} finally {
|
|
1127
|
-
await fs.rimraf(rootDir);
|
|
1128
|
-
}
|
|
1129
|
-
throw new Error(
|
|
1130
|
-
`${this.opts.app} did not have any matching ${APP_EXT} or ${IPA_EXT} ` +
|
|
1131
|
-
`bundles. Please make sure the provided package is valid and contains at least one matching ` +
|
|
1132
|
-
`application bundle which is not nested.`,
|
|
1133
|
-
);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
async onPostConfigureApp({cachedAppInfo, isUrl, appPath}) {
|
|
1137
|
-
// Pick the previously cached entry if its integrity has been preserved
|
|
1138
|
-
if (
|
|
1139
|
-
_.isPlainObject(cachedAppInfo) &&
|
|
1140
|
-
(await fs.stat(appPath)).isFile() &&
|
|
1141
|
-
(await fs.hash(appPath)) === cachedAppInfo.packageHash &&
|
|
1142
|
-
(await fs.exists(cachedAppInfo.fullPath)) &&
|
|
1143
|
-
(
|
|
1144
|
-
await fs.glob('**/*', {
|
|
1145
|
-
cwd: cachedAppInfo.fullPath,
|
|
1146
|
-
})
|
|
1147
|
-
).length === cachedAppInfo.integrity.folder
|
|
1148
|
-
) {
|
|
1149
|
-
this.log.info(`Using '${cachedAppInfo.fullPath}' which was cached from '${appPath}'`);
|
|
1150
|
-
return {appPath: cachedAppInfo.fullPath};
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
// Only local .app bundles that are available in-place should not be cached
|
|
1154
|
-
if (await isAppBundle(appPath)) {
|
|
1155
|
-
return false;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// Extract the app bundle and cache it
|
|
1159
|
-
try {
|
|
1160
|
-
return {appPath: await this.unzipApp(appPath)};
|
|
1161
|
-
} finally {
|
|
1162
|
-
// Cleanup previously downloaded archive
|
|
1163
|
-
if (isUrl) {
|
|
1164
|
-
await fs.rimraf(appPath);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
1068
|
async determineDevice() {
|
|
1170
1069
|
// in the one case where we create a sim, we will set this state
|
|
1171
1070
|
this.lifecycleData.createSim = false;
|
|
@@ -1603,7 +1502,7 @@ export class XCUITestDriver extends BaseDriver {
|
|
|
1603
1502
|
};
|
|
1604
1503
|
}
|
|
1605
1504
|
|
|
1606
|
-
const candidateBundleVersion = await extractBundleVersion(app);
|
|
1505
|
+
const candidateBundleVersion = await this.appInfosCache.extractBundleVersion(app);
|
|
1607
1506
|
this.log.debug(`CFBundleVersion from Info.plist: ${candidateBundleVersion}`);
|
|
1608
1507
|
if (!candidateBundleVersion) {
|
|
1609
1508
|
return {
|
|
@@ -1660,10 +1559,7 @@ export class XCUITestDriver extends BaseDriver {
|
|
|
1660
1559
|
return;
|
|
1661
1560
|
}
|
|
1662
1561
|
|
|
1663
|
-
await verifyApplicationPlatform(this
|
|
1664
|
-
isSimulator: this.isSimulator(),
|
|
1665
|
-
isTvOS: isTvOs(this.opts.platformName),
|
|
1666
|
-
});
|
|
1562
|
+
await verifyApplicationPlatform.bind(this)();
|
|
1667
1563
|
|
|
1668
1564
|
const {install, skipUninstall} = await this.checkAutInstallationState();
|
|
1669
1565
|
if (install) {
|
|
@@ -1671,7 +1567,6 @@ export class XCUITestDriver extends BaseDriver {
|
|
|
1671
1567
|
await installToRealDevice.bind(this)(this.opts.app, this.opts.bundleId, {
|
|
1672
1568
|
skipUninstall,
|
|
1673
1569
|
timeout: this.opts.appPushTimeout,
|
|
1674
|
-
strategy: this.opts.appInstallStrategy,
|
|
1675
1570
|
});
|
|
1676
1571
|
} else {
|
|
1677
1572
|
await installToSimulator.bind(this)(this.opts.app, this.opts.bundleId, {
|
|
@@ -1707,9 +1602,13 @@ export class XCUITestDriver extends BaseDriver {
|
|
|
1707
1602
|
}
|
|
1708
1603
|
|
|
1709
1604
|
/** @type {string[]} */
|
|
1710
|
-
const appPaths = await B.all(appsList.map((app) => this.helpers.configureApp(app,
|
|
1605
|
+
const appPaths = await B.all(appsList.map((app) => this.helpers.configureApp(app, {
|
|
1606
|
+
onPostProcess: onPostConfigureApp.bind(this),
|
|
1607
|
+
onDownload: onDownloadApp.bind(this),
|
|
1608
|
+
supportedExtensions: SUPPORTED_EXTENSIONS,
|
|
1609
|
+
})));
|
|
1711
1610
|
/** @type {string[]} */
|
|
1712
|
-
const appIds = await B.all(appPaths.map((appPath) => extractBundleId(appPath)));
|
|
1611
|
+
const appIds = await B.all(appPaths.map((appPath) => this.appInfosCache.extractBundleId(appPath)));
|
|
1713
1612
|
for (const [appId, appPath] of _.zip(appIds, appPaths)) {
|
|
1714
1613
|
if (this.isRealDevice()) {
|
|
1715
1614
|
await installToRealDevice.bind(this)(
|
|
@@ -1718,7 +1617,6 @@ export class XCUITestDriver extends BaseDriver {
|
|
|
1718
1617
|
{
|
|
1719
1618
|
skipUninstall: true, // to make the behavior as same as UIA2
|
|
1720
1619
|
timeout: this.opts.appPushTimeout,
|
|
1721
|
-
strategy: this.opts.appInstallStrategy,
|
|
1722
1620
|
},
|
|
1723
1621
|
);
|
|
1724
1622
|
} else {
|
|
@@ -1789,7 +1687,7 @@ export class XCUITestDriver extends BaseDriver {
|
|
|
1789
1687
|
return;
|
|
1790
1688
|
}
|
|
1791
1689
|
|
|
1792
|
-
const candidateBundleId = await extractBundleId(this.opts.prebuiltWDAPath);
|
|
1690
|
+
const candidateBundleId = await this.appInfosCache.extractBundleId(this.opts.prebuiltWDAPath);
|
|
1793
1691
|
this.wda.updatedWDABundleId = candidateBundleId.replace('.xctrunner', '');
|
|
1794
1692
|
this.log.info(
|
|
1795
1693
|
`Installing prebuilt WDA at '${this.opts.prebuiltWDAPath}'. ` +
|
|
@@ -1805,7 +1703,6 @@ export class XCUITestDriver extends BaseDriver {
|
|
|
1805
1703
|
{
|
|
1806
1704
|
skipUninstall: true,
|
|
1807
1705
|
timeout: this.opts.appPushTimeout,
|
|
1808
|
-
strategy: this.opts.appInstallStrategy,
|
|
1809
1706
|
},
|
|
1810
1707
|
);
|
|
1811
1708
|
} else {
|
package/lib/ios-fs-helpers.js
CHANGED
|
@@ -4,7 +4,7 @@ import {fs, tempDir, mkdirp, zip, util, timing} from 'appium/support';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import log from './logger';
|
|
6
6
|
|
|
7
|
-
const IO_TIMEOUT_MS = 4 * 60 * 1000;
|
|
7
|
+
export const IO_TIMEOUT_MS = 4 * 60 * 1000;
|
|
8
8
|
// Mobile devices use NAND memory modules for the storage,
|
|
9
9
|
// and the parallelism there is not as performant as on regular SSDs
|
|
10
10
|
const MAX_IO_CHUNK_SIZE = 8;
|
|
@@ -17,7 +17,7 @@ const MAX_IO_CHUNK_SIZE = 8;
|
|
|
17
17
|
* @param {string} remotePath Relative path to the file on the device
|
|
18
18
|
* @returns {Promise<Buffer>} The file content as a buffer
|
|
19
19
|
*/
|
|
20
|
-
async function pullFile(afcService, remotePath) {
|
|
20
|
+
export async function pullFile(afcService, remotePath) {
|
|
21
21
|
const stream = await afcService.createReadStream(remotePath, {autoDestroy: true});
|
|
22
22
|
const pullPromise = new B((resolve, reject) => {
|
|
23
23
|
stream.on('close', resolve);
|
|
@@ -51,7 +51,7 @@ async function folderExists(folderPath) {
|
|
|
51
51
|
* @param {string} remoteRootPath Relative path to the folder on the device
|
|
52
52
|
* @returns {Promise<Buffer>} The folder content as a zipped base64-encoded buffer
|
|
53
53
|
*/
|
|
54
|
-
async function pullFolder(afcService, remoteRootPath) {
|
|
54
|
+
export async function pullFolder(afcService, remoteRootPath) {
|
|
55
55
|
const tmpFolder = await tempDir.openDir();
|
|
56
56
|
try {
|
|
57
57
|
let localTopItem = null;
|
|
@@ -145,35 +145,71 @@ async function remoteMkdirp(afcService, remoteRoot) {
|
|
|
145
145
|
await afcService.createDirectory(remoteRoot);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* @typedef {Object} PushFileOptions
|
|
150
|
+
* @property {number} [timeoutMs=240000] The maximum count of milliceconds to wait until
|
|
151
|
+
* file push is completed. Cannot be lower than 60000ms
|
|
152
|
+
*/
|
|
153
|
+
|
|
148
154
|
/**
|
|
149
155
|
* Pushes a file to a real device
|
|
150
156
|
*
|
|
151
|
-
* @param {any} afcService Apple File Client service instance from
|
|
157
|
+
* @param {any} afcService afcService Apple File Client service instance from
|
|
152
158
|
* 'appium-ios-device' module
|
|
159
|
+
* @param {string|Buffer} localPathOrPayload Either full path to the source file
|
|
160
|
+
* or a buffer payload to be written into the remote destination
|
|
153
161
|
* @param {string} remotePath Relative path to the file on the device. The remote
|
|
154
162
|
* folder structure is created automatically if necessary.
|
|
155
|
-
* @param {
|
|
163
|
+
* @param {PushFileOptions} [opts={}]
|
|
156
164
|
*/
|
|
157
|
-
async function pushFile(afcService, remotePath,
|
|
165
|
+
export async function pushFile (afcService, localPathOrPayload, remotePath, opts = {}) {
|
|
166
|
+
const {
|
|
167
|
+
timeoutMs = IO_TIMEOUT_MS,
|
|
168
|
+
} = opts;
|
|
169
|
+
const timer = new timing.Timer().start();
|
|
158
170
|
await remoteMkdirp(afcService, path.dirname(remotePath));
|
|
159
|
-
const
|
|
171
|
+
const source = Buffer.isBuffer(localPathOrPayload)
|
|
172
|
+
? localPathOrPayload
|
|
173
|
+
: fs.createReadStream(localPathOrPayload, {autoClose: true});
|
|
174
|
+
const writeStream = await afcService.createWriteStream(remotePath, {
|
|
175
|
+
autoDestroy: true,
|
|
176
|
+
});
|
|
177
|
+
writeStream.on('finish', writeStream.destroy);
|
|
160
178
|
let pushError = null;
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
pushError = e;
|
|
164
|
-
});
|
|
165
|
-
stream.on('close', () => {
|
|
179
|
+
const filePushPromise = new B((resolve, reject) => {
|
|
180
|
+
writeStream.on('close', () => {
|
|
166
181
|
if (pushError) {
|
|
167
182
|
reject(pushError);
|
|
168
183
|
} else {
|
|
169
184
|
resolve();
|
|
170
185
|
}
|
|
171
186
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
187
|
+
const onStreamError = (e) => {
|
|
188
|
+
if (!Buffer.isBuffer(source)) {
|
|
189
|
+
source.unpipe(writeStream);
|
|
190
|
+
}
|
|
191
|
+
log.debug(e);
|
|
192
|
+
pushError = e;
|
|
193
|
+
};
|
|
194
|
+
writeStream.on('error', onStreamError);
|
|
195
|
+
if (!Buffer.isBuffer(source)) {
|
|
196
|
+
source.on('error', onStreamError);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
if (Buffer.isBuffer(source)) {
|
|
200
|
+
writeStream.write(source);
|
|
201
|
+
} else {
|
|
202
|
+
source.pipe(writeStream);
|
|
203
|
+
}
|
|
204
|
+
await filePushPromise.timeout(Math.max(timeoutMs, 60000));
|
|
205
|
+
const fileSize = Buffer.isBuffer(localPathOrPayload)
|
|
206
|
+
? localPathOrPayload.length
|
|
207
|
+
: (await fs.stat(localPathOrPayload)).size;
|
|
208
|
+
log.debug(
|
|
209
|
+
`Successfully pushed the file payload (${util.toReadableSizeString(fileSize)}) ` +
|
|
210
|
+
`to the remote location '${remotePath}' in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`
|
|
211
|
+
);
|
|
212
|
+
};
|
|
177
213
|
|
|
178
214
|
/**
|
|
179
215
|
* @typedef {Object} PushFolderOptions
|
|
@@ -194,7 +230,7 @@ async function pushFile(afcService, remotePath, base64Data) {
|
|
|
194
230
|
* will be deleted if already exists.
|
|
195
231
|
* @param {PushFolderOptions} opts
|
|
196
232
|
*/
|
|
197
|
-
async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) {
|
|
233
|
+
export async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) {
|
|
198
234
|
const {timeoutMs = IO_TIMEOUT_MS, enableParallelPush = false} = opts;
|
|
199
235
|
|
|
200
236
|
const timer = new timing.Timer().start();
|
|
@@ -239,7 +275,7 @@ async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) {
|
|
|
239
275
|
`(${util.pluralize('item', foldersToPush.length + 1, true)})`,
|
|
240
276
|
);
|
|
241
277
|
|
|
242
|
-
const
|
|
278
|
+
const _pushFile = async (/** @type {string} */ relativePath) => {
|
|
243
279
|
const absoluteSourcePath = path.join(srcRootPath, relativePath);
|
|
244
280
|
const readStream = fs.createReadStream(absoluteSourcePath, {autoClose: true});
|
|
245
281
|
const absoluteDestinationPath = path.join(dstRootPath, relativePath);
|
|
@@ -272,7 +308,7 @@ async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) {
|
|
|
272
308
|
log.debug(`Proceeding to parallel files push (max ${MAX_IO_CHUNK_SIZE} writers)`);
|
|
273
309
|
const pushPromises = [];
|
|
274
310
|
for (const relativeFilePath of filesToPush) {
|
|
275
|
-
pushPromises.push(B.resolve(
|
|
311
|
+
pushPromises.push(B.resolve(_pushFile(relativeFilePath)));
|
|
276
312
|
// keep the push queue filled
|
|
277
313
|
if (pushPromises.length >= MAX_IO_CHUNK_SIZE) {
|
|
278
314
|
await B.any(pushPromises);
|
|
@@ -290,7 +326,7 @@ async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) {
|
|
|
290
326
|
} else {
|
|
291
327
|
log.debug(`Proceeding to serial files push`);
|
|
292
328
|
for (const relativeFilePath of filesToPush) {
|
|
293
|
-
await
|
|
329
|
+
await _pushFile(relativeFilePath);
|
|
294
330
|
const elapsedMs = timer.getDuration().asMilliSeconds;
|
|
295
331
|
if (elapsedMs > timeoutMs) {
|
|
296
332
|
throw new B.TimeoutError(`Timed out after ${elapsedMs} ms`);
|
|
@@ -304,5 +340,3 @@ async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) {
|
|
|
304
340
|
`within ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`,
|
|
305
341
|
);
|
|
306
342
|
}
|
|
307
|
-
|
|
308
|
-
export {pullFile, pullFolder, pushFile, pushFolder};
|
|
@@ -8,7 +8,6 @@ const DEFAULT_APP_INSTALLATION_TIMEOUT_MS = 8 * 60 * 1000;
|
|
|
8
8
|
* @typedef {Object} InstallOptions
|
|
9
9
|
*
|
|
10
10
|
* @property {boolean} [skipUninstall] Whether to skip app uninstall before installing it
|
|
11
|
-
* @property {'serial'|'parallel'|'ios-deploy'} [strategy='serial'] One of possible install strategies ('serial', 'parallel', 'ios-deploy')
|
|
12
11
|
* @property {number} [timeout=480000] App install timeout
|
|
13
12
|
* @property {boolean} [shouldEnforceUninstall] Whether to enforce the app uninstallation. e.g. fullReset, or enforceAppInstall is true
|
|
14
13
|
*/
|
|
@@ -29,7 +28,6 @@ export async function installToRealDevice(app, bundleId, opts = {}) {
|
|
|
29
28
|
|
|
30
29
|
const {
|
|
31
30
|
skipUninstall,
|
|
32
|
-
strategy,
|
|
33
31
|
timeout = DEFAULT_APP_INSTALLATION_TIMEOUT_MS,
|
|
34
32
|
} = opts;
|
|
35
33
|
|
|
@@ -37,10 +35,12 @@ export async function installToRealDevice(app, bundleId, opts = {}) {
|
|
|
37
35
|
this.log.info(`Reset requested. Removing app with id '${bundleId}' from the device`);
|
|
38
36
|
await device.remove(bundleId);
|
|
39
37
|
}
|
|
40
|
-
this.log.debug(`Installing '${app}' on the device with UUID '${device.udid}'
|
|
38
|
+
this.log.debug(`Installing '${app}' on the device with UUID '${device.udid}'`);
|
|
41
39
|
|
|
42
40
|
try {
|
|
43
|
-
await device.install(app,
|
|
41
|
+
await device.install(app, bundleId, {
|
|
42
|
+
timeoutMs: timeout,
|
|
43
|
+
});
|
|
44
44
|
this.log.debug('The app has been installed successfully.');
|
|
45
45
|
} catch (e) {
|
|
46
46
|
// Want to clarify the device's application installation state in this situation.
|
|
@@ -61,7 +61,9 @@ export async function installToRealDevice(app, bundleId, opts = {}) {
|
|
|
61
61
|
`be already cached on the device, probably with a different signature. ` +
|
|
62
62
|
`Will try to remove it and install a new copy. Original error: ${e.message}`);
|
|
63
63
|
await device.remove(bundleId);
|
|
64
|
-
await device.install(app,
|
|
64
|
+
await device.install(app, bundleId, {
|
|
65
|
+
timeoutMs: timeout,
|
|
66
|
+
});
|
|
65
67
|
this.log.debug('The app has been installed after one retrial.');
|
|
66
68
|
}
|
|
67
69
|
}
|