From cde5fd3a4e6d5a346222b12a358f5f6767c33cd1 Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Fri, 25 May 2018 21:25:11 +1200 Subject: [PATCH] Running on windows support (#628) * Update package scripts to use node for cross-plattform. * Fix detox test / run-server using paths that don't work on windows. * Replace CRLFs in exec.js, fixes running the emulator on windows. * Should fix build.js on mac. * Remove some paranoia. * Make eslint pass on JS scripts. * Ignore exec hack in coverage to get CI passing. * Walk back detox test change so it works with detox-cli. * Don't try to start emulator if it's already running. * Remove unneeded comments, only refresh ADB devices if needed. * Update detox unit tests to pass on windows. Use path module in tests to normalize paths. Use process.platform to test with actual absolute paths on windows. * Add windows support to detox-cli. * Some basic documentation in roadmap. * docs: 'detox' -> 'Detox' * More docs fixes. * Oops, missed building Detox-ios-src.tbz * Lint complained about hashbangs in detox scripts. --- detox-cli/cli.js | 28 +++++++++++++++ detox-cli/cli.sh | 12 ------- detox-cli/package.json | 2 +- detox/local-cli/detox-run-server.js | 11 +++--- detox/local-cli/detox-test.js | 7 ++-- detox/package.json | 6 ++-- detox/scripts/build.js | 43 +++++++++++++++++++++++ detox/scripts/build.sh | 29 --------------- detox/scripts/postinstall.js | 5 +++ detox/scripts/postinstall.sh | 5 --- detox/src/configurations.mock.js | 2 +- detox/src/devices/Device.test.js | 7 ++-- detox/src/devices/EmulatorDriver.js | 14 ++++++-- detox/src/devices/android/APKPath.test.js | 16 +++++---- detox/src/utils/exec.js | 10 ++++++ docs/More.Roadmap.md | 7 ++++ 16 files changed, 132 insertions(+), 72 deletions(-) create mode 100644 detox-cli/cli.js delete mode 100755 detox-cli/cli.sh create mode 100755 detox/scripts/build.js delete mode 100755 detox/scripts/build.sh create mode 100755 detox/scripts/postinstall.js delete mode 100755 detox/scripts/postinstall.sh diff --git a/detox-cli/cli.js b/detox-cli/cli.js new file mode 100644 index 0000000000..ca7fe3823f --- /dev/null +++ b/detox-cli/cli.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node +const cp = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +const detoxPath = path.join(process.cwd(), 'node_modules/detox'); +const detoxPackageJsonPath = path.join(detoxPath, 'package.json'); + +if (fs.existsSync(detoxPackageJsonPath)) { + // { shell: true } option seems to break quoting on windows? Otherwise this would be much simpler. + if (process.platform === 'win32') { + const result = cp.spawnSync( + 'cmd', + ['/c', path.join(process.cwd(), 'node_modules/.bin/detox.cmd')].concat(process.argv.slice(2)), + { stdio: 'inherit' }); + process.exit(result.status); + } else { + const result = cp.spawnSync( + path.join(process.cwd(), 'node_modules/.bin/detox'), + process.argv.slice(2), + { stdio: 'inherit' }); + process.exit(result.status); + } +} else { + console.log(detoxPackageJsonPath); + console.log("detox is not installed in this directory"); + process.exit(1); +} \ No newline at end of file diff --git a/detox-cli/cli.sh b/detox-cli/cli.sh deleted file mode 100755 index b9d76c9f3f..0000000000 --- a/detox-cli/cli.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -DETOX_PATH="${PWD}/node_modules/detox"; -DETOX_PACKAGE_JSON_PATH="${DETOX_PATH}/package.json"; - -if [ -f "${DETOX_PACKAGE_JSON_PATH}" ]; then - "${PWD}/node_modules/.bin/detox" $@ -else - echo "${DETOX_PACKAGE_JSON_PATH}" - echo "detox is not installed in this directory" - exit 1 -fi \ No newline at end of file diff --git a/detox-cli/package.json b/detox-cli/package.json index fe42724de5..519062687e 100644 --- a/detox-cli/package.json +++ b/detox-cli/package.json @@ -7,7 +7,7 @@ "test": ":" }, "bin": { - "detox": "./cli.sh" + "detox": "./cli.js" }, "repository": { "type": "git", diff --git a/detox/local-cli/detox-run-server.js b/detox/local-cli/detox-run-server.js index 082f766190..a521300ce6 100644 --- a/detox/local-cli/detox-run-server.js +++ b/detox/local-cli/detox-run-server.js @@ -2,14 +2,11 @@ const program = require('commander'); const cp = require('child_process'); -const fs = require('fs'); const path = require('path'); program.parse(process.argv); -if (fs.existsSync(path.join(process.cwd(), 'node_modules/.bin/detox-server'))) { - cp.execSync('node_modules/.bin/detox-server', {stdio: 'inherit'}); -} else { - cp.execSync('node_modules/detox/node_modules/.bin/detox-server', {stdio: 'inherit'}); -} - +const serverPackagePath = require.resolve('detox-server/package.json'); +const cli = require(serverPackagePath).bin['detox-server']; +const binPath = path.join(path.dirname(serverPackagePath), cli); +cp.execFileSync('node', [binPath], {stdio: 'inherit'}); diff --git a/detox/local-cli/detox-test.js b/detox/local-cli/detox-test.js index 5b2b6fee54..ed4cb3a2ce 100644 --- a/detox/local-cli/detox-test.js +++ b/detox/local-cli/detox-test.js @@ -99,7 +99,8 @@ function runMocha() { const headless = program.headless ? `--headless` : ''; const debugSynchronization = program.debugSynchronization ? `--debug-synchronization ${program.debugSynchronization}` : ''; - const command = `node_modules/.bin/mocha ${testFolder} ${configFile} ${configuration} ${loglevel} ${cleanup} ${reuse} ${debugSynchronization} ${platformString} ${artifactsLocation} ${headless}`; + const binPath = path.join('node_modules', '.bin', 'mocha'); + const command = `${binPath} ${testFolder} ${configFile} ${configuration} ${loglevel} ${cleanup} ${reuse} ${debugSynchronization} ${platformString} ${artifactsLocation} ${headless}`; console.log(command); cp.execSync(command, {stdio: 'inherit'}); @@ -107,8 +108,10 @@ function runMocha() { function runJest() { const configFile = runnerConfig ? `--config=${runnerConfig}` : ''; + const platform = program.platform ? `--testNamePattern='^((?!${getPlatformSpecificString(program.platform)}).)*$'` : ''; + const binPath = path.join('node_modules', '.bin', 'jest'); const platformString = platform ? `--testNamePattern='^((?!${getPlatformSpecificString(platform)}).)*$'` : ''; - const command = `node_modules/.bin/jest ${testFolder} ${configFile} --runInBand ${platformString}`; + const command = `${binPath} ${testFolder} ${configFile} --runInBand ${platformString}`; console.log(command); cp.execSync(command, { stdio: 'inherit', diff --git a/detox/package.json b/detox/package.json index 593a377f3b..4472787788 100644 --- a/detox/package.json +++ b/detox/package.json @@ -21,14 +21,14 @@ "author": "Tal Kol ", "license": "MIT", "scripts": { - "build": "scripts/build.sh", - "lint": "eslint src", + "build": "node scripts/build.js", + "lint": "eslint src scripts", "unit": "jest --coverage --verbose", "pretest": "npm run lint", "test": "npm run unit", "unit:watch": "jest --watch", "prepublish": "npm run build", - "postinstall": "scripts/postinstall.sh" + "postinstall": "node scripts/postinstall.js" }, "devDependencies": { "eslint": "^4.11.0", diff --git a/detox/scripts/build.js b/detox/scripts/build.js new file mode 100755 index 0000000000..9286c5f936 --- /dev/null +++ b/detox/scripts/build.js @@ -0,0 +1,43 @@ +const childProcess = require('child_process'); +const fs = require('fs-extra'); + +// Just make the usage a little prettier +function sh(cmdline, opts) { + const args = cmdline.split(' '); + const cmd = args.shift(); + return childProcess.execFileSync(cmd, args, opts); +} + +if (process.platform === 'darwin') { + console.log("\nPackaging Detox iOS sources"); + + fs.removeSync('Detox-ios-src.tbz'); + // Prepare Earl Grey without building + sh("ios/EarlGrey/Scripts/setup-earlgrey.sh"); + sh("find ./ios -name Build -type d -exec rm -rf {} ;"); + + sh("tar -cjf ../Detox-ios-src.tbz .", { cwd: "ios" }); +} + +if (process.argv[2] === "android" || process.argv[3] === "android") { + console.log("\nBuilding Detox aars"); + const aars = [ + "detox-minReactNative44-debug.aar", + "detox-minReactNative46-debug.aar", + "detox-minReactNative44-release.aar", + "detox-minReactNative46-release.aar" + ]; + aars.forEach(aar => { + fs.removeSync(aar); + }); + + sh("./gradlew assembleDebug assembleRelease", { + cwd: "android", + stdio: "inherit", + shell: true + }); + + aars.forEach(aar => { + fs.copySync(`android/detox/build/outputs/aar/${aar}`, aar); + }); +} diff --git a/detox/scripts/build.sh b/detox/scripts/build.sh deleted file mode 100755 index 15fd5952d2..0000000000 --- a/detox/scripts/build.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -e - -if [ `uname` == "Darwin" ]; then - echo -e "\nPackaging Detox iOS sources" - rm -fr Detox-ios-src.tbz - #Prepare Earl Grey without building - ios/EarlGrey/Scripts/setup-earlgrey.sh > /dev/null - find ./ios -name Build -type d -exec rm -rf {} \; > /dev/null - - cd ios - tar -cjf ../Detox-ios-src.tbz . - cd .. -fi - -if [ "$1" == "android" -o "$2" == "android" ] ; then - echo -e "\nBuilding Detox aars" - rm -fr detox-oldOkhttp-debug.aar - rm -fr detox-newOkhttp-debug.aar - rm -fr detox-oldOkhttp-release.aar - rm -fr detox-newOkhttp-release.aar - cd android - ./gradlew assembleDebug - ./gradlew assembleRelease - cd .. - cp -fr android/detox/build/outputs/aar/detox-oldOkhttp-debug.aar . - cp -fr android/detox/build/outputs/aar/detox-newOkhttp-debug.aar . - cp -fr android/detox/build/outputs/aar/detox-oldOkhttp-release.aar . - cp -fr android/detox/build/outputs/aar/detox-newOkhttp-release.aar . -fi diff --git a/detox/scripts/postinstall.js b/detox/scripts/postinstall.js new file mode 100755 index 0000000000..cee9bce305 --- /dev/null +++ b/detox/scripts/postinstall.js @@ -0,0 +1,5 @@ +if (process.platform === "darwin") { + require("child_process").execSync(`${__dirname}/build_framework.ios.sh`, { + stdio: "inherit" + }); +} diff --git a/detox/scripts/postinstall.sh b/detox/scripts/postinstall.sh deleted file mode 100755 index f05bfc8668..0000000000 --- a/detox/scripts/postinstall.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -e - -if [ `uname` == "Darwin" ]; then - source "$(dirname ${0})/build_framework.ios.sh" -fi diff --git a/detox/src/configurations.mock.js b/detox/src/configurations.mock.js index 39cdb9b151..38bf97f0c6 100644 --- a/detox/src/configurations.mock.js +++ b/detox/src/configurations.mock.js @@ -85,7 +85,7 @@ const pathsTests = { }, "configurations": { "absolutePath": { - "binaryPath": "/tmp/abcdef/123", + "binaryPath": process.platform === "win32" ? "C:\\Temp\\abcdef\\123" : "/tmp/abcdef/123", "type": "ios.simulator", "name": "iPhone 7 Plus, iOS 10.2" }, diff --git a/detox/src/devices/Device.test.js b/detox/src/devices/Device.test.js index e6db81cb44..2a54351862 100644 --- a/detox/src/devices/Device.test.js +++ b/detox/src/devices/Device.test.js @@ -1,6 +1,7 @@ -const _ = require('lodash'); const configurationsMock = require('../configurations.mock'); +const path = require('path'); + const validScheme = configurationsMock.validOneDeviceAndSession; const invalidDeviceNoBinary = configurationsMock.invalidDeviceNoBinary; const invalidDeviceNoDeviceName = configurationsMock.invalidDeviceNoDeviceName; @@ -547,11 +548,11 @@ describe('Device', () => { it(`should accept absolute path for binary`, async () => { const actualPath = await launchAndTestBinaryPath('absolutePath'); - expect(actualPath).toEqual('/tmp/abcdef/123'); + expect(actualPath).toEqual(process.platform === 'win32' ? 'C:\\Temp\\abcdef\\123' : '/tmp/abcdef/123'); }); it(`should accept relative path for binary`, async () => { const actualPath = await launchAndTestBinaryPath('relativePath'); - expect(actualPath).toEqual(`${process.cwd()}/abcdef/123`); + expect(actualPath).toEqual(path.join(process.cwd(), 'abcdef/123')); }); }); diff --git a/detox/src/devices/EmulatorDriver.js b/detox/src/devices/EmulatorDriver.js index 313b177806..e5a4e7b4a9 100644 --- a/detox/src/devices/EmulatorDriver.js +++ b/detox/src/devices/EmulatorDriver.js @@ -52,10 +52,18 @@ class EmulatorDriver extends AndroidDriver { } await this._fixEmulatorConfigIniSkinName(name); - await this.emulator.boot(name); - const adbDevices = await this.adb.devices(); - const filteredDevices = _.filter(adbDevices, {type: 'emulator', name: name}); + let adbDevices = await this.adb.devices(); + let filteredDevices = _.filter(adbDevices, {type: 'emulator', name: name}); + + // If it's not already running, start it now. + if (!filteredDevices.length) { + await this.emulator.boot(name); + + // Refresh devices after boot completes. + adbDevices = await this.adb.devices(); + filteredDevices = _.filter(adbDevices, {type: 'emulator', name: name}); + } let adbName; switch (filteredDevices.length) { diff --git a/detox/src/devices/android/APKPath.test.js b/detox/src/devices/android/APKPath.test.js index 6dff221d61..41446ffb18 100644 --- a/detox/src/devices/android/APKPath.test.js +++ b/detox/src/devices/android/APKPath.test.js @@ -1,3 +1,7 @@ +// Using path methods will normalize slashes to backslashes on win32, so tests must match. +const path = require('path'); +const rootPath = process.platform === 'win32' ? 'C:\\Users\\SomeUser' : '~/somePath'; + describe('APKPath', () => { let APKPath; @@ -6,20 +10,20 @@ describe('APKPath', () => { }); it(`simple path`, async () => { - const inputApkPath = '~/somePath/build/outputs/apk/debug/app-debug.apk'; - const expectedTestApkPath = '~/somePath/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk'; + const inputApkPath = path.join(rootPath, 'build/outputs/apk/debug/app-debug.apk'); + const expectedTestApkPath = path.join(rootPath, 'build/outputs/apk/androidTest/debug/app-debug-androidTest.apk'); expect(APKPath.getTestApkPath(inputApkPath)).toEqual(expectedTestApkPath); }); it(`path for a gradle build flavor`, async () => { - const inputApkPath = '~/somePath/build/outputs/apk/development/debug/app-development-debug.apk'; - const expectedTestApkPath = '~/somePath/build/outputs/apk/androidTest/development/debug/app-development-debug-androidTest.apk'; + const inputApkPath = path.join(rootPath, 'build/outputs/apk/development/debug/app-development-debug.apk'); + const expectedTestApkPath = path.join(rootPath, 'build/outputs/apk/androidTest/development/debug/app-development-debug-androidTest.apk'); expect(APKPath.getTestApkPath(inputApkPath)).toEqual(expectedTestApkPath); }); it(`path for a gradle build with multiple flavors`, async () => { - const inputApkPath = '~/android/app/build/outputs/apk/pocPlayStore/debug/app-poc-playStore-debug.apk'; - const expectedTestApkPath = '~/android/app/build/outputs/apk/androidTest/pocPlayStore/debug/app-poc-playStore-debug-androidTest.apk'; + const inputApkPath = path.join(rootPath, 'build/outputs/apk/pocPlayStore/debug/app-poc-playStore-debug.apk'); + const expectedTestApkPath = path.join(rootPath, 'build/outputs/apk/androidTest/pocPlayStore/debug/app-poc-playStore-debug-androidTest.apk'); expect(APKPath.getTestApkPath(inputApkPath)).toEqual(expectedTestApkPath); }); diff --git a/detox/src/utils/exec.js b/detox/src/utils/exec.js index 741fd34394..cd497f6157 100644 --- a/detox/src/utils/exec.js +++ b/detox/src/utils/exec.js @@ -47,6 +47,16 @@ async function execWithRetriesAndLogs(bin, options, statusLogs, retries = 10, in // log.error(`${_operationCounter}: stderr:`, result.stderr); //} + /* istanbul ignore next */ + if (process.platform === 'win32') { + if (result.stdout) { + result.stdout = result.stdout.replace(/\r\n/g, '\n'); + } + if (result.stderr) { + result.stderr = result.stderr.replace(/\r\n/g, '\n'); + } + } + return result; } diff --git a/docs/More.Roadmap.md b/docs/More.Roadmap.md index 9926fe1aba..4996938a8c 100755 --- a/docs/More.Roadmap.md +++ b/docs/More.Roadmap.md @@ -11,6 +11,13 @@ The current API supports addition of multiple platforms, Android is next. This i ### iOS physical device support Currently Detox only supports running on iOS simulators, we plan on adding support for running on devices as well. +### Windows support +There is some work done for running Detox on Windows, but it's still fairly untested. Please open issues for anything you run into, but be aware of these limitations: + +- Apple doesn't support iOS apps on Windows, so you're limited to the in-progress Android support. +- `binaryPath` can be left as a relative path with `/`, or use `\\` if you don't need cross-platform support. +- `build` should not use `./gradlew ...`, but simply `gradlew ...` - you may prefer scripting the build outside of Detox if you want to maintain cross-platform support - or simply have two configurations! + ### Expectations on device logs One of our most wanted features, being able to assert log outputs.