Skip to content

Commit 8899895

Browse files
feat: Ditch bluebird and lodash (#1130)
1 parent 86469ae commit 8899895

9 files changed

Lines changed: 331 additions & 241 deletions

File tree

Scripts/build-webdriveragent.mjs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ const WDA_BUNDLE_TV_PATH = path.join(DERIVED_DATA_PATH, 'Build', 'Products', 'De
2020
const TARGETS = ['runner', 'tv_runner'];
2121
const SDKS = ['sim', 'tv_sim'];
2222

23+
/**
24+
* Build WebDriverAgent and pack the app bundle into a zip archive.
25+
*
26+
* @param {string} [xcodeVersion] Xcode version to include in archive name.
27+
*/
2328
async function buildWebDriverAgent (xcodeVersion) {
2429
const target = process.env.TARGET;
2530
const sdk = process.env.SDK;
@@ -77,10 +82,12 @@ async function buildWebDriverAgent (xcodeVersion) {
7782
}
7883

7984
if (isMainModule) {
80-
buildWebDriverAgent().catch((e) => {
85+
try {
86+
await buildWebDriverAgent();
87+
} catch (e) {
8188
LOG.error(e);
8289
process.exit(1);
83-
});
90+
}
8491
}
8592

8693
export default buildWebDriverAgent;

Scripts/fetch-prebuilt-wda.mjs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ import { fileURLToPath } from 'node:url';
33
import { readFileSync } from 'node:fs';
44
import axios from 'axios';
55
import { logger, fs, mkdirp, net } from '@appium/support';
6-
import _ from 'lodash';
7-
import B from 'bluebird';
86

97
const __filename = fileURLToPath(import.meta.url);
108
const __dirname = path.dirname(__filename);
119
const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === __filename;
1210

1311
const log = logger.getLogger('WDA');
1412

13+
/**
14+
* Download all prebuilt WebDriverAgent archives for the current package version.
15+
*/
1516
async function fetchPrebuiltWebDriverAgentAssets () {
1617
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
1718
const tag = packageJson.version;
@@ -51,20 +52,25 @@ async function fetchPrebuiltWebDriverAgentAssets () {
5152
const url = asset.browser_download_url;
5253
log.info(`Downloading: ${url}`);
5354
try {
54-
const nameOfAgent = _.last(url.split('/'));
55+
const nameOfAgent = url.split('/').at(-1);
56+
if (!nameOfAgent) {
57+
continue;
58+
}
5559
agentsDownloading.push(downloadAgent(url, path.join(webdriveragentsDir, nameOfAgent)));
5660
} catch { }
5761
}
5862

5963
// Wait for them all to finish
60-
return await B.all(agentsDownloading);
64+
return await Promise.all(agentsDownloading);
6165
}
6266

6367
if (isMainModule) {
64-
fetchPrebuiltWebDriverAgentAssets().catch((e) => {
68+
try {
69+
await fetchPrebuiltWebDriverAgentAssets();
70+
} catch (e) {
6571
log.error(e);
6672
process.exit(1);
67-
});
73+
}
6874
}
6975

7076
export default fetchPrebuiltWebDriverAgentAssets;

lib/check-dependencies.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,9 @@ import {WDA_SCHEME, SDK_SIMULATOR, WDA_RUNNER_APP} from './constants';
55
import {BOOTSTRAP_PATH} from './utils';
66
import type {XcodeBuild} from './xcodebuild';
77

8-
async function buildWDASim(): Promise<void> {
9-
const args = [
10-
'-project',
11-
path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'),
12-
'-scheme',
13-
WDA_SCHEME,
14-
'-sdk',
15-
SDK_SIMULATOR,
16-
'CODE_SIGN_IDENTITY=""',
17-
'CODE_SIGNING_REQUIRED="NO"',
18-
'GCC_TREAT_WARNINGS_AS_ERRORS=0',
19-
];
20-
await exec('xcodebuild', args);
21-
}
22-
8+
/**
9+
* Ensure simulator WDA is built and return the resulting app bundle path.
10+
*/
2311
export async function bundleWDASim(xcodebuild: XcodeBuild): Promise<string> {
2412
const derivedDataPath = await xcodebuild.retrieveDerivedDataPath();
2513
if (!derivedDataPath) {
@@ -38,3 +26,18 @@ export async function bundleWDASim(xcodebuild: XcodeBuild): Promise<string> {
3826
await buildWDASim();
3927
return wdaBundlePath;
4028
}
29+
30+
async function buildWDASim(): Promise<void> {
31+
const args = [
32+
'-project',
33+
path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'),
34+
'-scheme',
35+
WDA_SCHEME,
36+
'-sdk',
37+
SDK_SIMULATOR,
38+
'CODE_SIGN_IDENTITY=""',
39+
'CODE_SIGNING_REQUIRED="NO"',
40+
'GCC_TREAT_WARNINGS_AS_ERRORS=0',
41+
];
42+
await exec('xcodebuild', args);
43+
}

lib/utils.ts

Lines changed: 97 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import {exec, SubProcess} from 'teen_process';
33
import path, {dirname} from 'node:path';
44
import {fileURLToPath} from 'node:url';
55
import {log} from './logger';
6-
import _ from 'lodash';
76
import {PLATFORM_NAME_TVOS} from './constants';
8-
import B from 'bluebird';
97
import _fs from 'node:fs';
108
import {waitForCondition} from 'asyncbox';
119
import {arch} from 'node:os';
@@ -19,13 +17,18 @@ const currentFilename =
1917

2018
const currentDirname = dirname(currentFilename);
2119

20+
let moduleRootCache: string | undefined;
21+
2222
/**
2323
* Calculates the path to the current module's root folder
2424
*
2525
* @returns {string} The full path to module root
2626
* @throws {Error} If the current module root folder cannot be determined
2727
*/
28-
const getModuleRoot = _.memoize(function getModuleRoot(): string {
28+
const getModuleRoot = function getModuleRoot(): string {
29+
if (moduleRootCache) {
30+
return moduleRootCache;
31+
}
2932
let currentDir = currentDirname;
3033
let isAtFsRoot = false;
3134
while (!isAtFsRoot) {
@@ -35,22 +38,37 @@ const getModuleRoot = _.memoize(function getModuleRoot(): string {
3538
_fs.existsSync(manifestPath) &&
3639
JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === 'appium-webdriveragent'
3740
) {
41+
moduleRootCache = currentDir;
3842
return currentDir;
3943
}
4044
} catch {}
4145
currentDir = path.dirname(currentDir);
4246
isAtFsRoot = currentDir.length <= path.dirname(currentDir).length;
4347
}
4448
throw new Error('Cannot find the root folder of the appium-webdriveragent Node.js module');
45-
});
49+
};
4650

4751
export const BOOTSTRAP_PATH = getModuleRoot();
4852

53+
/**
54+
* Arguments for setting xctestrun file
55+
*/
56+
export interface XctestrunFileArgs {
57+
deviceInfo: DeviceInfo;
58+
sdkVersion: string;
59+
bootstrapPath: string;
60+
wdaRemotePort: number | string;
61+
wdaBindingIP?: string;
62+
}
63+
64+
/**
65+
* Find and terminate all processes matching the given pgrep pattern.
66+
*/
4967
export async function killAppUsingPattern(pgrepPattern: string): Promise<void> {
5068
const signals = [2, 15, 9];
5169
for (const signal of signals) {
5270
const matchedPids = await getPIDsUsingPattern(pgrepPattern);
53-
if (_.isEmpty(matchedPids)) {
71+
if (matchedPids.length === 0) {
5472
return;
5573
}
5674
const args = [`-${signal}`, ...matchedPids];
@@ -59,21 +77,24 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise<void> {
5977
} catch (err: any) {
6078
log.debug(`kill ${args.join(' ')} -> ${err.message}`);
6179
}
62-
if (signal === _.last(signals)) {
80+
if (signal === signals[signals.length - 1]) {
6381
// there is no need to wait after SIGKILL
6482
return;
6583
}
6684
try {
6785
await waitForCondition(
6886
async () => {
69-
const pidCheckPromises = matchedPids.map((pid) =>
70-
exec('kill', ['-0', pid])
87+
const pidCheckPromises = matchedPids.map(async (pid) => {
88+
try {
89+
await exec('kill', ['-0', pid]);
7190
// the process is still alive
72-
.then(() => false)
91+
return false;
92+
} catch {
7393
// the process is dead
74-
.catch(() => true),
75-
);
76-
return (await B.all(pidCheckPromises)).every((x) => x === true);
94+
return true;
95+
}
96+
});
97+
return (await Promise.all(pidCheckPromises)).every((x) => x === true);
7798
},
7899
{
79100
waitMs: 1000,
@@ -93,9 +114,12 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise<void> {
93114
* @returns Return true if the platformName is tvOS
94115
*/
95116
export function isTvOS(platformName: string): boolean {
96-
return _.toLower(platformName) === _.toLower(PLATFORM_NAME_TVOS);
117+
return platformName?.toLowerCase() === PLATFORM_NAME_TVOS.toLowerCase();
97118
}
98119

120+
/**
121+
* Configure keychain access required for real-device code signing.
122+
*/
99123
export async function setRealDeviceSecurity(
100124
keychainPath: string,
101125
keychainPassword: string,
@@ -106,17 +130,6 @@ export async function setRealDeviceSecurity(
106130
await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]);
107131
}
108132

109-
/**
110-
* Arguments for setting xctestrun file
111-
*/
112-
export interface XctestrunFileArgs {
113-
deviceInfo: DeviceInfo;
114-
sdkVersion: string;
115-
bootstrapPath: string;
116-
wdaRemotePort: number | string;
117-
wdaBindingIP?: string;
118-
}
119-
120133
/**
121134
* Creates xctestrun file per device & platform version.
122135
* We expects to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device
@@ -140,7 +153,7 @@ export async function setXctestrunFile(args: XctestrunFileArgs): Promise<string>
140153
wdaRemotePort,
141154
wdaBindingIP,
142155
);
143-
const newXctestRunContent = _.merge(xctestRunContent, updateWDAPort);
156+
const newXctestRunContent = mergeObjects(xctestRunContent, updateWDAPort);
144157
await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true);
145158

146159
return xctestrunFilePath;
@@ -285,6 +298,23 @@ export async function getWDAUpgradeTimestamp(): Promise<number | null> {
285298
return mtime.getTime();
286299
}
287300

301+
/**
302+
* Escape regular expression metacharacters in a string.
303+
*/
304+
export function escapeRegExp(value: string): string {
305+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
306+
}
307+
308+
/**
309+
* Truncate a string to the given length and append ellipsis if needed.
310+
*/
311+
export function truncateString(value: string, length: number): string {
312+
if (value.length <= length) {
313+
return value;
314+
}
315+
return `${value.slice(0, Math.max(0, length - 1))}…`;
316+
}
317+
288318
/**
289319
* Kills running XCTest processes for the particular device.
290320
*/
@@ -296,7 +326,7 @@ export async function resetTestProcesses(udid: string, isSimulator: boolean): Pr
296326
processPatterns.push(`xctest.*${udid}`);
297327
}
298328
log.debug(`Killing running processes '${processPatterns.join(', ')}' for the device ${udid}...`);
299-
await B.all(processPatterns.map(killAppUsingPattern));
329+
await Promise.all(processPatterns.map(killAppUsingPattern));
300330
}
301331

302332
/**
@@ -329,22 +359,25 @@ export async function getPIDsListeningOnPort(
329359
return result;
330360
}
331361

332-
if (!_.isFunction(filteringFunc)) {
362+
if (typeof filteringFunc !== 'function') {
333363
return result;
334364
}
335-
return await B.filter(result, async (pid) => {
336-
let stdout: string;
337-
try {
338-
({stdout} = await exec('ps', ['-p', pid, '-o', 'command']));
339-
} catch (e: any) {
340-
if (e.code === 1) {
341-
// The process does not exist anymore, there's nothing to filter
342-
return false;
365+
const filtered = await Promise.all(
366+
result.map(async (pid) => {
367+
let stdout: string;
368+
try {
369+
({stdout} = await exec('ps', ['-p', pid, '-o', 'command']));
370+
} catch (e: any) {
371+
if (e.code === 1) {
372+
// The process does not exist anymore, there's nothing to filter
373+
return null;
374+
}
375+
throw e;
343376
}
344-
throw e;
345-
}
346-
return await filteringFunc(stdout);
347-
});
377+
return (await filteringFunc(stdout)) ? pid : null;
378+
}),
379+
);
380+
return filtered.filter((pid): pid is string => Boolean(pid));
348381
}
349382

350383
// Private functions
@@ -359,7 +392,7 @@ async function getPIDsUsingPattern(pattern: string): Promise<string[]> {
359392
return stdout
360393
.split(/\s+/)
361394
.map((x) => parseInt(x, 10))
362-
.filter(_.isInteger)
395+
.filter(Number.isInteger)
363396
.map((x) => `${x}`);
364397
} catch (err: any) {
365398
log.debug(
@@ -368,3 +401,27 @@ async function getPIDsUsingPattern(pattern: string): Promise<string[]> {
368401
return [];
369402
}
370403
}
404+
405+
function mergeObjects<T extends Record<string, any>, U extends Record<string, any>>(
406+
target: T,
407+
source: U,
408+
): T & U {
409+
const output: Record<string, any> = {...target};
410+
for (const [key, sourceValue] of Object.entries(source)) {
411+
const targetValue = output[key];
412+
if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
413+
output[key] = mergeObjects(targetValue, sourceValue);
414+
continue;
415+
}
416+
output[key] = sourceValue;
417+
}
418+
return output as T & U;
419+
}
420+
421+
function isPlainObject(value: unknown): value is Record<string, any> {
422+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
423+
return false;
424+
}
425+
const prototype = Object.getPrototypeOf(value);
426+
return prototype === Object.prototype || prototype === null;
427+
}

0 commit comments

Comments
 (0)