@xenon-device-management/xenon 1.17.3 → 1.17.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/package.json +1 -1
- package/lib/src/device-managers/ios/IOSStreamService.js +13 -2
- package/lib/src/device-managers/ios/iosStreamDiagnostics.js +30 -0
- package/lib/src/services/SessionLifecycleService.js +30 -66
- package/lib/test/unit/SessionLifecycleCaps.spec.js +9 -12
- package/lib/test/unit/iosStreamDiagnostics.spec.js +49 -0
- package/lib/test/unit/wda-cap-injection.spec.js +53 -66
- package/package.json +1 -1
package/lib/package.json
CHANGED
|
@@ -692,6 +692,12 @@ let IOSStreamService = class IOSStreamService {
|
|
|
692
692
|
if (!fs_extra_1.default.existsSync(logDir))
|
|
693
693
|
fs_extra_1.default.mkdirSync(logDir);
|
|
694
694
|
const wdaRunLog = path_1.default.join(logDir, `runwda-${udid}.log`);
|
|
695
|
+
// Truncated per run, because this file is also the evidence the failure
|
|
696
|
+
// is classified from. Appending across runs meant a "Did not find test
|
|
697
|
+
// app" line from a day earlier was still matched today, so every later
|
|
698
|
+
// failure of any kind — including one where WDA was demonstrably
|
|
699
|
+
// installed — was reported as "WebDriverAgent is not installed".
|
|
700
|
+
fs_extra_1.default.writeFileSync(wdaRunLog, '');
|
|
695
701
|
(_a = session.wdaProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (d) => fs_extra_1.default.appendFileSync(wdaRunLog, d));
|
|
696
702
|
(_b = session.wdaProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (d) => fs_extra_1.default.appendFileSync(wdaRunLog, d));
|
|
697
703
|
// 4. Wait for WDA to be ready
|
|
@@ -770,11 +776,16 @@ let IOSStreamService = class IOSStreamService {
|
|
|
770
776
|
}
|
|
771
777
|
if (((_c = session.wdaProcess) === null || _c === void 0 ? void 0 : _c.exitCode) !== null) {
|
|
772
778
|
const logContent = fs_extra_1.default.existsSync(wdaRunLog) ? fs_extra_1.default.readFileSync(wdaRunLog, 'utf8') : '';
|
|
773
|
-
//
|
|
774
|
-
//
|
|
779
|
+
// Both of these are permanent until a human acts, and they call for
|
|
780
|
+
// opposite actions — install the app, versus trust the app that is
|
|
781
|
+
// already installed. Surface which, rather than a raw exit code and
|
|
782
|
+
// a log tail.
|
|
775
783
|
if ((0, iosStreamDiagnostics_1.isMissingWdaError)(logContent)) {
|
|
776
784
|
throw new Error((0, iosStreamDiagnostics_1.missingWdaMessage)(udid));
|
|
777
785
|
}
|
|
786
|
+
if ((0, iosStreamDiagnostics_1.isWdaLaunchFailure)(logContent)) {
|
|
787
|
+
throw new Error((0, iosStreamDiagnostics_1.wdaLaunchFailureMessage)(udid));
|
|
788
|
+
}
|
|
778
789
|
throw new Error(`WDA process exited with code ${(_d = session.wdaProcess) === null || _d === void 0 ? void 0 : _d.exitCode}. Log: ${logContent.slice(-200)}`);
|
|
779
790
|
}
|
|
780
791
|
yield new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.isMissingWdaError = isMissingWdaError;
|
|
11
11
|
exports.missingWdaMessage = missingWdaMessage;
|
|
12
|
+
exports.isWdaLaunchFailure = isWdaLaunchFailure;
|
|
13
|
+
exports.wdaLaunchFailureMessage = wdaLaunchFailureMessage;
|
|
12
14
|
exports.classifyTunnelStderr = classifyTunnelStderr;
|
|
13
15
|
exports.isOwnStreamProcess = isOwnStreamProcess;
|
|
14
16
|
/**
|
|
@@ -27,6 +29,34 @@ function missingWdaMessage(udid) {
|
|
|
27
29
|
return (`WebDriverAgent is not installed on ${udid}. ` +
|
|
28
30
|
`Live streaming requires the WebDriverAgentRunner app — install WDA on this device to enable the mirror.`);
|
|
29
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* True when WDA is installed but the runner could not be *started*.
|
|
34
|
+
*
|
|
35
|
+
* go-ios reports the two conditions quite differently, and conflating them
|
|
36
|
+
* sends people to reinstall an app that is already there:
|
|
37
|
+
*
|
|
38
|
+
* missing: cannot get test app information: Did not find test app for '…'
|
|
39
|
+
* present: cannot start test runner: LaunchAppWithStdIo: failed to launch
|
|
40
|
+
* app: launchApp: failed to get PID
|
|
41
|
+
*
|
|
42
|
+
* The second is nearly always the device refusing to run the binary — an
|
|
43
|
+
* untrusted developer certificate, a locked screen, or a lapsed signature —
|
|
44
|
+
* none of which reinstalling addresses on its own.
|
|
45
|
+
*/
|
|
46
|
+
function isWdaLaunchFailure(logContent) {
|
|
47
|
+
if (!logContent)
|
|
48
|
+
return false;
|
|
49
|
+
return (/failed to launch app/i.test(logContent) ||
|
|
50
|
+
/LaunchAppWithStdIo/i.test(logContent) ||
|
|
51
|
+
/cannot start test runner/i.test(logContent));
|
|
52
|
+
}
|
|
53
|
+
/** Human-facing, actionable message for the installed-but-unlaunchable case. */
|
|
54
|
+
function wdaLaunchFailureMessage(udid) {
|
|
55
|
+
return (`WebDriverAgent is installed on ${udid} but would not start. ` +
|
|
56
|
+
`This is usually the device refusing to run it: trust the developer certificate under ` +
|
|
57
|
+
`Settings → General → VPN & Device Management, unlock the screen, and check the signing ` +
|
|
58
|
+
`profile has not expired. Launching WebDriverAgentRunner once by hand confirms it.`);
|
|
59
|
+
}
|
|
30
60
|
/**
|
|
31
61
|
* Classify a chunk of go-ios `tunnel start` stderr.
|
|
32
62
|
*
|
|
@@ -51,8 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
51
51
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
52
|
};
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
exports.SessionLifecycleService =
|
|
55
|
-
exports.stripXctrunnerSuffix = stripXctrunnerSuffix;
|
|
54
|
+
exports.SessionLifecycleService = void 0;
|
|
56
55
|
const typedi_1 = require("typedi");
|
|
57
56
|
const uuid_1 = require("uuid");
|
|
58
57
|
const lodash_1 = __importDefault(require("lodash"));
|
|
@@ -89,26 +88,6 @@ const commandsQueueGuard = new async_lock_1.default();
|
|
|
89
88
|
// Serializes concurrent deleteSession cleanup for the same sessionId so
|
|
90
89
|
// that onSessionStopped events don't fire twice if two clients race.
|
|
91
90
|
const sessionCleanupLock = new async_lock_1.default();
|
|
92
|
-
/** The suffix appium-webdriveragent appends to `updatedWDABundleId`. */
|
|
93
|
-
exports.XCTRUNNER_SUFFIX = '.xctrunner';
|
|
94
|
-
/**
|
|
95
|
-
* Normalise an installed WebDriverAgent bundle id for `appium:updatedWDABundleId`.
|
|
96
|
-
*
|
|
97
|
-
* The driver derives its keep-list entry as
|
|
98
|
-
* `${updatedWDABundleId ?? WDA_RUNNER_BUNDLE_ID}${'.xctrunner'}`, so the
|
|
99
|
-
* capability wants the id *without* the runner suffix — it puts it back. Handing
|
|
100
|
-
* it the id exactly as installed yields `…xctrunner.xctrunner`, which matches
|
|
101
|
-
* nothing, and the app the keep-list was meant to protect is uninstalled.
|
|
102
|
-
*
|
|
103
|
-
* Only a trailing occurrence is removed: a bundle id may legitimately contain
|
|
104
|
-
* the word elsewhere, and `String.replace` without an anchor would cut the
|
|
105
|
-
* first one it found.
|
|
106
|
-
*/
|
|
107
|
-
function stripXctrunnerSuffix(bundleId) {
|
|
108
|
-
return bundleId.endsWith(exports.XCTRUNNER_SUFFIX)
|
|
109
|
-
? bundleId.slice(0, -exports.XCTRUNNER_SUFFIX.length)
|
|
110
|
-
: bundleId;
|
|
111
|
-
}
|
|
112
91
|
let SessionLifecycleService = class SessionLifecycleService {
|
|
113
92
|
constructor() {
|
|
114
93
|
this.logger = logger_1.default.scope('SessionLifecycleService');
|
|
@@ -340,8 +319,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
340
319
|
const wdaUrl = `http://127.0.0.1:${streamInfo.wdaPort}`;
|
|
341
320
|
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, 'WDA active, finalizing session...');
|
|
342
321
|
yield new Promise((resolve) => setTimeout(resolve, 2000));
|
|
343
|
-
|
|
344
|
-
this.injectWDAUrl(caps, wdaUrl, bundleId);
|
|
322
|
+
this.injectWDAUrl(caps, wdaUrl);
|
|
345
323
|
}
|
|
346
324
|
catch (err) {
|
|
347
325
|
this.logger.warn(`⚠️ Artisan WDA: Pre-session boot failed: ${err.message}`);
|
|
@@ -350,12 +328,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
350
328
|
}
|
|
351
329
|
else if (isWdaActive) {
|
|
352
330
|
const wdaUrl = `http://127.0.0.1:${streamStatus.wdaPort}`;
|
|
353
|
-
|
|
354
|
-
// is the branch that runs in the ordinary case — a preview already
|
|
355
|
-
// open when a test starts. Omitting it told Appium to manage "the
|
|
356
|
-
// preinstalled WDA" without saying which, and it removed ours.
|
|
357
|
-
const bundleId = yield streamService.detectWDABundleId(device.udid);
|
|
358
|
-
this.injectWDAUrl(caps, wdaUrl, bundleId);
|
|
331
|
+
this.injectWDAUrl(caps, wdaUrl);
|
|
359
332
|
}
|
|
360
333
|
const hasWdaUrl = lodash_1.default.has(caps.alwaysMatch, 'appium:webDriverAgentUrl') ||
|
|
361
334
|
lodash_1.default.has((_a = caps.firstMatch) === null || _a === void 0 ? void 0 : _a[0], 'appium:webDriverAgentUrl');
|
|
@@ -368,7 +341,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
368
341
|
}
|
|
369
342
|
});
|
|
370
343
|
}
|
|
371
|
-
injectWDAUrl(caps, wdaUrl
|
|
344
|
+
injectWDAUrl(caps, wdaUrl) {
|
|
372
345
|
var _a, _b, _c;
|
|
373
346
|
// W3C capability rules forbid a property from appearing in BOTH alwaysMatch
|
|
374
347
|
// and a firstMatch entry; appium rejects the session with "property
|
|
@@ -380,44 +353,35 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
380
353
|
if (!target)
|
|
381
354
|
return;
|
|
382
355
|
const other = target === caps.alwaysMatch ? (_c = caps.firstMatch) === null || _c === void 0 ? void 0 : _c[0] : caps.alwaysMatch;
|
|
356
|
+
// The URL, and only the URL. Xenon owns this WebDriverAgent: it launched
|
|
357
|
+
// it through go-ios and keeps the XCTest session alive for the stream. The
|
|
358
|
+
// driver is a guest here, and `webDriverAgentUrl` is precisely how you say
|
|
359
|
+
// that — `selectWdaStartupStrategyName` returns 'existing-url' on it before
|
|
360
|
+
// considering anything else, and that strategy's own quit() reads
|
|
361
|
+
// "Stopping neither xcodebuild nor XCTest session since WDA lifecycle is
|
|
362
|
+
// not managed by this driver".
|
|
383
363
|
target['appium:webDriverAgentUrl'] = wdaUrl;
|
|
384
|
-
// `usePreinstalledWDA` and
|
|
385
|
-
// all. On its own, the first one sends the driver down
|
|
386
|
-
// `preparePreinstalled`, which calls `cleanupApps(driver,
|
|
387
|
-
// [driver.wda.bundleIdForXctest])` — that removes every installed app
|
|
388
|
-
// whose CFBundleName is WebDriverAgentRunner except the one bundle id it
|
|
389
|
-
// was told to keep. Without `updatedWDABundleId` that keep-list is the
|
|
390
|
-
// driver's own default, `com.facebook.WebDriverAgentRunner.xctrunner`, so
|
|
391
|
-
// the WDA Xenon just launched (`com.qasecret.…` here) is not on it and is
|
|
392
|
-
// uninstalled from the device mid-session. Observed twice on a real
|
|
393
|
-
// iPhone: "Removing WebDriverAgent runner app
|
|
394
|
-
// 'com.qasecret.WebDriverAgentRunner.xctrunner'", after which every iOS
|
|
395
|
-
// feature fails with "WebDriverAgent is not installed" until someone
|
|
396
|
-
// re-signs and reinstalls it by hand.
|
|
364
|
+
// `usePreinstalledWDA` is removed, never added, and this is the whole fix.
|
|
397
365
|
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
// move is to say less, not more.
|
|
366
|
+
// It does not change which strategy runs — the URL already won that — it
|
|
367
|
+
// only makes `launchOnce` call `preparePreinstalled` first, which is:
|
|
401
368
|
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
delete target['appium:usePreinstalledWDA'];
|
|
419
|
-
delete target['appium:updatedWDABundleId'];
|
|
420
|
-
}
|
|
369
|
+
// await driver.mobileKillApp(driver.wda.bundleIdForXctest);
|
|
370
|
+
// await cleanupApps(driver, [driver.wda.bundleIdForXctest]);
|
|
371
|
+
//
|
|
372
|
+
// Kill the app, then uninstall every WebDriverAgentRunner not on a
|
|
373
|
+
// one-entry keep-list. Against a WDA that Xenon is hosting that is
|
|
374
|
+
// exactly wrong twice over, and both halves were observed on a real
|
|
375
|
+
// iPhone: the uninstall as "Removing WebDriverAgent runner app
|
|
376
|
+
// 'com.qasecret.WebDriverAgentRunner.xctrunner'", leaving the device with
|
|
377
|
+
// no WDA until someone re-signs one by hand; and the kill as the driver's
|
|
378
|
+
// own next request failing with ECONNRESET, because go-ios's runwda was
|
|
379
|
+
// gone while iproxy still held the port.
|
|
380
|
+
//
|
|
381
|
+
// Getting the keep-list right only converts the uninstall into the kill.
|
|
382
|
+
// The driver must not be invited to manage this app at all.
|
|
383
|
+
delete target['appium:usePreinstalledWDA'];
|
|
384
|
+
delete target['appium:updatedWDABundleId'];
|
|
421
385
|
// These conflict with a pre-installed WDA URL — strip from both buckets.
|
|
422
386
|
for (const bucket of [target, other]) {
|
|
423
387
|
if (!bucket)
|
|
@@ -19,34 +19,31 @@ describe('SessionLifecycleService.injectWDAUrl — W3C capability composition',
|
|
|
19
19
|
alwaysMatch: { 'appium:automationName': 'XCUITest' },
|
|
20
20
|
firstMatch: [{ 'appium:udid': 'abc' }],
|
|
21
21
|
};
|
|
22
|
-
injectWDAUrl(caps, URL
|
|
22
|
+
injectWDAUrl(caps, URL);
|
|
23
23
|
// Present in alwaysMatch...
|
|
24
24
|
expect(caps.alwaysMatch['appium:webDriverAgentUrl']).to.equal(URL);
|
|
25
|
-
expect(caps.alwaysMatch['appium:usePreinstalledWDA']).to.equal(true);
|
|
26
|
-
expect(caps.alwaysMatch['appium:updatedWDABundleId']).to.equal('com.qasecret.WebDriverAgentRunner');
|
|
27
25
|
// ...and NOT in firstMatch — the bug was duplicating into both, which appium
|
|
28
26
|
// rejects with "property 'webDriverAgentUrl' should not exist on both
|
|
29
27
|
// primary and secondary object".
|
|
30
28
|
expect(caps.firstMatch[0]).to.not.have.property('appium:webDriverAgentUrl');
|
|
31
|
-
expect(caps.firstMatch[0]).to.not.have.property('appium:usePreinstalledWDA');
|
|
32
|
-
expect(caps.firstMatch[0]).to.not.have.property('appium:updatedWDABundleId');
|
|
33
29
|
});
|
|
34
30
|
it('falls back to firstMatch[0] when there is no alwaysMatch', () => {
|
|
35
31
|
const caps = { firstMatch: [{ 'appium:udid': 'abc' }] };
|
|
36
|
-
injectWDAUrl(caps, URL
|
|
32
|
+
injectWDAUrl(caps, URL);
|
|
37
33
|
expect(caps.firstMatch[0]['appium:webDriverAgentUrl']).to.equal(URL);
|
|
38
|
-
expect(caps.firstMatch[0]['appium:usePreinstalledWDA']).to.equal(true);
|
|
39
34
|
});
|
|
40
|
-
it('
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// `
|
|
44
|
-
//
|
|
35
|
+
it('never invites the driver to manage the WDA, in either bucket', () => {
|
|
36
|
+
// Two earlier versions of this case asserted the opposite, and each
|
|
37
|
+
// pinned a behaviour that damaged a real device: `usePreinstalledWDA`
|
|
38
|
+
// makes `launchOnce` run `preparePreinstalled`, which kills the runner
|
|
39
|
+
// and uninstalls every sibling not on a one-entry keep-list. Xenon hosts
|
|
40
|
+
// this WDA itself, so the driver must never be invited to manage it. See
|
|
45
41
|
// test/unit/wda-cap-injection.spec.ts for the full chain.
|
|
46
42
|
const caps = { firstMatch: [{ 'appium:udid': 'abc' }] };
|
|
47
43
|
injectWDAUrl(caps, URL);
|
|
48
44
|
expect(caps.firstMatch[0]['appium:webDriverAgentUrl']).to.equal(URL);
|
|
49
45
|
expect(caps.firstMatch[0]).to.not.have.property('appium:usePreinstalledWDA');
|
|
46
|
+
expect(caps.firstMatch[0]).to.not.have.property('appium:updatedWDABundleId');
|
|
50
47
|
});
|
|
51
48
|
it('scrubs conflicting WDA caps from both buckets', () => {
|
|
52
49
|
const caps = {
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const chai_1 = require("chai");
|
|
4
4
|
const iosStreamDiagnostics_1 = require("../../src/device-managers/ios/iosStreamDiagnostics");
|
|
5
|
+
/**
|
|
6
|
+
* Both verbatim from a real go-ios run log, and the whole point is that they
|
|
7
|
+
* mean opposite things: reinstall the app, versus trust the app that is
|
|
8
|
+
* already installed.
|
|
9
|
+
*/
|
|
10
|
+
const GO_IOS_MISSING = `{"level":"ERROR","msg":"Failed running WDA","error":"runXUITestWithBundleIdsXcode15Ctx: ` +
|
|
11
|
+
`cannot get test app information: Did not find test app for ` +
|
|
12
|
+
`'com.qasecret.WebDriverAgentRunner.xctrunner' on device. Is it installed?"}`;
|
|
13
|
+
const GO_IOS_WONT_LAUNCH = `{"level":"ERROR","msg":"Failed running WDA","error":"runXUITestWithBundleIdsXcode15Ctx: ` +
|
|
14
|
+
`cannot start test runner: LaunchAppWithStdIo: failed to launch app: launchApp: ` +
|
|
15
|
+
`failed to get PID: pidFromResponse: could not get pid"}`;
|
|
5
16
|
describe('iosStreamDiagnostics', () => {
|
|
6
17
|
describe('isMissingWdaError', () => {
|
|
7
18
|
it('detects the go-ios "Did not find test app" WDA crash', () => {
|
|
@@ -25,6 +36,44 @@ describe('iosStreamDiagnostics', () => {
|
|
|
25
36
|
(0, chai_1.expect)(msg.toLowerCase()).to.contain('install');
|
|
26
37
|
});
|
|
27
38
|
});
|
|
39
|
+
describe('telling "not installed" apart from "would not start"', () => {
|
|
40
|
+
it('reads the real missing-app log as missing', () => {
|
|
41
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isMissingWdaError)(GO_IOS_MISSING)).to.equal(true);
|
|
42
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isWdaLaunchFailure)(GO_IOS_MISSING)).to.equal(false);
|
|
43
|
+
});
|
|
44
|
+
it('reads the real launch-failure log as a launch failure, not as missing', () => {
|
|
45
|
+
// This is the one that was reported as "WebDriverAgent is not installed"
|
|
46
|
+
// against a device where it demonstrably was installed, sending the
|
|
47
|
+
// reader off to reinstall an app that was already there.
|
|
48
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isWdaLaunchFailure)(GO_IOS_WONT_LAUNCH)).to.equal(true);
|
|
49
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isMissingWdaError)(GO_IOS_WONT_LAUNCH)).to.equal(false);
|
|
50
|
+
});
|
|
51
|
+
it('gives the two cases opposite advice', () => {
|
|
52
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.missingWdaMessage)('udid-1')).to.contain('not installed');
|
|
53
|
+
const launch = (0, iosStreamDiagnostics_1.wdaLaunchFailureMessage)('udid-1');
|
|
54
|
+
(0, chai_1.expect)(launch).to.contain('is installed');
|
|
55
|
+
(0, chai_1.expect)(launch).to.contain('VPN & Device Management');
|
|
56
|
+
(0, chai_1.expect)(launch).to.not.contain('not installed');
|
|
57
|
+
});
|
|
58
|
+
it('is safe on empty input', () => {
|
|
59
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isWdaLaunchFailure)('')).to.equal(false);
|
|
60
|
+
});
|
|
61
|
+
it('is why the run log must be truncated per run', () => {
|
|
62
|
+
// These classifiers read a file, and the file used to be appended to
|
|
63
|
+
// across runs. A stale "Did not find test app" from a previous day still
|
|
64
|
+
// wins over today's launch failure — which is exactly how an installed
|
|
65
|
+
// WDA came to be reported as missing. IOSStreamService now empties the
|
|
66
|
+
// log before each spawn; this pins what happens if it stops.
|
|
67
|
+
const stale = `${GO_IOS_MISSING}\n${GO_IOS_WONT_LAUNCH}`;
|
|
68
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isMissingWdaError)(stale)).to.equal(true);
|
|
69
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isWdaLaunchFailure)(stale)).to.equal(true);
|
|
70
|
+
});
|
|
71
|
+
it('does not flag an unrelated crash as either', () => {
|
|
72
|
+
const crash = 'WDA crashed: signal 11 (SIGSEGV)';
|
|
73
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isMissingWdaError)(crash)).to.equal(false);
|
|
74
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isWdaLaunchFailure)(crash)).to.equal(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
28
77
|
describe('classifyTunnelStderr', () => {
|
|
29
78
|
it('extracts the real target udid from the go-ios JSON payload', () => {
|
|
30
79
|
const line = JSON.stringify({
|
|
@@ -4,103 +4,90 @@ require("reflect-metadata");
|
|
|
4
4
|
const chai_1 = require("chai");
|
|
5
5
|
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `cleanupApps(driver, [driver.wda.bundleIdForXctest])` — removing every
|
|
10
|
-
* installed app whose CFBundleName is WebDriverAgentRunner except the single
|
|
11
|
-
* bundle id on that keep-list.
|
|
7
|
+
* Xenon owns this WebDriverAgent. It launches it through go-ios and keeps the
|
|
8
|
+
* XCTest session alive for the live stream; the Appium driver is a guest.
|
|
12
9
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* `appium:webDriverAgentUrl` is exactly how that is expressed —
|
|
11
|
+
* `selectWdaStartupStrategyName` returns 'existing-url' on it before
|
|
12
|
+
* considering anything else, and that strategy's `quit()` reads "Stopping
|
|
13
|
+
* neither xcodebuild nor XCTest session since WDA lifecycle is not managed by
|
|
14
|
+
* this driver".
|
|
18
15
|
*
|
|
19
|
-
*
|
|
16
|
+
* `appium:usePreinstalledWDA` invites the opposite. It does not change the
|
|
17
|
+
* strategy — the URL already won — it only makes `launchOnce` run
|
|
18
|
+
* `preparePreinstalled` first:
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* await driver.mobileKillApp(driver.wda.bundleIdForXctest);
|
|
21
|
+
* await cleanupApps(driver, [driver.wda.bundleIdForXctest]);
|
|
23
22
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* Both halves were observed against a real iPhone: the uninstall as "Removing
|
|
24
|
+
* WebDriverAgent runner app 'com.qasecret.WebDriverAgentRunner.xctrunner'",
|
|
25
|
+
* leaving the device with no WDA at all, and the kill as the driver's own next
|
|
26
|
+
* request failing with ECONNRESET because go-ios's runwda was gone while
|
|
27
|
+
* iproxy still held the port.
|
|
26
28
|
*/
|
|
27
29
|
describe('WDA capability injection', () => {
|
|
28
30
|
// The method is private by design; this is the one place its contract is
|
|
29
31
|
// asserted, so it is reached deliberately rather than widening the API.
|
|
30
|
-
const inject = (caps, url
|
|
32
|
+
const inject = (caps, url) => SessionLifecycleService_1.SessionLifecycleService.prototype.injectWDAUrl.call(Object.create(SessionLifecycleService_1.SessionLifecycleService.prototype), caps, url);
|
|
31
33
|
const WDA_URL = 'http://127.0.0.1:8100';
|
|
32
|
-
|
|
33
|
-
it('names the bundle whenever it claims a preinstalled WDA', () => {
|
|
34
|
+
it('reuses the running WDA by URL', () => {
|
|
34
35
|
const caps = { alwaysMatch: {}, firstMatch: [{}] };
|
|
35
|
-
inject(caps, WDA_URL
|
|
36
|
-
(0, chai_1.expect)(caps.alwaysMatch['appium:usePreinstalledWDA']).to.equal(true);
|
|
37
|
-
(0, chai_1.expect)(caps.alwaysMatch['appium:updatedWDABundleId']).to.equal('com.qasecret.WebDriverAgentRunner');
|
|
38
|
-
});
|
|
39
|
-
it('sends the id the driver will rebuild into the installed one', () => {
|
|
40
|
-
// This is the whole contract, and getting it wrong is not a near miss:
|
|
41
|
-
// the driver's keep-list is `updatedWDABundleId + '.xctrunner'`, so an
|
|
42
|
-
// id that is off by that suffix protects a bundle that does not exist
|
|
43
|
-
// and the real runner is uninstalled from the device.
|
|
44
|
-
const caps = { alwaysMatch: {}, firstMatch: [{}] };
|
|
45
|
-
inject(caps, WDA_URL, BUNDLE);
|
|
46
|
-
const keepListEntry = caps.alwaysMatch['appium:updatedWDABundleId'] + SessionLifecycleService_1.XCTRUNNER_SUFFIX;
|
|
47
|
-
(0, chai_1.expect)(keepListEntry).to.equal(BUNDLE);
|
|
48
|
-
});
|
|
49
|
-
it('leaves an id that already lacks the suffix alone', () => {
|
|
50
|
-
const caps = { alwaysMatch: {}, firstMatch: [{}] };
|
|
51
|
-
inject(caps, WDA_URL, 'com.qasecret.WebDriverAgentRunner');
|
|
52
|
-
(0, chai_1.expect)(caps.alwaysMatch['appium:updatedWDABundleId'] + SessionLifecycleService_1.XCTRUNNER_SUFFIX).to.equal(BUNDLE);
|
|
53
|
-
});
|
|
54
|
-
it('never claims a preinstalled WDA it cannot name', () => {
|
|
55
|
-
// The destructive combination. `webDriverAgentUrl` alone still reuses the
|
|
56
|
-
// running WDA and does not enter the cleanup path at all.
|
|
57
|
-
const caps = { alwaysMatch: {}, firstMatch: [{}] };
|
|
58
|
-
inject(caps, WDA_URL, null);
|
|
36
|
+
inject(caps, WDA_URL);
|
|
59
37
|
(0, chai_1.expect)(caps.alwaysMatch['appium:webDriverAgentUrl']).to.equal(WDA_URL);
|
|
60
|
-
(0, chai_1.expect)(caps.alwaysMatch['appium:usePreinstalledWDA']).to.equal(undefined);
|
|
61
|
-
(0, chai_1.expect)(caps.alwaysMatch['appium:updatedWDABundleId']).to.equal(undefined);
|
|
62
38
|
});
|
|
63
|
-
it('never
|
|
39
|
+
it('never invites the driver to manage the app', () => {
|
|
64
40
|
const caps = { alwaysMatch: {}, firstMatch: [{}] };
|
|
65
41
|
inject(caps, WDA_URL);
|
|
66
|
-
(0, chai_1.expect)(caps.alwaysMatch
|
|
42
|
+
(0, chai_1.expect)(caps.alwaysMatch).to.not.have.property('appium:usePreinstalledWDA');
|
|
43
|
+
(0, chai_1.expect)(caps.alwaysMatch).to.not.have.property('appium:updatedWDABundleId');
|
|
67
44
|
});
|
|
68
|
-
it('
|
|
69
|
-
//
|
|
70
|
-
//
|
|
45
|
+
it('strips the invitation even when the caller supplied it', () => {
|
|
46
|
+
// Honouring it would kill the WDA Xenon is hosting. On this path the
|
|
47
|
+
// capability is not the caller's to give.
|
|
71
48
|
const caps = {
|
|
72
|
-
alwaysMatch: { 'appium:usePreinstalledWDA': true, 'appium:updatedWDABundleId': '
|
|
49
|
+
alwaysMatch: { 'appium:usePreinstalledWDA': true, 'appium:updatedWDABundleId': 'com.x.WDA' },
|
|
73
50
|
firstMatch: [{}],
|
|
74
51
|
};
|
|
75
|
-
inject(caps, WDA_URL
|
|
76
|
-
(0, chai_1.expect)(caps.alwaysMatch
|
|
77
|
-
(0, chai_1.expect)(caps.alwaysMatch
|
|
52
|
+
inject(caps, WDA_URL);
|
|
53
|
+
(0, chai_1.expect)(caps.alwaysMatch).to.not.have.property('appium:usePreinstalledWDA');
|
|
54
|
+
(0, chai_1.expect)(caps.alwaysMatch).to.not.have.property('appium:updatedWDABundleId');
|
|
55
|
+
});
|
|
56
|
+
it('strips it from the other bucket too', () => {
|
|
57
|
+
const caps = {
|
|
58
|
+
alwaysMatch: {},
|
|
59
|
+
firstMatch: [{ 'appium:usePreinstalledWDA': true, 'appium:updatedWDABundleId': 'com.x.WDA' }],
|
|
60
|
+
};
|
|
61
|
+
inject(caps, WDA_URL);
|
|
62
|
+
(0, chai_1.expect)(caps.firstMatch[0]).to.not.have.property('appium:usePreinstalledWDA');
|
|
63
|
+
(0, chai_1.expect)(caps.firstMatch[0]).to.not.have.property('appium:updatedWDABundleId');
|
|
78
64
|
});
|
|
79
|
-
it('keeps the injected
|
|
65
|
+
it('keeps the injected url out of the other capability bucket', () => {
|
|
80
66
|
// W3C forbids a property in both alwaysMatch and a firstMatch entry;
|
|
81
67
|
// appium rejects the whole session if it happens.
|
|
82
68
|
const caps = {
|
|
83
69
|
alwaysMatch: {},
|
|
84
|
-
firstMatch: [{ 'appium:webDriverAgentUrl': 'http://stale'
|
|
70
|
+
firstMatch: [{ 'appium:webDriverAgentUrl': 'http://stale' }],
|
|
85
71
|
};
|
|
86
|
-
inject(caps, WDA_URL
|
|
87
|
-
(0, chai_1.expect)(caps.firstMatch[0]
|
|
88
|
-
(0, chai_1.expect)(caps.firstMatch[0]['appium:usePreinstalledWDA']).to.equal(undefined);
|
|
72
|
+
inject(caps, WDA_URL);
|
|
73
|
+
(0, chai_1.expect)(caps.firstMatch[0]).to.not.have.property('appium:webDriverAgentUrl');
|
|
89
74
|
(0, chai_1.expect)(caps.alwaysMatch['appium:webDriverAgentUrl']).to.equal(WDA_URL);
|
|
90
75
|
});
|
|
91
|
-
it('
|
|
92
|
-
|
|
93
|
-
(
|
|
76
|
+
it('falls back to firstMatch[0] when there is no alwaysMatch', () => {
|
|
77
|
+
const caps = { firstMatch: [{ 'appium:udid': 'abc' }] };
|
|
78
|
+
inject(caps, WDA_URL);
|
|
79
|
+
(0, chai_1.expect)(caps.firstMatch[0]['appium:webDriverAgentUrl']).to.equal(WDA_URL);
|
|
80
|
+
(0, chai_1.expect)(caps.firstMatch[0]).to.not.have.property('appium:usePreinstalledWDA');
|
|
94
81
|
});
|
|
95
82
|
it('strips the caps that conflict with reusing a WDA by URL', () => {
|
|
96
83
|
const caps = {
|
|
97
84
|
alwaysMatch: { 'appium:wdaLocalPort': 8100, 'appium:derivedDataPath': '/tmp/dd' },
|
|
98
85
|
firstMatch: [{ 'appium:mjpegServerPort': 9100, 'appium:usePrebuiltWDA': true }],
|
|
99
86
|
};
|
|
100
|
-
inject(caps, WDA_URL
|
|
101
|
-
(0, chai_1.expect)(caps.alwaysMatch
|
|
102
|
-
(0, chai_1.expect)(caps.alwaysMatch
|
|
103
|
-
(0, chai_1.expect)(caps.firstMatch[0]
|
|
104
|
-
(0, chai_1.expect)(caps.firstMatch[0]
|
|
87
|
+
inject(caps, WDA_URL);
|
|
88
|
+
(0, chai_1.expect)(caps.alwaysMatch).to.not.have.property('appium:wdaLocalPort');
|
|
89
|
+
(0, chai_1.expect)(caps.alwaysMatch).to.not.have.property('appium:derivedDataPath');
|
|
90
|
+
(0, chai_1.expect)(caps.firstMatch[0]).to.not.have.property('appium:mjpegServerPort');
|
|
91
|
+
(0, chai_1.expect)(caps.firstMatch[0]).to.not.have.property('appium:usePrebuiltWDA');
|
|
105
92
|
});
|
|
106
93
|
});
|
package/package.json
CHANGED