Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import _ from 'lodash';
import B from 'bluebird';
import {exec} from 'teen_process';
import type {TeenProcessExecResult} from 'teen_process';
import {fs, plist} from '@appium/support';
import path from 'node:path';

export const XCRUN_TIMEOUT = 15000;

/**
* Memoizes function calls by caching results for serialized argument lists.
*
* @param fn The function to memoize
* @returns A memoized wrapper around the input function
*/
export function memoize<Args extends unknown[], Result>(
fn: (...args: Args) => Result,
): (...args: Args) => Result {
const cache = new Map<string, Result>();
return (...args: Args): Result => {
const key = JSON.stringify(args);
if (!cache.has(key)) {
cache.set(key, fn(...args));
}
return cache.get(key) as Result;
};
Comment on lines +14 to +24
}

/**
* Executes 'xcrun' command line utility
*
Expand Down Expand Up @@ -45,8 +62,12 @@ export async function findAppPaths(bundleId: string): Promise<string[]> {
return [];
}

const matchedPaths = _.trim(stdout).split('\n').map(_.trim).filter(Boolean);
if (_.isEmpty(matchedPaths)) {
const matchedPaths = stdout
.trim()
.split('\n')
.map((p) => p.trim())
.filter(Boolean);
if (!matchedPaths.length) {
return [];
}
const results = matchedPaths.map((p) =>
Expand All @@ -56,7 +77,7 @@ export async function findAppPaths(bundleId: string): Promise<string[]> {
}
})(),
);
return (await B.all(results)).filter(Boolean) as string[];
return (await Promise.all(results)).filter(Boolean) as string[];
}

/**
Expand Down
13 changes: 6 additions & 7 deletions lib/xcode.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {fs, logger} from '@appium/support';
import path from 'node:path';
import {retry} from 'asyncbox';
import _ from 'lodash';
import {exec} from 'teen_process';
import * as semver from 'semver';
import {runXcrunCommand, findAppPaths, XCRUN_TIMEOUT, readXcodePlist} from './helpers';
import {runXcrunCommand, findAppPaths, XCRUN_TIMEOUT, readXcodePlist, memoize} from './helpers';
import type {XcodeVersion} from './types';

const DEFAULT_NUMBER_OF_RETRIES = 2;
Expand All @@ -22,7 +21,7 @@ const log = logger.getLogger('Xcode');
export async function getPathFromXcodeSelect(timeout: number = XCRUN_TIMEOUT): Promise<string> {
const generateErrorMessage = async (prefix: string): Promise<string> => {
const xcodePaths = await findAppPaths(XCODE_BUNDLE_ID);
if (_.isEmpty(xcodePaths)) {
if (!xcodePaths.length) {
return `${prefix}. Consider installing Xcode to address this issue.`;
}

Expand Down Expand Up @@ -91,7 +90,7 @@ export async function getPathFromDeveloperDir(): Promise<string> {
* @returns Full path to Xcode Developer subfolder timeout
* @throws {Error} If there was an error while retrieving the path.
*/
export const getPath = _.memoize(
export const getPath = memoize(
(timeout: number = XCRUN_TIMEOUT): Promise<string> =>
process.env.DEVELOPER_DIR ? getPathFromDeveloperDir() : getPathFromXcodeSelect(timeout),
);
Expand Down Expand Up @@ -189,7 +188,7 @@ export async function getMaxIOSSDKWithoutRetry(timeout: number = XCRUN_TIMEOUT):
* @returns The SDK version
* @throws {Error} If the SDK version number cannot be determined
*/
export const getMaxIOSSDK = _.memoize(function getMaxIOSSDK(
export const getMaxIOSSDK = memoize(function getMaxIOSSDK(
retries: number = DEFAULT_NUMBER_OF_RETRIES,
timeout: number = XCRUN_TIMEOUT,
) {
Expand Down Expand Up @@ -221,7 +220,7 @@ export async function getMaxTVOSSDKWithoutRetry(timeout: number = XCRUN_TIMEOUT)
* @returns The SDK version
* @throws {Error} If the SDK version number cannot be determined
*/
export const getMaxTVOSSDK = _.memoize(async function getMaxTVOSSDK(
export const getMaxTVOSSDK = memoize(async function getMaxTVOSSDK(
retries: number = DEFAULT_NUMBER_OF_RETRIES,
timeout: number = XCRUN_TIMEOUT,
): Promise<string> {
Expand Down Expand Up @@ -254,7 +253,7 @@ async function getVersionWithoutRetry(
* @returns Xcode version
* @throws {Error} If there was a failure while retrieving the version
*/
const getVersionMemoized = _.memoize(function getVersionMemoized(
const getVersionMemoized = memoize(function getVersionMemoized(
retries: number = DEFAULT_NUMBER_OF_RETRIES,
timeout: number = XCRUN_TIMEOUT,
): Promise<semver.SemVer | null> {
Expand Down
9 changes: 2 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@
"dependencies": {
"@appium/support": "^7.0.0-rc.1",
"asyncbox": "^6.0.1",
"bluebird": "^3.7.2",
"lodash": "^4.17.4",
"plist": "^3.0.1",
"semver": "^7.0.0",
"teen_process": "^4.0.4"
},
Expand All @@ -46,8 +43,8 @@
"format": "prettier -w ./lib ./test",
"format:check": "prettier --check ./lib ./test",
"prepare": "npm run build",
"test": "mocha --exit --timeout 1m \"./test/unit/**/*-specs.js\"",
"e2e-test": "mocha --exit --timeout 5m \"./test/e2e/**/*-specs.js\""
"test": "mocha --exit --timeout 1m \"./test/unit/**/*-specs.ts\"",
"e2e-test": "mocha --exit --timeout 5m \"./test/e2e/**/*-specs.ts\""
},
"prettier": {
"bracketSpacing": false,
Expand All @@ -60,8 +57,6 @@
"@appium/types": "^1.0.0-rc.1",
"@semantic-release/changelog": "^6.0.1",
"@semantic-release/git": "^10.0.1",
"@types/bluebird": "^3.5.38",
"@types/lodash": "^4.14.196",
"@types/mocha": "^10.0.1",
"@types/node": "^25.0.0",
"chai": "^6.0.0",
Expand Down
120 changes: 0 additions & 120 deletions test/e2e/xcode-specs.js

This file was deleted.

110 changes: 110 additions & 0 deletions test/e2e/xcode-specs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {fs, util} from '@appium/support';
import {expect, use} from 'chai';
import chaiAsPromised from 'chai-as-promised';
import * as xcode from '../../lib/xcode';

use(chaiAsPromised);

describe('xcode @skip-linux', function () {
// on slow machines and busy CI systems these can be slow and flakey
this.timeout(30000);

describe('getPath', function () {
it('should get the path to xcode from xcode-select', async function () {
const xcodePath = await xcode.getPathFromXcodeSelect();
expect(xcodePath).to.exist;
await fs.exists(xcodePath);
});

it('should get the path to xcode if provided in DEVELOPER_DIR', async function () {
process.env.DEVELOPER_DIR = await xcode.getPathFromXcodeSelect();
try {
const xcodePath = await xcode.getPathFromDeveloperDir();
expect(xcodePath).to.exist;
await fs.exists(xcodePath);
} finally {
delete process.env.DEVELOPER_DIR;
}
});

it('should fail if the path to xcode provided in DEVELOPER_DIR is wrong', async function () {
process.env.DEVELOPER_DIR = 'yolo';
try {
await expect(xcode.getPathFromDeveloperDir()).to.be.rejected;
} finally {
delete process.env.DEVELOPER_DIR;
}
});

it('should get the path to xcode', async function () {
const xcodePath = await xcode.getPath();
expect(xcodePath).to.eql(await xcode.getPathFromXcodeSelect());
});
});

describe('getVersion', function () {
const versionRE = /\d\.\d\.*\d*/;

it('should get the version of xcode', async function () {
const version = await xcode.getVersion(false);
expect(version).to.exist;
expect(version).to.be.a('string');
expect(versionRE.test(version)).to.be.true;
});

it('should get the path and version again, these values are cached', async function () {
await xcode.getPath();
await xcode.getVersion(false);

let before = Number(new Date());
const xcodePath = await xcode.getPath();
let after = Number(new Date());

expect(xcodePath).to.exist;
await fs.exists(xcodePath);
expect(after - before).to.be.at.most(2);

before = Number(new Date());
const version = await xcode.getVersion(false);
after = Number(new Date());

expect(version).to.exist;
expect(version).to.be.a('string');
expect(versionRE.test(version)).to.be.true;
expect(after - before).to.be.at.most(2);
});

it('should get the parsed version', async function () {
const nonParsedVersion = await xcode.getVersion(false);
const version = await xcode.getVersion(true);
expect(version).to.exist;
expect(version.versionString).to.be.a('string');
expect(version.versionString).to.eql(nonParsedVersion);

expect(parseFloat(String(version.versionFloat))).to.equal(version.versionFloat);
expect(parseInt(String(version.major), 10)).to.equal(version.major);
expect(parseInt(String(version.minor), 10)).to.equal(version.minor);
});
});

it('should get clang version', async function () {
const cliVersion = await xcode.getClangVersion();
expect(cliVersion).to.exist;
expect(util.coerceVersion(cliVersion!, true)).to.be.a('string');
});

it('should get max iOS SDK version', async function () {
const version = await xcode.getMaxIOSSDK();

expect(version).to.exist;
expect(version).to.be.a('string');
expect(parseFloat(String(version)) - 6.1).to.be.at.least(0);
});

it('should get max tvOS SDK version', async function () {
const version = await xcode.getMaxTVOSSDK();

expect(version).to.exist;
expect(version).to.be.a('string');
});
});
Loading
Loading