From b87df4e2ce188c75d15985b42b19362fe1f8f79a Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 20 Mar 2019 08:25:26 +0900 Subject: [PATCH] fix xctestrun file detection when useXctestrunFile is true (#903) * fix xctestrun path * tweak handking sdk version * tuen a bit * update caps in readme * add a test to raise an error * move control flow into string * fix reviews * add a tips for building module --- README.md | 2 +- lib/driver.js | 4 +- lib/wda/utils.js | 92 ++++++++++++++++++++---------- lib/wda/webdriveragent.js | 2 + lib/wda/xcodebuild.js | 4 +- test/unit/wda/utils-specs.js | 107 +++++++++++++++++++++++++++++++++++ 6 files changed, 179 insertions(+), 32 deletions(-) create mode 100644 test/unit/wda/utils-specs.js diff --git a/README.md b/README.md index 49e5a7986..b7d007c64 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ Differences noted here |`calendarAccessAuthorized`|Set this to `true` if you want to enable calendar access on IOS Simulator with given bundleId. Set to `false`, if you want to disable calendar access on IOS Simulator with given bundleId. If not set, the calendar authorization status will not be set.|e.g., `true`| |`isHeadless`|Set this capability to `true` if automated tests are running on Simulator and the device display is not needed to be visible. This only has an effect since Xcode9 and only for simulators. All running instances of Simulator UI are going to be automatically terminated if headless test is started. `false` is the default value.|e.g., `true`| |`webkitDebugProxyPort`|Local port number used for communication with ios-webkit-debug-proxy. Only relevant for real devices. The default value equals to `27753`.|e.g. `20000`| -|`useXctestrunFile`|Use Xctestrun file to launch WDA. It will search for such file in `bootstrapPath`. Expected name of file is `WebDriverAgentRunner_iphoneos-arm64.xctestrun` for real device and `WebDriverAgentRunner_iphonesimulator-x86_64.xctestrun` for simulator. One can do `build-for-testing` for `WebDriverAgent` project for simulator and real device and then you will see [Product Folder like this](docs/useXctestrunFile.png) and you need to copy content of this folder at `bootstrapPath` location. Since, this capability expects that you have already built `WDA` project, it neither check whether you have necessary dependencies to build `WDA` nor it try to build project. Defaults to `false`|e.g., `true`| +|`useXctestrunFile`|Use Xctestrun file to launch WDA. It will search for such file in `bootstrapPath`. Expected name of file is `WebDriverAgentRunner_iphoneos-arm64.xctestrun` for real device and `WebDriverAgentRunner_iphonesimulator-x86_64.xctestrun` for simulator. One can do `build-for-testing` for `WebDriverAgent` project for simulator and real device and then you will see [Product Folder like this](docs/useXctestrunFile.png) and you need to copy content of this folder at `bootstrapPath` location. Since this capability expects that you have already built `WDA` project, it neither checks whether you have necessary dependencies to build `WDA` nor will it try to build project. Defaults to `false`. _Tips: `Xcodebuild` builds for the target platform version. We'd recommend you to build with minimal OS version which you'd like to run as the original WDA module. e.g. If you build WDA for 12.2, the module cannot run on iOS 11.4 because of loading some module error on simulator. A module built with 11.4 can work on iOS 12.2. (This is xcodebuild's expected behaviour.)_ |e.g., `true`| |`absoluteWebLocations`|This capability will direct the `Get Element Location` command, when used within webviews, to return coordinates which are relative to the origin of the page, rather than relative to the current scroll offset. This capability has no effect outside of webviews. Default `false`.|e.g., `true`| |`simulatorWindowCenter`|Allows to explicitly set the coordinates of Simulator window center for Xcode9+ SDK. This capability only has an effect if Simulator window has not been opened yet for the current session before it started.|e.g. `{-100.0,100.0}` or `{500,500}`, spaces are not allowed| |`useJSONSource`|Get JSON source from WDA and parse into XML on Appium server. This can be much faster, especially on large devices. Defaults to `false`.|e.g., `true`| diff --git a/lib/driver.js b/lib/driver.js index e97948d72..1a5a4b6ce 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -266,6 +266,7 @@ class XCUITestDriver extends BaseDriver { this.opts.device = device; this.opts.udid = udid; this.opts.realDevice = realDevice; + this.opts.iosSdkVersion = null; // For WDA and xcodebuild if (_.isEmpty(this.xcodeVersion) && (!this.opts.webDriverAgentUrl || !this.opts.realDevice)) { // no `webDriverAgentUrl`, or on a simulator, so we need an Xcode version @@ -274,7 +275,8 @@ class XCUITestDriver extends BaseDriver { log.info(`Xcode version set to '${this.xcodeVersion.versionString}' ${tools}`); this.iosSdkVersion = await getAndCheckIosSdkVersion(); - log.info(`iOS SDK Version set to '${this.iosSdkVersion}'`); + this.opts.iosSdkVersion = this.iosSdkVersion; // Pass to xcodebuild + log.info(`iOS SDK Version set to '${this.opts.iosSdkVersion}'`); } this.logEvent('xcodeDetailsRetrieved'); diff --git a/lib/wda/utils.js b/lib/wda/utils.js index a7b1d0742..c66bf6a73 100644 --- a/lib/wda/utils.js +++ b/lib/wda/utils.js @@ -192,40 +192,34 @@ CODE_SIGN_IDENTITY = ${signingId} return xcconfigPath; } +/** + * Information of the device under test + * @typedef {Object} DeviceInfo + * @property {string} isRealDevice - Equals to true if the current device is a real device + * @property {string} udid - The device UDID. + * @property {string} platformVersion - The platform version of OS. +*/ /** * Creates xctestrun file per device & platform version. - * We expects to have WebDriverAgentRunner_iphoneos${platformVersion}-arm64.xctestrun for real device - * and WebDriverAgentRunner_iphonesimulator${platformVersion}-x86_64.xctestrun for simulator located @bootstrapPath + * We expects to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device + * and WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-x86_64.xctestrun for simulator located @bootstrapPath + * Newer Xcode (Xcode 10.0 at least) generate xctestrun file following sdkVersion. + * e.g. Xcode which has iOS SDK Version 12.2 generate WebDriverAgentRunner_iphonesimulator.2-x86_64.xctestrun + * even if the cap has platform version 11.4 * - * @param {boolean} isRealDevice - Equals to true if the current device is a real device - * @param {string} udid - The device UDID. - * @param {string} platformVersion - The platform version of OS. + * @param {DeviceInfo} + * @param {string} sdkVersion - The Xcode SDK version of OS. * @param {string} bootstrapPath - The folder path containing xctestrun file. * @param {string} wdaRemotePort - The remote port WDA is listening on. * @return {string} returns xctestrunFilePath for given device - * @throws if WebDriverAgentRunner_iphoneos${platformVersion}-arm64.xctestrun for real device - * or WebDriverAgentRunner_iphonesimulator${platformVersion}-x86_64.xctestrun for simulator is not found @bootstrapPath, + * @throws if WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device + * or WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-x86_64.xctestrun for simulator is not found @bootstrapPath, * then it will throw file not found exception */ -async function setXctestrunFile (isRealDevice, udid, platformVersion, bootstrapPath, wdaRemotePort) { - let xctestrunDeviceFileName = `${udid}_${platformVersion}.xctestrun`; - let xctestrunFilePath = path.resolve(bootstrapPath, xctestrunDeviceFileName); - - if (!await fs.exists(xctestrunFilePath)) { - let xctestBaseFileName = isRealDevice ? `WebDriverAgentRunner_iphoneos${platformVersion}-arm64.xctestrun` : - `WebDriverAgentRunner_iphonesimulator${platformVersion}-x86_64.xctestrun`; - let originalXctestrunFile = path.resolve(bootstrapPath, xctestBaseFileName); - if (!await fs.exists(originalXctestrunFile)) { - log.errorAndThrow(`if you are using useXctestrunFile capability then you need to have ${originalXctestrunFile} file`); - } - // If this is first time run for given device, then first generate xctestrun file for device. - // We need to have a xctestrun file per device because we cant not have same wda port for all devices. - await fs.copyFile(originalXctestrunFile, xctestrunFilePath); - } - - let xctestRunContent = await plist.parsePlistFile(xctestrunFilePath); - - let updateWDAPort = { +async function setXctestrunFile (deviceInfo, sdkVersion, bootstrapPath, wdaRemotePort) { + const xctestrunFilePath = await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath); + const xctestRunContent = await plist.parsePlistFile(xctestrunFilePath); + const updateWDAPort = { WebDriverAgentRunner: { EnvironmentVariables: { USE_PORT: wdaRemotePort @@ -233,12 +227,52 @@ async function setXctestrunFile (isRealDevice, udid, platformVersion, bootstrapP } }; - let newXctestRunContent = _.merge(xctestRunContent, updateWDAPort); + const newXctestRunContent = _.merge(xctestRunContent, updateWDAPort); await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true); return xctestrunFilePath; } +/** + * Return the path of xctestrun if it exists + * @param {DeviceInfo} + * @param {string} sdkVersion - The Xcode SDK version of OS. + * @param {string} bootstrapPath - The folder path containing xctestrun file. + */ +async function getXctestrunFilePath (deviceInfo, sdkVersion, bootstrapPath) { + // First try the SDK path, for Xcode 10 (at least) + const sdkBased = [ + path.resolve(bootstrapPath, `${deviceInfo.udid}_${sdkVersion}.xctestrun`), + sdkVersion, + ]; + // Next try Platform path, for earlier Xcode versions + const platformBased = [ + path.resolve(bootstrapPath, `${deviceInfo.udid}_${deviceInfo.platformVersion}.xctestrun`), + deviceInfo.platformVersion, + ]; + + for (const [filePath, version] of [sdkBased, platformBased]) { + if (await fs.exists(filePath)) { + return filePath; + } + const originalXctestrunFile = path.resolve(bootstrapPath, getXctestrunFilePathName(deviceInfo.isRealDevice, version)); + if (await fs.exists(originalXctestrunFile)) { + // If this is first time run for given device, then first generate xctestrun file for device. + // We need to have a xctestrun file **per device** because we cant not have same wda port for all devices. + await fs.copyFile(originalXctestrunFile, filePath); + return filePath; + } + } + + log.errorAndThrow(`If you are using 'useXctestrunFile' capability then you ` + + `need to have a xctestrun file (expected: ` + + `'${path.resolve(bootstrapPath, getXctestrunFilePathName(deviceInfo.isRealDevice, sdkVersion))}')`); +} + +function getXctestrunFilePathName (isRealDevice, version) { + return `WebDriverAgentRunner_iphone${isRealDevice ? `os${version}-arm64` : `simulator${version}-x86_64`}.xctestrun`; +} + async function killProcess (name, proc) { if (proc && proc.proc) { log.info(`Shutting down ${name} process (pid ${proc.proc.pid})`); @@ -290,5 +324,5 @@ async function getWDAUpgradeTimestamp (bootstrapPath) { export { updateProjectFile, resetProjectFile, checkForDependencies, setRealDeviceSecurity, fixForXcode7, fixForXcode9, - generateXcodeConfigFile, setXctestrunFile, killProcess, randomInt, WDA_RUNNER_BUNDLE_ID, - getWDAUpgradeTimestamp }; + generateXcodeConfigFile, setXctestrunFile, getXctestrunFilePath, + killProcess, randomInt, WDA_RUNNER_BUNDLE_ID, getWDAUpgradeTimestamp }; diff --git a/lib/wda/webdriveragent.js b/lib/wda/webdriveragent.js index 6bf1e6e53..eae9e9bf5 100644 --- a/lib/wda/webdriveragent.js +++ b/lib/wda/webdriveragent.js @@ -27,6 +27,7 @@ class WebDriverAgent { this.device = args.device; this.platformVersion = args.platformVersion; + this.iosSdkVersion = args.iosSdkVersion; this.host = args.host; this.realDevice = !!args.realDevice; @@ -48,6 +49,7 @@ class WebDriverAgent { this.xcodebuild = new XcodeBuild(this.xcodeVersion, this.device, { platformVersion: this.platformVersion, + iosSdkVersion: this.iosSdkVersion, agentPath: this.agentPath, bootstrapPath: this.bootstrapPath, realDevice: this.realDevice, diff --git a/lib/wda/xcodebuild.js b/lib/wda/xcodebuild.js index 0002fb91b..a000aedaf 100644 --- a/lib/wda/xcodebuild.js +++ b/lib/wda/xcodebuild.js @@ -31,6 +31,7 @@ class XcodeBuild { this.bootstrapPath = args.bootstrapPath; this.platformVersion = args.platformVersion; + this.iosSdkVersion = args.iosSdkVersion; this.showXcodeLog = !!args.showXcodeLog; @@ -63,7 +64,8 @@ class XcodeBuild { if (this.xcodeVersion.major <= 7) { log.errorAndThrow('useXctestrunFile can only be used with xcode version 8 onwards'); } - this.xctestrunFilePath = await setXctestrunFile(this.realDevice, this.device.udid, this.platformVersion, this.bootstrapPath, this.wdaRemotePort); + const deviveInfo = {isRealDevice: this.realDevice, udid: this.device.udid, platformVersion: this.platformVersion}; + this.xctestrunFilePath = await setXctestrunFile(deviveInfo, this.iosSdkVersion, this.bootstrapPath, this.wdaRemotePort); return; } diff --git a/test/unit/wda/utils-specs.js b/test/unit/wda/utils-specs.js new file mode 100644 index 000000000..def8a9d97 --- /dev/null +++ b/test/unit/wda/utils-specs.js @@ -0,0 +1,107 @@ +import { getXctestrunFilePath } from '../../../lib/wda/utils'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { withMocks } from 'appium-test-support'; +import { fs } from 'appium-support'; +import path from 'path'; +import { fail } from 'assert'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('utils', function () { + describe('#getXctestrunFilePath', withMocks({fs}, function (mocks) { + const platformVersion = '12.0'; + const sdkVersion = '12.2'; + const udid = 'xxxxxyyyyyyzzzzzz'; + const bootstrapPath = 'path/to/data'; + + afterEach(function () { + mocks.verify(); + }); + + it('should return sdk based path with udid', async function () { + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) + .returns(true); + mocks.fs.expects('copyFile') + .never(); + const deviceInfo = {isRealDevice: true, udid, platformVersion}; + await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath) + .should.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)); + }); + + it('should return sdk based path without udid, copy them', async function () { + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) + .returns(false); + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphoneos${sdkVersion}-arm64.xctestrun`)) + .returns(true); + mocks.fs.expects('copyFile') + .withExactArgs( + path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphoneos${sdkVersion}-arm64.xctestrun`), + path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`) + ) + .returns(true); + const deviceInfo = {isRealDevice: true, udid, platformVersion}; + await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath) + .should.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)); + }); + + it('should return platform based path', async function () { + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) + .returns(false); + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-x86_64.xctestrun`)) + .returns(false); + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)) + .returns(true); + mocks.fs.expects('copyFile') + .never(); + const deviceInfo = {isRealDevice: false, udid, platformVersion}; + await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath) + .should.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)); + }); + + it('should return platform based path without udid, copy them', async function () { + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) + .returns(false); + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-x86_64.xctestrun`)) + .returns(false); + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)) + .returns(false); + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${platformVersion}-x86_64.xctestrun`)) + .returns(true); + mocks.fs.expects('copyFile') + .withExactArgs( + path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${platformVersion}-x86_64.xctestrun`), + path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`) + ) + .returns(true); + + const deviceInfo = {isRealDevice: false, udid, platformVersion}; + await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath) + .should.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)); + }); + + it('should raise an exception because of no files', async function () { + const expected = path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-x86_64.xctestrun`); + mocks.fs.expects('exists').exactly(4).returns(false); + + const deviceInfo = {isRealDevice: false, udid, platformVersion}; + try { + await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath); + fail(); + } catch (err) { + err.message.should.equal(`If you are using 'useXctestrunFile' capability then you need to have a xctestrun file (expected: '${expected}')`); + } + }); + })); +});