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
2 changes: 2 additions & 0 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ jobs:
name: Install dev dependencies
- run: npm run lint
name: Run linter
- run: npm run format:check
name: Run Prettier check
- run: npm run test
name: Run unit tests
17 changes: 9 additions & 8 deletions lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as semver from 'semver';
import { spawn } from 'node:child_process';
import { Readable } from 'node:stream';
import {spawn} from 'node:child_process';
import {Readable} from 'node:stream';

export const DEFAULT_EXEC_TIMEOUT = 10 * 60 * 1000; // ms
export const SIM_RUNTIME_NAME = 'com.apple.CoreSimulator.SimRuntime.';
Expand All @@ -13,7 +13,7 @@ export const SIM_RUNTIME_NAME = 'com.apple.CoreSimulator.SimRuntime.';
* @return The version in 'major.minor' form
* @throws {Error} If the version not parseable by the `semver` package
*/
export function normalizeVersion (version: string): string {
export function normalizeVersion(version: string): string {
const semverVersion = semver.coerce(version);
if (!semverVersion) {
throw new Error(`Unable to parse version '${version}'`);
Expand All @@ -24,7 +24,7 @@ export function normalizeVersion (version: string): string {
/**
* @returns The xcrun binary name
*/
export function getXcrunBinary (): string {
export function getXcrunBinary(): string {
return process.env.XCRUN_BINARY || 'xcrun';
}

Expand All @@ -33,7 +33,7 @@ export function getXcrunBinary (): string {
*
* @returns Promise resolving to UUID string
*/
export async function uuidV4 (): Promise<string> {
export async function uuidV4(): Promise<string> {
const uuidLib = await import('uuid');
return uuidLib.v4();
}
Expand All @@ -45,7 +45,7 @@ export async function uuidV4 (): Promise<string> {
* @return Promise resolving to parsed JSON object
* @throws {Error} If plutil fails to convert the input
*/
export async function convertPlistToJson (plistInput: string): Promise<any> {
export async function convertPlistToJson(plistInput: string): Promise<any> {
const plutilProcess = spawn('plutil', ['-convert', 'json', '-o', '-', '-']);
let jsonOutput = '';
plutilProcess.stdout.on('data', (chunk) => {
Expand All @@ -71,11 +71,12 @@ export async function convertPlistToJson (plistInput: string): Promise<any> {
});
} catch (err) {
plutilProcess.kill(9);
throw new Error(`Failed to convert plist to JSON: ${err instanceof Error ? err.message : String(err)}`);
throw new Error(
`Failed to convert plist to JSON: ${err instanceof Error ? err.message : String(err)}`,
);
} finally {
plutilProcess.removeAllListeners();
inputStream.removeAllListeners();
}
return JSON.parse(jsonOutput);
}

4 changes: 2 additions & 2 deletions lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import appiumLogger from '@appium/logger';

export const LOG_PREFIX = 'simctl';

function getLogger () {
function getLogger() {
const logger = global._global_npmlog || appiumLogger;
if (!logger.debug) {
logger.addLevel('debug', 1000, { fg: 'blue', bg: 'black' }, 'dbug');
logger.addLevel('debug', 1000, {fg: 'blue', bg: 'black'}, 'dbug');
}
return logger;
}
Expand Down
66 changes: 34 additions & 32 deletions lib/simctl.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import _ from 'lodash';
import which from 'which';
import { log, LOG_PREFIX } from './logger';
import {
DEFAULT_EXEC_TIMEOUT, getXcrunBinary,
} from './helpers';
import { exec as tpExec, SubProcess } from 'teen_process';
import {log, LOG_PREFIX} from './logger';
import {DEFAULT_EXEC_TIMEOUT, getXcrunBinary} from './helpers';
import {exec as tpExec, SubProcess} from 'teen_process';
import * as addmediaCommands from './subcommands/addmedia';
import * as appinfoCommands from './subcommands/appinfo';
import * as bootCommands from './subcommands/boot';
Expand All @@ -30,9 +28,7 @@ import * as terminateCommands from './subcommands/terminate';
import * as uiCommands from './subcommands/ui';
import * as uninstallCommands from './subcommands/uninstall';
import * as locationCommands from './subcommands/location';
import type {
XCRun, ExecOpts, SimctlOpts, ExecResult,
} from './types';
import type {XCRun, ExecOpts, SimctlOpts, ExecResult} from './types';

const SIMCTL_ENV_PREFIX = 'SIMCTL_CHILD_';

Expand All @@ -43,27 +39,27 @@ export class Simctl {
private _udid: string | null;
private _devicesSetPath: string | null;

constructor (opts: SimctlOpts = {}) {
this.xcrun = _.cloneDeep(opts.xcrun ?? { path: null });
constructor(opts: SimctlOpts = {}) {
this.xcrun = _.cloneDeep(opts.xcrun ?? {path: null});
this.execTimeout = opts.execTimeout ?? DEFAULT_EXEC_TIMEOUT;
this.logErrors = opts.logErrors ?? true;
this._udid = opts.udid ?? null;
this._devicesSetPath = opts.devicesSetPath ?? null;
}

set udid (value: string | null) {
set udid(value: string | null) {
this._udid = value;
}

get udid (): string | null {
get udid(): string | null {
return this._udid;
}

set devicesSetPath (value: string | null) {
set devicesSetPath(value: string | null) {
this._devicesSetPath = value;
}

get devicesSetPath (): string | null {
get devicesSetPath(): string | null {
return this._devicesSetPath;
}

Expand All @@ -72,26 +68,30 @@ export class Simctl {
* @returns The UDID string
* @throws {Error} If UDID is not set
*/
requireUdid (commandName: string | null = null): string {
requireUdid(commandName: string | null = null): string {
if (!this.udid) {
throw new Error(`udid is required to be set for ` +
(commandName ? `the '${commandName}' command` : 'this simctl command'));
throw new Error(
`udid is required to be set for ` +
(commandName ? `the '${commandName}' command` : 'this simctl command'),
);
}
return this.udid;
}

/**
* @returns Promise resolving to the xcrun binary path
*/
async requireXcrun (): Promise<string> {
async requireXcrun(): Promise<string> {
const xcrunBinary = getXcrunBinary();

if (!this.xcrun.path) {
try {
this.xcrun.path = await which(xcrunBinary);
} catch {
throw new Error(`${xcrunBinary} tool has not been found in PATH. ` +
`Are Xcode developers tools installed?`);
throw new Error(
`${xcrunBinary} tool has not been found in PATH. ` +
`Are Xcode developers tools installed?`,
);
}
}
if (!this.xcrun.path) {
Expand All @@ -110,10 +110,7 @@ export class Simctl {
* `SubProcess` instance depending of `opts.asynchronous` value.
* @throws {Error} If the simctl subcommand command returns non-zero return code.
*/
async exec<T extends ExecOpts> (
subcommand: string,
opts?: T
): Promise<ExecResult<T>> {
async exec<T extends ExecOpts>(subcommand: string, opts?: T): Promise<ExecResult<T>> {
const {
args: initialArgs = [],
env: initialEnv = {},
Expand All @@ -122,20 +119,21 @@ export class Simctl {
logErrors = true,
architectures,
timeout,
} = opts ?? {} as T;
} = opts ?? ({} as T);
// run a particular simctl command
const args = [
'simctl',
...(this.devicesSetPath ? ['--set', this.devicesSetPath] : []),
subcommand,
...initialArgs
...initialArgs,
];
// Prefix all passed in environment variables with 'SIMCTL_CHILD_', simctl
// will then pass these to the child (spawned) process.
const env = _.defaults(
_.mapKeys(initialEnv,
(value, key) => _.startsWith(key, SIMCTL_ENV_PREFIX) ? key : `${SIMCTL_ENV_PREFIX}${key}`),
process.env
_.mapKeys(initialEnv, (value, key) =>
_.startsWith(key, SIMCTL_ENV_PREFIX) ? key : `${SIMCTL_ENV_PREFIX}${key}`,
),
process.env,
);

const execOpts: any = {
Expand All @@ -150,14 +148,19 @@ export class Simctl {
let execArgs: [string, string[], any];
if (architectures?.length) {
const archArgs = _.flatMap(
(_.isArray(architectures) ? architectures : [architectures]).map((arch) => ['-arch', arch])
(_.isArray(architectures) ? architectures : [architectures]).map((arch) => [
'-arch',
arch,
]),
);
execArgs = ['arch', [...archArgs, xcrun, ...args], execOpts];
} else {
execArgs = [xcrun, args, execOpts];
}
// We know what we are doing here - the type system can't handle the dynamic nature
return (asynchronous ? new SubProcess(...execArgs) : await tpExec(...execArgs)) as ExecResult<T>;
return (
asynchronous ? new SubProcess(...execArgs) : await tpExec(...execArgs)
) as ExecResult<T>;
} catch (e: any) {
if (!this.logErrors || !logErrors) {
// if we don't want to see the errors, just throw and allow the calling
Expand Down Expand Up @@ -218,4 +221,3 @@ export class Simctl {
}

export default Simctl;

9 changes: 6 additions & 3 deletions lib/subcommands/addmedia.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Simctl } from '../simctl';
import type { TeenProcessExecResult } from 'teen_process';
import type {Simctl} from '../simctl';
import type {TeenProcessExecResult} from 'teen_process';

/**
* Add the particular media file to Simulator's library.
Expand All @@ -12,7 +12,10 @@ import type { TeenProcessExecResult } from 'teen_process';
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
export async function addMedia (this: Simctl, filePath: string): Promise<TeenProcessExecResult<string>> {
export async function addMedia(
this: Simctl,
filePath: string,
): Promise<TeenProcessExecResult<string>> {
return await this.exec('addmedia', {
args: [this.requireUdid('addmedia'), filePath],
});
Expand Down
10 changes: 5 additions & 5 deletions lib/subcommands/appinfo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Simctl } from '../simctl';
import type { AppInfo } from '../types';
import { convertPlistToJson } from '../helpers';
import type {Simctl} from '../simctl';
import type {AppInfo} from '../types';
import {convertPlistToJson} from '../helpers';
import _ from 'lodash';

/**
Expand All @@ -13,7 +13,7 @@ import _ from 'lodash';
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
export async function appInfo (this: Simctl, bundleId: string): Promise<AppInfo> {
export async function appInfo(this: Simctl, bundleId: string): Promise<AppInfo> {
const {stdout} = await this.exec('appinfo', {
args: [this.requireUdid('appinfo'), bundleId],
});
Expand All @@ -26,7 +26,7 @@ export async function appInfo (this: Simctl, bundleId: string): Promise<AppInfo>
result = await convertPlistToJson(stdout);
} catch (err) {
throw new Error(
`Cannot retrieve app info for ${bundleId}: ${err instanceof Error ? err.message : String(err)}`
`Cannot retrieve app info for ${bundleId}: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
Expand Down
8 changes: 4 additions & 4 deletions lib/subcommands/boot.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _ from 'lodash';
import { log, LOG_PREFIX } from '../logger';
import type { Simctl } from '../simctl';
import {log, LOG_PREFIX} from '../logger';
import type {Simctl} from '../simctl';

/**
* Boot the particular Simulator if it is not running.
Expand All @@ -9,10 +9,10 @@ import type { Simctl } from '../simctl';
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
export async function bootDevice (this: Simctl): Promise<void> {
export async function bootDevice(this: Simctl): Promise<void> {
try {
await this.exec('boot', {
args: [this.requireUdid('boot')]
args: [this.requireUdid('boot')],
});
} catch (e: any) {
if (_.includes(e.message, 'Unable to boot device in current state: Booted')) {
Expand Down
Loading