Skip to content

Commit

Permalink
feat: Add doctor checks
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach committed Jan 7, 2024
1 parent 48906b6 commit 4c69bcb
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 3 deletions.
51 changes: 51 additions & 0 deletions .github/workflows/functional-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Functional Tests

on:
pull_request:
branches:
- master
paths-ignore:
- 'docs/**'
- '*.md'

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
CI: true

jobs:
build:

strategy:
matrix:
xcodeVersion: ['13.4', '14.2']
platform: ['macos-12']
fail-fast: false

runs-on: ${{ matrix.platform }}

name: e2e
steps:
- uses: actions/checkout@v3

- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 'lts/*'

- name: Select Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "${{ matrix.xcodeVersion }}"

- run: |
export cwd=$(pwd)
pushd "$cwd"
cd ~
npm install -g appium
appium driver install --source=local "$cwd"
appium driver doctor mac2
popd
name: Install and run doctor checks
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ On top of standard Appium requirements Mac2 driver also expects the following pr
- Xcode 13 or later should be installed
- `xcode-select` should be pointing to `<full_path_to_xcode_app>/Contents/Developer` developer directory instead of `/Library/Developer/CommandLineTools` to run `xcodebuild` commands
- Xcode Helper app should be enabled for Accessibility access. The app itself could be usually found at `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/Agents/Xcode Helper.app`. In order to enable Accessibility access for it simply open the parent folder in Finder: `open /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/Agents/` and drag & drop the `Xcode Helper` app to `Security & Privacy -> Privacy -> Accessibility` list of your `System Preferences`. This action must only be done once.
- `testmanagerd` proccess requires UIAutomation authentication since macOS 12. `automationmodetool enable-automationmode-without-authentication` command may help to disable it. This may be particularly useful in CI environments. [Apple forum thread](https://developer.apple.com/forums/thread/693850).
- `testmanagerd` process requires UIAutomation authentication since macOS 12. `automationmodetool enable-automationmode-without-authentication` command may help to disable it. This may be particularly useful in CI environments. [Apple forum thread](https://developer.apple.com/forums/thread/693850).

### Doctor

Since driver version 1.9.0 you can automate the validation for the most of the above
requirements as well as various optional ones needed by driver extensions by running the
`appium driver doctor mac2` server command.


## Capabilities
Expand Down
65 changes: 65 additions & 0 deletions lib/doctor/optional-checks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* eslint-disable require-await */
import {resolveExecutablePath} from './utils';
import {doctor} from 'appium/support';
import {exec} from 'teen_process';
import '@colors/colors';

/** @satisfies {import('@appium/types').IDoctorCheck} */
export class OptionalFfmpegCheck {
FFMPEG_BINARY = 'ffmpeg';
FFMPEG_INSTALL_LINK = 'https://www.ffmpeg.org/download.html';

async diagnose() {
const ffmpegPath = await resolveExecutablePath(this.FFMPEG_BINARY);

return ffmpegPath
? doctor.okOptional(`${this.FFMPEG_BINARY} exists at '${ffmpegPath}'`)
: doctor.nokOptional(`${this.FFMPEG_BINARY} cannot be found`);
}

async fix() {
return (
`${`${this.FFMPEG_BINARY}`.bold} is used to capture screen recordings. ` +
`Please read ${this.FFMPEG_INSTALL_LINK}.`
);
}

hasAutofix() {
return false;
}

isOptional() {
return true;
}
}
export const optionalFfmpegCheck = new OptionalFfmpegCheck();


/** @satisfies {import('@appium/types').IDoctorCheck} */
export class OptionalAutomationModeCheck {
async diagnose() {
let stdout;
try {
({stdout} = await exec('automationmodetool'));
} catch (err) {
return doctor.nokOptional(`Cannot run 'automationmodetool': ${err.stderr || err.message}`);
}
if (stdout.includes('disabled')) {
return doctor.nokOptional(`Automation Mode is disabled`);
}
return doctor.okOptional(`Automation Mode is enabled`);
}

async fix() {
return `Run \`automationmodetool enable-automationmode-without-authentication\` to enable Automation Mode`;
}

hasAutofix() {
return false;
}

isOptional() {
return true;
}
}
export const optionalAutomationModeCheck = new OptionalAutomationModeCheck();
113 changes: 113 additions & 0 deletions lib/doctor/required-checks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/* eslint-disable require-await */
import {doctor} from 'appium/support';
import {exec} from 'teen_process';
import { getPath as getXcodePath } from 'appium-xcode';
import '@colors/colors';


/** @satisfies {import('@appium/types').IDoctorCheck} */
export class XcodeCheck {
async diagnose() {
try {
const xcodePath = await getXcodePath();
return doctor.ok(`xCode is installed at '${xcodePath}'`);
} catch (err) {
return doctor.nok(err.message);
}
}

async fix() {
return `Manually install ${'Xcode'.bold} and configure the active developer directory path using the xcode-select tool`;
}

hasAutofix() {
return false;
}

isOptional() {
return false;
}
}
export const xcodeCheck = new XcodeCheck();


/** @satisfies {import('@appium/types').IDoctorCheck} */
export class XcodebuildCheck {
XCODE_VER_PATTERN = /^Xcode\s+([\d.]+)$/m;
MIN_XCODE_VERSION = 13;

async diagnose() {
let xcodeVerMatch;
let stdout;
let stderr;
try {
({stdout, stderr} = await exec('xcodebuild', ['-version']));
xcodeVerMatch = this.XCODE_VER_PATTERN.exec(stdout);
} catch (err) {
return doctor.nok(`Cannot run 'xcodebuild': ${err.stderr || err.message}`);
}
if (!xcodeVerMatch) {
return doctor.nok(`Cannot determine Xcode version. stdout: ${stdout}; stderr: ${stderr}`);
}
const xcodeMajorVer = parseInt(xcodeVerMatch[1], 10);
if (xcodeMajorVer < this.MIN_XCODE_VERSION) {
return doctor.nok(`The actual Xcode version (${xcodeVerMatch[0]}) is older than the expected ` +
`one (${this.MIN_XCODE_VERSION})`);
}
return doctor.ok(`xcodebuild is installed and has a matching version number ` +
`(${xcodeVerMatch[1]} >= ${this.MIN_XCODE_VERSION})`);
}

async fix() {
return `Fix problems xcodebuild is complaining about`;
}

hasAutofix() {
return false;
}

isOptional() {
return false;
}
}
export const xcodebuildCheck = new XcodebuildCheck();


/** @satisfies {import('@appium/types').IDoctorCheck} */
export class MacosVersionCheck {
MIN_MACOS_VERSION = 11;

async diagnose() {
let macosVer;
let stdout;
let stderr;
try {
({stdout, stderr} = await exec('sw_vers', ['--productVersion']));
macosVer = stdout.trim();
} catch (err) {
return doctor.nok(`Cannot run 'sw_vers --productVersion': ${err.stderr || err.message}`);
}
const macosMajorVer = parseInt(macosVer, 10);
if (!macosMajorVer) {
return doctor.nok(`Cannot determine macOS version. stdout: ${stdout}; stderr: ${stderr}`);
}
if (macosMajorVer < this.MIN_MACOS_VERSION) {
return doctor.nok(`The actual macOS version (${macosVer}) is older than the expected ` +
`one (${this.MIN_MACOS_VERSION})`);
}
return doctor.ok(`macOS has a matching version number (${macosVer} >= ${this.MIN_MACOS_VERSION})`);
}

async fix() {
return `Fix problems xcodebuild is complaining about`;
}

hasAutofix() {
return false;
}

isOptional() {
return false;
}
}
export const macosVersionCheck = new MacosVersionCheck();
17 changes: 17 additions & 0 deletions lib/doctor/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {fs} from 'appium/support';

/**
* Return an executable path of cmd
*
* @param {string} cmd Standard output by command
* @return {Promise<string?>} The full path of cmd. `null` if the cmd is not found.
*/
export async function resolveExecutablePath(cmd) {
try {
const executablePath = await fs.which(cmd);
if (executablePath && (await fs.exists(executablePath))) {
return executablePath;
}
} catch (err) {}
return null;
}
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,20 @@
"platformNames": [
"Mac"
],
"mainClass": "Mac2Driver"
"mainClass": "Mac2Driver",
"doctor": {
"checks": [
"./build/lib/doctor/required-checks.js",
"./build/lib/doctor/optional-checks.js"
]
}
},
"main": "./build/index.js",
"bin": {},
"dependencies": {
"@appium/strongbox": "^0.x",
"@colors/colors": "^1.6.0",
"appium-xcode": "^5.1.4",
"asyncbox": "^3.0.0",
"axios": "^1.x",
"bluebird": "^3.5.1",
Expand All @@ -68,7 +76,7 @@
"precommit-lint"
],
"peerDependencies": {
"appium": "^2.0.0"
"appium": "^2.4.1"
},
"devDependencies": {
"@appium/eslint-config-appium": "^8.0.4",
Expand Down

0 comments on commit 4c69bcb

Please sign in to comment.