diff --git a/lib/helpers.ts b/lib/helpers.ts index e07c2f9..49ade39 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -1,5 +1,3 @@ -import _ from 'lodash'; -import B from 'bluebird'; import {exec} from 'teen_process'; import type {TeenProcessExecResult} from 'teen_process'; import {fs, plist} from '@appium/support'; @@ -7,6 +5,25 @@ 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( + fn: (...args: Args) => Result, +): (...args: Args) => Result { + const cache = new Map(); + return (...args: Args): Result => { + const key = JSON.stringify(args); + if (!cache.has(key)) { + cache.set(key, fn(...args)); + } + return cache.get(key) as Result; + }; +} + /** * Executes 'xcrun' command line utility * @@ -45,8 +62,12 @@ export async function findAppPaths(bundleId: string): Promise { 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) => @@ -56,7 +77,7 @@ export async function findAppPaths(bundleId: string): Promise { } })(), ); - return (await B.all(results)).filter(Boolean) as string[]; + return (await Promise.all(results)).filter(Boolean) as string[]; } /** diff --git a/lib/xcode.ts b/lib/xcode.ts index a03a120..c3fa6bf 100644 --- a/lib/xcode.ts +++ b/lib/xcode.ts @@ -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; @@ -22,7 +21,7 @@ const log = logger.getLogger('Xcode'); export async function getPathFromXcodeSelect(timeout: number = XCRUN_TIMEOUT): Promise { const generateErrorMessage = async (prefix: string): Promise => { const xcodePaths = await findAppPaths(XCODE_BUNDLE_ID); - if (_.isEmpty(xcodePaths)) { + if (!xcodePaths.length) { return `${prefix}. Consider installing Xcode to address this issue.`; } @@ -91,7 +90,7 @@ export async function getPathFromDeveloperDir(): Promise { * @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 => process.env.DEVELOPER_DIR ? getPathFromDeveloperDir() : getPathFromXcodeSelect(timeout), ); @@ -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, ) { @@ -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 { @@ -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 { diff --git a/package.json b/package.json index 3d38659..bb3934b 100644 --- a/package.json +++ b/package.json @@ -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" }, @@ -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, @@ -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", diff --git a/test/e2e/xcode-specs.js b/test/e2e/xcode-specs.js deleted file mode 100644 index cb0db74..0000000 --- a/test/e2e/xcode-specs.js +++ /dev/null @@ -1,120 +0,0 @@ -import * as xcode from '../../lib/xcode'; -import {fs, util} from '@appium/support'; -import _ from 'lodash'; - -describe('xcode @skip-linux', function () { - // on slow machines and busy CI systems these can be slow and flakey - this.timeout(30000); - - let chai; - let chaiAsPromised; - let should; - - before(async function () { - chai = await import('chai'); - chaiAsPromised = await import('chai-as-promised'); - - should = chai.should(); - chai.use(chaiAsPromised.default); - }); - - describe('getPath', function () { - it('should get the path to xcode from xcode-select', async function () { - const path = await xcode.getPathFromXcodeSelect(); - should.exist(path); - await fs.exists(path); - }); - - it('should get the path to xcode if provided in DEVELOPER_DIR', async function () { - process.env.DEVELOPER_DIR = await xcode.getPathFromXcodeSelect(); - try { - const path = await xcode.getPathFromDeveloperDir(); - should.exist(path); - await fs.exists(path); - } 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 xcode.getPathFromDeveloperDir().should.eventually.be.rejected; - } finally { - delete process.env.DEVELOPER_DIR; - } - }); - - it('should get the path to xcode', async function () { - const path = await xcode.getPath(); - path.should.eql(await xcode.getPathFromXcodeSelect()); - }); - }); - - describe('getVersion', function () { - let versionRE = /\d\.\d\.*\d*/; - - it('should get the version of xcode', async function () { - let version = /** @type {string} */ (await xcode.getVersion()); - should.exist(version); - _.isString(version).should.be.true; - versionRE.test(version).should.be.true; - }); - - it('should get the path and version again, these values are cached', async function () { - await xcode.getPath(); - await xcode.getVersion(); - - let before = Number(new Date()); - let path = await xcode.getPath(); - let after = Number(new Date()); - - should.exist(path); - await fs.exists(path); - (after - before).should.be.at.most(2); - - before = Number(new Date()); - let version = /** @type {string} */ (await xcode.getVersion()); - after = Number(new Date()); - - should.exist(version); - _.isString(version).should.be.true; - versionRE.test(version).should.be.true; - (after - before).should.be.at.most(2); - }); - - it('should get the parsed version', async function () { - let nonParsedVersion = await xcode.getVersion(); - let version = /** @type {import('../../lib/xcode').XcodeVersion} */ ( - await xcode.getVersion(true) - ); - should.exist(version); - _.isString(version.versionString).should.be.true; - version.versionString.should.eql(nonParsedVersion); - - parseFloat(String(version.versionFloat)).should.equal(version.versionFloat); - parseInt(String(version.major), 10).should.equal(version.major); - parseInt(String(version.minor), 10).should.equal(version.minor); - }); - }); - - it('should get clang version', async function () { - const cliVersion = await xcode.getClangVersion(); - _.isString(util.coerceVersion(/** @type {string} */ (cliVersion), true)).should.be.true; - }); - - it('should get max iOS SDK version', async function () { - let version = await xcode.getMaxIOSSDK(); - - should.exist(version); - (typeof version).should.equal('string'); - (parseFloat(String(version)) - 6.1).should.be.at.least(0); - }); - - it('should get max tvOS SDK version', async function () { - let version = await xcode.getMaxTVOSSDK(); - - should.exist(version); - (typeof version).should.equal('string'); - }); -}); diff --git a/test/e2e/xcode-specs.ts b/test/e2e/xcode-specs.ts new file mode 100644 index 0000000..12159e5 --- /dev/null +++ b/test/e2e/xcode-specs.ts @@ -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'); + }); +}); diff --git a/test/unit/helpers-specs.ts b/test/unit/helpers-specs.ts new file mode 100644 index 0000000..7366388 --- /dev/null +++ b/test/unit/helpers-specs.ts @@ -0,0 +1,36 @@ +import {expect} from 'chai'; +import {memoize} from '../../lib/helpers'; + +describe('helpers', function () { + describe('memoize', function () { + it('should cache the result for identical arguments', function () { + let callCount = 0; + const add = memoize((a: number, b: number) => { + callCount += 1; + return a + b; + }); + + const result1 = add(1, 2); + const result2 = add(1, 2); + + expect(result1).to.equal(3); + expect(result2).to.equal(3); + expect(callCount).to.equal(1); + }); + + it('should not reuse cache for different arguments', function () { + let callCount = 0; + const add = memoize((a: number, b: number) => { + callCount += 1; + return a + b; + }); + + const result1 = add(1, 2); + const result2 = add(2, 3); + + expect(result1).to.equal(3); + expect(result2).to.equal(5); + expect(callCount).to.equal(2); + }); + }); +}); diff --git a/test/unit/index-specs.js b/test/unit/index-specs.ts similarity index 51% rename from test/unit/index-specs.js rename to test/unit/index-specs.ts index c1b7895..fa89766 100644 --- a/test/unit/index-specs.js +++ b/test/unit/index-specs.ts @@ -1,14 +1,8 @@ +import {expect} from 'chai'; import xcode from '../../lib/index'; describe('index', function () { - let chai; - - before(async function () { - chai = await import('chai'); - chai.should(); - }); - it('exported objects should exist', function () { - xcode.should.exist; + expect(xcode).to.exist; }); }); diff --git a/tsconfig.json b/tsconfig.json index 0237410..3df5858 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "extends": "@appium/tsconfig/tsconfig.json", "compilerOptions": { "outDir": "build", - "types": ["node"], + "types": ["node", "mocha"], "checkJs": true }, "ts-node": { @@ -13,6 +13,7 @@ } }, "include": [ - "lib" + "lib", + "test" ] }