@xenon-device-management/xenon 2.15.0 → 2.17.0
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/README.md +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/index-Dk5J6HB1.js +219 -0
- package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +23 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +25 -8
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +12 -4
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +29 -33
- package/lib/src/scripts/run-migrations.js +320 -25
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +52 -23
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +23 -4
- package/lib/src/sessions/SessionManager.js +33 -9
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +440 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +264 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +43 -96
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +16 -1
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const supertest_1 = __importDefault(require("supertest"));
|
|
18
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
19
|
+
const deviceLogBook_1 = require("../../src/services/logcat/deviceLogBook");
|
|
20
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
21
|
+
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
22
|
+
// The driver's own log class (test/helpers/driver-log.ts): an iPhone's and a
|
|
23
|
+
// simulator's `logs.syslog` extend it, so what Xenon reads is what the driver keeps.
|
|
24
|
+
const driver_log_1 = require("../helpers/driver-log");
|
|
25
|
+
// How the Device logs tab reads an iPhone line's level (web log-derive.ts).
|
|
26
|
+
const DASHBOARD_SYSLOG = /<(Error|Fault|Warning)>/;
|
|
27
|
+
class TestDeviceLogs extends SessionDeviceLogs_1.SessionDeviceLogs {
|
|
28
|
+
context() {
|
|
29
|
+
return { pluginArgs: { bindHostOrIp: '127.0.0.1' }, port: 4723, nodeId: 'this-server' };
|
|
30
|
+
}
|
|
31
|
+
flushIntervalMs() {
|
|
32
|
+
return 60000;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const IPHONE = {
|
|
36
|
+
udid: '00008110-000A',
|
|
37
|
+
platform: 'ios',
|
|
38
|
+
realDevice: true,
|
|
39
|
+
host: 'http://127.0.0.1:4723',
|
|
40
|
+
nodeId: 'this-server',
|
|
41
|
+
};
|
|
42
|
+
const SIMULATOR = Object.assign(Object.assign({}, IPHONE), { udid: 'SIM-1', realDevice: false });
|
|
43
|
+
const syslog = (n, level = 'Notice') => `Oct 5 20:31:${String(n % 60).padStart(2, '0')} iPhone Shop[4127] <${level}>: line ${n}`;
|
|
44
|
+
function deviceLogs(sessionId) {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
const res = yield (0, supertest_1.default)((0, selector_health_fixture_1.selectorHealthApp)(selector_health_fixture_1.ADMIN)).get(`/session/${sessionId}/logs/device`);
|
|
47
|
+
(0, chai_1.expect)(res.status, res.text).to.equal(200);
|
|
48
|
+
return res.body;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
describe("An iPhone or simulator session's Device logs", function () {
|
|
52
|
+
this.timeout(60000);
|
|
53
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
54
|
+
let logs;
|
|
55
|
+
let sessionId;
|
|
56
|
+
let driverLog;
|
|
57
|
+
let count = 0;
|
|
58
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
59
|
+
count += 1;
|
|
60
|
+
sessionId = `dl-ios-${count}`;
|
|
61
|
+
yield scratch.db.session.create({
|
|
62
|
+
data: {
|
|
63
|
+
id: sessionId,
|
|
64
|
+
device_udid: IPHONE.udid,
|
|
65
|
+
device_platform: 'ios',
|
|
66
|
+
device_version: '26.0',
|
|
67
|
+
desired_capabilities: '{}',
|
|
68
|
+
session_capabilities: '{}',
|
|
69
|
+
node_id: 'this-server',
|
|
70
|
+
has_live_video: false,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
logs = new TestDeviceLogs();
|
|
74
|
+
driverLog = new driver_log_1.TestDriverLog();
|
|
75
|
+
}));
|
|
76
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
77
|
+
yield logs.stop(sessionId);
|
|
78
|
+
}));
|
|
79
|
+
it('keeps the lines from the create and from the run, each once, in order', () => __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
// Logged during the create, before Xenon could listen: in the driver's buffer.
|
|
81
|
+
for (let i = 0; i < 3; i++)
|
|
82
|
+
driverLog.line(syslog(i));
|
|
83
|
+
yield logs.start({ sessionId, device: IPHONE, since: Date.now() - 60000, driverLog });
|
|
84
|
+
for (let i = 3; i < 250; i++)
|
|
85
|
+
driverLog.line(syslog(i));
|
|
86
|
+
yield logs.stop(sessionId);
|
|
87
|
+
(0, chai_1.expect)(driverLog.listenerCount('output')).to.equal(0);
|
|
88
|
+
driverLog.line(syslog(999)); // after the session: not its line
|
|
89
|
+
const rows = yield deviceLogs(sessionId);
|
|
90
|
+
(0, chai_1.expect)(rows.map((r) => r.message)).to.deep.equal(Array.from({ length: 250 }, (_, i) => syslog(i)));
|
|
91
|
+
}));
|
|
92
|
+
it("leaves the driver's lines to the test that asks for them", () => __awaiter(this, void 0, void 0, function* () {
|
|
93
|
+
driverLog.line(syslog(1));
|
|
94
|
+
yield logs.start({ sessionId, device: IPHONE, since: Date.now() - 60000, driverLog });
|
|
95
|
+
driverLog.line(syslog(2));
|
|
96
|
+
yield logs.stop(sessionId);
|
|
97
|
+
const forTheTest = yield driverLog.getLogs();
|
|
98
|
+
(0, chai_1.expect)(forTheTest.map((e) => e.message)).to.deep.equal([
|
|
99
|
+
syslog(1),
|
|
100
|
+
syslog(2),
|
|
101
|
+
]);
|
|
102
|
+
}));
|
|
103
|
+
it("keeps each line's text, so Errors only finds the phone's errors, at the time it arrived", () => __awaiter(this, void 0, void 0, function* () {
|
|
104
|
+
yield logs.start({ sessionId, device: IPHONE, since: Date.now() - 60000, driverLog });
|
|
105
|
+
const before = Date.now();
|
|
106
|
+
driverLog.line(syslog(1));
|
|
107
|
+
driverLog.line(syslog(2, 'Error'));
|
|
108
|
+
driverLog.line(syslog(3, 'Fault'));
|
|
109
|
+
const after = Date.now();
|
|
110
|
+
yield logs.stop(sessionId);
|
|
111
|
+
const rows = yield deviceLogs(sessionId);
|
|
112
|
+
(0, chai_1.expect)(rows.map((r) => { var _a, _b; return (_b = (_a = DASHBOARD_SYSLOG.exec(r.message)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : null; })).to.deep.equal([
|
|
113
|
+
null,
|
|
114
|
+
'Error',
|
|
115
|
+
'Fault',
|
|
116
|
+
]);
|
|
117
|
+
for (const r of rows) {
|
|
118
|
+
const t = new Date(r.timestamp).getTime();
|
|
119
|
+
(0, chai_1.expect)(t).to.be.within(before, after);
|
|
120
|
+
}
|
|
121
|
+
}));
|
|
122
|
+
it("records a simulator's compact lines, without `log stream`'s banner", () => __awaiter(this, void 0, void 0, function* () {
|
|
123
|
+
driverLog.line('Filtering the log data using "subsystem != \\"com.apple.CoreTelephony\\""');
|
|
124
|
+
driverLog.line('Timestamp Ty Process[PID:TID]');
|
|
125
|
+
yield logs.start({ sessionId, device: SIMULATOR, since: Date.now() - 60000, driverLog });
|
|
126
|
+
driverLog.line('2026-10-05 20:31:40.123 F Shop[4127:8812] Terminating app');
|
|
127
|
+
driverLog.line('\tThread 0 Crashed:');
|
|
128
|
+
yield logs.stop(sessionId);
|
|
129
|
+
(0, chai_1.expect)((yield deviceLogs(sessionId)).map((r) => r.message)).to.deep.equal([
|
|
130
|
+
'2026-10-05 20:31:40.123 F Shop[4127:8812] Terminating app',
|
|
131
|
+
'\tThread 0 Crashed:',
|
|
132
|
+
]);
|
|
133
|
+
}));
|
|
134
|
+
it("keeps the app's own lines, the errors inside it, the app manager's lines and faults, and says so first", () => __awaiter(this, void 0, void 0, function* () {
|
|
135
|
+
const line = (proc, text, level = 'Notice') => `Oct 5 23:48:33 Rabindras-iPhone ${proc} <${level}>: ${text}`;
|
|
136
|
+
// From the create, before Xenon listens: the app's first line comes
|
|
137
|
+
// before the phone announces its pid.
|
|
138
|
+
driverLog.line(line('Shop[4127]', 'first line'));
|
|
139
|
+
driverLog.line(line('runningboardd(RunningBoard)[34]', 'Acquiring [app<com.example.shop(UUID)>:4127]'));
|
|
140
|
+
yield logs.start({
|
|
141
|
+
sessionId,
|
|
142
|
+
device: IPHONE,
|
|
143
|
+
since: Date.now() - 60000,
|
|
144
|
+
driverLog,
|
|
145
|
+
appUnderTest: 'com.example.shop',
|
|
146
|
+
});
|
|
147
|
+
driverLog.line(line('duetexpertd[412]', 'chatter'));
|
|
148
|
+
driverLog.line(line('Shop(Networking)[4127]', 'request failed', 'Error'));
|
|
149
|
+
driverLog.line(line('wifid[61]', 'link lost', 'Fault'));
|
|
150
|
+
yield logs.stop(sessionId);
|
|
151
|
+
(0, chai_1.expect)((yield deviceLogs(sessionId)).map((r) => r.message)).to.deep.equal([
|
|
152
|
+
"Xenon: Kept here: what the app under test (com.example.shop) logs itself, every error inside it, what the phone says about its launch, state, crashes and end, and faults. The rest of the phone's log is left out.",
|
|
153
|
+
line('Shop[4127]', 'first line'),
|
|
154
|
+
line('runningboardd(RunningBoard)[34]', 'Acquiring [app<com.example.shop(UUID)>:4127]'),
|
|
155
|
+
line('Shop(Networking)[4127]', 'request failed', 'Error'),
|
|
156
|
+
line('wifid[61]', 'link lost', 'Fault'),
|
|
157
|
+
]);
|
|
158
|
+
}));
|
|
159
|
+
it('records nothing for a session that skips log capture', () => __awaiter(this, void 0, void 0, function* () {
|
|
160
|
+
yield logs.start({ sessionId, device: IPHONE, since: Date.now(), driverLog: undefined });
|
|
161
|
+
(0, chai_1.expect)(logs.isRecording(sessionId)).to.equal(false);
|
|
162
|
+
(0, chai_1.expect)(yield deviceLogs(sessionId)).to.deep.equal([]);
|
|
163
|
+
}));
|
|
164
|
+
it(`holds to the same limit as Android, ${deviceLogBook_1.DEVICE_LOG_LINE_LIMIT} lines then errors`, () => __awaiter(this, void 0, void 0, function* () {
|
|
165
|
+
yield logs.start({ sessionId, device: IPHONE, since: Date.now() - 60000, driverLog });
|
|
166
|
+
for (let i = 0; i < deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 100; i++) {
|
|
167
|
+
driverLog.line(syslog(i, i >= deviceLogBook_1.DEVICE_LOG_LINE_LIMIT && i % 2 === 0 ? 'Error' : 'Notice'));
|
|
168
|
+
}
|
|
169
|
+
yield logs.stop(sessionId);
|
|
170
|
+
const rows = yield deviceLogs(sessionId);
|
|
171
|
+
(0, chai_1.expect)(rows).to.have.length(deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 1 + 50 + 1);
|
|
172
|
+
(0, chai_1.expect)(rows[deviceLogBook_1.DEVICE_LOG_LINE_LIMIT].message).to.match(/^Xenon: This session reached 10,000/);
|
|
173
|
+
(0, chai_1.expect)(rows[rows.length - 1].message).to.match(/^Xenon: 50 device log lines were left out/);
|
|
174
|
+
}));
|
|
175
|
+
it("doesn't record a node's iPhone, whose driver is the node's", () => {
|
|
176
|
+
(0, chai_1.expect)(logs.appliesTo(IPHONE)).to.equal(true);
|
|
177
|
+
(0, chai_1.expect)(logs.appliesTo(SIMULATOR)).to.equal(true);
|
|
178
|
+
(0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, IPHONE), { nodeId: 'a-node' }))).to.equal(false);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const plugin_1 = require("../../src/plugin");
|
|
20
|
+
const NodeDevices_1 = __importDefault(require("../../src/device-managers/NodeDevices"));
|
|
21
|
+
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
22
|
+
const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
|
|
23
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
24
|
+
const event_manager_1 = require("../../src/dashboard/event-manager");
|
|
25
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
26
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
27
|
+
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
28
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
29
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
30
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
31
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
32
|
+
/**
|
|
33
|
+
* A session's device log stops however the session ends on this server, its
|
|
34
|
+
* dashboard on or off: on a node that ends the lines it holds for the hub.
|
|
35
|
+
*/
|
|
36
|
+
describe('a session’s device log stops however the session ends', function () {
|
|
37
|
+
this.timeout(30000);
|
|
38
|
+
(0, scratch_database_1.useScratchDatabase)();
|
|
39
|
+
let restore;
|
|
40
|
+
let stop;
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
restore = (0, container_registration_1.saveRegistrations)(SessionMetricsService_1.SessionMetricsService, SessionDeviceLogs_1.SessionDeviceLogs);
|
|
43
|
+
stop = sinon_1.default.stub().resolves();
|
|
44
|
+
typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
|
|
45
|
+
typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, { stop });
|
|
46
|
+
});
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
sinon_1.default.restore();
|
|
49
|
+
restore();
|
|
50
|
+
});
|
|
51
|
+
it('at shutdown', () => __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
|
|
53
|
+
yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).stopSessionForShutdown('s-down', 'shutdown');
|
|
54
|
+
(0, chai_1.expect)(stop.calledWith('s-down')).to.equal(true);
|
|
55
|
+
}));
|
|
56
|
+
it('when the driver shuts down unexpectedly on a node', () => __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
sinon_1.default.stub(NodeDevices_1.default.prototype, 'unblockDevice').resolves();
|
|
58
|
+
const plugin = { pluginArgs: { hub: 'http://hub:4723' } };
|
|
59
|
+
yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-crash', caps: {} }, null);
|
|
60
|
+
(0, chai_1.expect)(stop.calledWith('s-crash')).to.equal(true);
|
|
61
|
+
}));
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* After a hub restart, a node session's device log goes on being collected
|
|
65
|
+
* where the hub records sessions: after the newest line it stored.
|
|
66
|
+
*/
|
|
67
|
+
describe('a hub restart resumes collecting a node session’s device log', function () {
|
|
68
|
+
this.timeout(30000);
|
|
69
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
70
|
+
let restore;
|
|
71
|
+
let start;
|
|
72
|
+
let savedStore;
|
|
73
|
+
let savedArgs;
|
|
74
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
75
|
+
restore = (0, container_registration_1.saveRegistrations)(SessionMetricsService_1.SessionMetricsService, SessionDeviceLogs_1.SessionDeviceLogs);
|
|
76
|
+
start = sinon_1.default.stub().resolves();
|
|
77
|
+
typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { start: sinon_1.default.stub() });
|
|
78
|
+
typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, { start });
|
|
79
|
+
savedArgs = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs;
|
|
80
|
+
savedStore = device_store_1.DeviceStoreFactory._deviceStore;
|
|
81
|
+
device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
|
|
82
|
+
yield scratch.db.session.deleteMany({});
|
|
83
|
+
yield scratch.db.device.deleteMany({});
|
|
84
|
+
const phone = (udid, extra) => scratch.db.device.create({
|
|
85
|
+
data: Object.assign({ udid, host: 'http://127.0.0.1:1', nodeId: 'node-2', platform: 'android', name: udid, sdk: '10', busy: true, userBlocked: false, offline: false }, extra),
|
|
86
|
+
});
|
|
87
|
+
yield phone('node-phone', {});
|
|
88
|
+
yield phone('cloud-phone', {
|
|
89
|
+
cloud: JSON.stringify('browserstack'),
|
|
90
|
+
host: 'https://hub.example.com',
|
|
91
|
+
});
|
|
92
|
+
for (const [id, udid, desired] of [
|
|
93
|
+
['on-node', 'node-phone', '{}'],
|
|
94
|
+
['logs-off', 'node-phone', JSON.stringify({ 'xe:save_device_logs': false })],
|
|
95
|
+
['in-cloud', 'cloud-phone', '{}'],
|
|
96
|
+
]) {
|
|
97
|
+
yield scratch.db.session.create({
|
|
98
|
+
data: {
|
|
99
|
+
id,
|
|
100
|
+
device_udid: udid,
|
|
101
|
+
device_platform: 'android',
|
|
102
|
+
device_version: '10',
|
|
103
|
+
desired_capabilities: desired,
|
|
104
|
+
session_capabilities: '{}',
|
|
105
|
+
node_id: 'node-2',
|
|
106
|
+
has_live_video: false,
|
|
107
|
+
status: 'running',
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
|
|
114
|
+
SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
|
|
115
|
+
device_store_1.DeviceStoreFactory._deviceStore = savedStore;
|
|
116
|
+
typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs = savedArgs;
|
|
117
|
+
sinon_1.default.restore();
|
|
118
|
+
restore();
|
|
119
|
+
});
|
|
120
|
+
const recover = (enableDashboard) => __awaiter(this, void 0, void 0, function* () {
|
|
121
|
+
typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard });
|
|
122
|
+
yield new SessionManager_1.SessionManager().recoverActiveSessions('hub-1', '/wd/hub');
|
|
123
|
+
// The device log is resumed without waiting on it.
|
|
124
|
+
yield new Promise((r) => setImmediate(r));
|
|
125
|
+
});
|
|
126
|
+
it('resumes a node session’s, through the node, never a cloud provider’s nor one turned off', () => __awaiter(this, void 0, void 0, function* () {
|
|
127
|
+
yield recover(true);
|
|
128
|
+
(0, chai_1.expect)(start.getCalls().map((c) => c.args[0].sessionId)).to.deep.equal(['on-node']);
|
|
129
|
+
const args = start.firstCall.args[0];
|
|
130
|
+
(0, chai_1.expect)(args.resume).to.equal(true);
|
|
131
|
+
(0, chai_1.expect)(typeof args.source.nodeDeviceLogs).to.equal('function');
|
|
132
|
+
}));
|
|
133
|
+
it('resumes none on a hub with its dashboard off, which records no device logs', () => __awaiter(this, void 0, void 0, function* () {
|
|
134
|
+
yield recover(false);
|
|
135
|
+
(0, chai_1.expect)(start.called).to.equal(false);
|
|
136
|
+
}));
|
|
137
|
+
});
|
|
@@ -15,65 +15,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const supertest_1 = __importDefault(require("supertest"));
|
|
18
|
-
const stream_1 = require("stream");
|
|
19
|
-
const LogcatStreamService_1 = require("../../src/device-managers/android/LogcatStreamService");
|
|
20
|
-
const PackageResolver_1 = require("../../src/services/logcat/PackageResolver");
|
|
21
18
|
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
22
19
|
const deviceLogBook_1 = require("../../src/services/logcat/deviceLogBook");
|
|
23
20
|
const scratch_database_1 = require("../helpers/scratch-database");
|
|
24
21
|
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
25
|
-
const
|
|
22
|
+
const fake_logcat_1 = require("../helpers/fake-logcat");
|
|
26
23
|
// How the Device logs tab reads a line's level for its "Errors only" filter
|
|
27
24
|
// (web log-derive.ts `logLineLevel`, whose own tests hold it to lines in the
|
|
28
25
|
// shape these rows have; the web package can't be imported here).
|
|
29
26
|
const DASHBOARD_THREADTIME = /^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s+\d+\s+\d+\s+([VDIWEF])\s/;
|
|
30
27
|
const levelOf = (message) => { var _a, _b; return (_b = (_a = DASHBOARD_THREADTIME.exec(message)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : null; };
|
|
31
|
-
/** A threadtime line logged at `at` by this machine's local clock. */
|
|
32
|
-
function lineAt(at, msg, level = 'I', tag = 'Tag', pid = 4127) {
|
|
33
|
-
const t = new Date(at);
|
|
34
|
-
return (`${pad(t.getMonth() + 1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}:` +
|
|
35
|
-
`${pad(t.getSeconds())}.${pad(t.getMilliseconds(), 3)} ${pid} ${pid} ${level} ${tag}: ${msg}\n`);
|
|
36
|
-
}
|
|
37
|
-
/** An `adb logcat` child whose output the test writes. */
|
|
38
|
-
function fakeProc() {
|
|
39
|
-
const stdout = new stream_1.PassThrough();
|
|
40
|
-
const handlers = {};
|
|
41
|
-
return {
|
|
42
|
-
stdout,
|
|
43
|
-
killed: false,
|
|
44
|
-
on(ev, cb) {
|
|
45
|
-
(handlers[ev] || (handlers[ev] = [])).push(cb);
|
|
46
|
-
},
|
|
47
|
-
/** The process ends, as when the phone restarts or is unplugged. */
|
|
48
|
-
exit() {
|
|
49
|
-
stdout.end();
|
|
50
|
-
(handlers.close || []).forEach((h) => h());
|
|
51
|
-
},
|
|
52
|
-
kill() {
|
|
53
|
-
this.killed = true;
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
/** The real stream service, its `adb logcat` children fake. */
|
|
58
|
-
class FakeLogcatStreams extends LogcatStreamService_1.LogcatStreamService {
|
|
59
|
-
constructor() {
|
|
60
|
-
super(...arguments);
|
|
61
|
-
this.procs = [];
|
|
62
|
-
}
|
|
63
|
-
spawnLogcat() {
|
|
64
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
65
|
-
const proc = fakeProc();
|
|
66
|
-
this.procs.push(proc);
|
|
67
|
-
return proc;
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
makeResolver() {
|
|
71
|
-
return new PackageResolver_1.PackageResolver(() => __awaiter(this, void 0, void 0, function* () { return ' PID NAME\n 4127 com.example.app\n'; }));
|
|
72
|
-
}
|
|
73
|
-
get proc() {
|
|
74
|
-
return this.procs[this.procs.length - 1];
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
28
|
class TestDeviceLogs extends SessionDeviceLogs_1.SessionDeviceLogs {
|
|
78
29
|
constructor(logcat) {
|
|
79
30
|
super();
|
|
@@ -115,19 +66,6 @@ const PHONE = {
|
|
|
115
66
|
host: 'http://127.0.0.1:4723',
|
|
116
67
|
nodeId: 'this-server',
|
|
117
68
|
};
|
|
118
|
-
/** Lets readline, the stream's package lookups and the mux deliver what was written. */
|
|
119
|
-
const settle = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
120
|
-
for (let i = 0; i < 5; i++)
|
|
121
|
-
yield new Promise((r) => setImmediate(r));
|
|
122
|
-
});
|
|
123
|
-
const until = (check_1, ...args_1) => __awaiter(void 0, [check_1, ...args_1], void 0, function* (check, ms = 2000) {
|
|
124
|
-
const end = Date.now() + ms;
|
|
125
|
-
while (!check()) {
|
|
126
|
-
if (Date.now() > end)
|
|
127
|
-
throw new Error('timed out');
|
|
128
|
-
yield new Promise((r) => setTimeout(r, 5));
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
69
|
/** The session's Device logs as the session page gets them. */
|
|
132
70
|
function deviceLogs(sessionId) {
|
|
133
71
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -159,7 +97,7 @@ describe("An Android session's Device logs", function () {
|
|
|
159
97
|
has_live_video: false,
|
|
160
98
|
},
|
|
161
99
|
});
|
|
162
|
-
logcat = new FakeLogcatStreams();
|
|
100
|
+
logcat = new fake_logcat_1.FakeLogcatStreams();
|
|
163
101
|
logs = new TestDeviceLogs(logcat);
|
|
164
102
|
}));
|
|
165
103
|
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -171,14 +109,14 @@ describe("An Android session's Device logs", function () {
|
|
|
171
109
|
yield logs.start({ sessionId, device: PHONE, since });
|
|
172
110
|
// History from before the session, as `logcat -T` sends it first.
|
|
173
111
|
for (let i = 0; i < 20; i++)
|
|
174
|
-
logcat.proc.stdout.write(lineAt(since - 600000 + i, `old ${i}`));
|
|
112
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since - 600000 + i, `old ${i}`));
|
|
175
113
|
// 250 lines over the run, a batch between each "command".
|
|
176
114
|
for (let batch = 0; batch < 5; batch++) {
|
|
177
115
|
for (let i = 0; i < 50; i++) {
|
|
178
116
|
const n = batch * 50 + i;
|
|
179
|
-
logcat.proc.stdout.write(lineAt(since + 1000 + n, `line ${n}`));
|
|
117
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 1000 + n, `line ${n}`));
|
|
180
118
|
}
|
|
181
|
-
yield settle();
|
|
119
|
+
yield (0, fake_logcat_1.settle)();
|
|
182
120
|
}
|
|
183
121
|
yield logs.stop(sessionId);
|
|
184
122
|
const rows = yield deviceLogs(sessionId);
|
|
@@ -188,11 +126,11 @@ describe("An Android session's Device logs", function () {
|
|
|
188
126
|
const since = Date.now() - 60000;
|
|
189
127
|
yield logs.start({ sessionId, device: PHONE, since });
|
|
190
128
|
const crashAt = since + 30123;
|
|
191
|
-
logcat.proc.stdout.write(lineAt(crashAt - 5, 'Start proc com.example.app', 'I', 'ActivityManager'));
|
|
192
|
-
logcat.proc.stdout.write(lineAt(crashAt, 'FATAL EXCEPTION: main', 'E', 'AndroidRuntime'));
|
|
193
|
-
logcat.proc.stdout.write(lineAt(crashAt, 'Process: com.example.app, PID: 4127', 'E', 'AndroidRuntime'));
|
|
194
|
-
logcat.proc.stdout.write(lineAt(crashAt + 1, 'Sending signal. PID: 4127 SIG: 9', 'I', 'Process'));
|
|
195
|
-
yield settle();
|
|
129
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(crashAt - 5, 'Start proc com.example.app', 'I', 'ActivityManager'));
|
|
130
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(crashAt, 'FATAL EXCEPTION: main', 'E', 'AndroidRuntime'));
|
|
131
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(crashAt, 'Process: com.example.app, PID: 4127', 'E', 'AndroidRuntime'));
|
|
132
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(crashAt + 1, 'Sending signal. PID: 4127 SIG: 9', 'I', 'Process'));
|
|
133
|
+
yield (0, fake_logcat_1.settle)();
|
|
196
134
|
yield logs.stop(sessionId);
|
|
197
135
|
const rows = yield deviceLogs(sessionId);
|
|
198
136
|
(0, chai_1.expect)(rows.map((r) => levelOf(r.message))).to.deep.equal(['I', 'E', 'E', 'I']);
|
|
@@ -210,8 +148,8 @@ describe("An Android session's Device logs", function () {
|
|
|
210
148
|
const at = since + 10000;
|
|
211
149
|
const frames = Array.from({ length: 40 }, (_, i) => `\tat com.example.Frame${i}.run(Frame.java:${i})`);
|
|
212
150
|
for (const frame of frames)
|
|
213
|
-
logcat.proc.stdout.write(lineAt(at, frame, 'E', 'AndroidRuntime'));
|
|
214
|
-
yield settle();
|
|
151
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(at, frame, 'E', 'AndroidRuntime'));
|
|
152
|
+
yield (0, fake_logcat_1.settle)();
|
|
215
153
|
yield logs.stop(sessionId);
|
|
216
154
|
const rows = yield deviceLogs(sessionId);
|
|
217
155
|
(0, chai_1.expect)(rows.map((r) => r.message.replace(/^.*AndroidRuntime: /, ''))).to.deep.equal(frames);
|
|
@@ -224,14 +162,14 @@ describe("An Android session's Device logs", function () {
|
|
|
224
162
|
const since = Date.now() - 60000;
|
|
225
163
|
yield logs.start({ sessionId, device: PHONE, since });
|
|
226
164
|
for (let i = 0; i < SessionDeviceLogs_1.WRITE_BATCH + 10; i++)
|
|
227
|
-
logcat.proc.stdout.write(lineAt(since + i, `line ${i}`));
|
|
228
|
-
yield settle();
|
|
165
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + i, `line ${i}`));
|
|
166
|
+
yield (0, fake_logcat_1.settle)();
|
|
229
167
|
// A full batch is written while the session runs, without waiting for
|
|
230
168
|
// its end or the interval (a minute here).
|
|
231
169
|
const written = () => scratch.db.log.count({ where: { session_id: sessionId } });
|
|
232
170
|
const end = Date.now() + 2000;
|
|
233
171
|
while ((yield written()) < SessionDeviceLogs_1.WRITE_BATCH && Date.now() < end)
|
|
234
|
-
yield settle();
|
|
172
|
+
yield (0, fake_logcat_1.settle)();
|
|
235
173
|
(0, chai_1.expect)(yield written()).to.be.at.least(SessionDeviceLogs_1.WRITE_BATCH);
|
|
236
174
|
(0, chai_1.expect)(logs.isRecording(sessionId)).to.equal(true);
|
|
237
175
|
yield logs.stop(sessionId);
|
|
@@ -243,18 +181,18 @@ describe("An Android session's Device logs", function () {
|
|
|
243
181
|
const since = Date.now() - 60000;
|
|
244
182
|
yield logs.start({ sessionId, device: PHONE, since });
|
|
245
183
|
const first = logcat.proc;
|
|
246
|
-
first.stdout.write(lineAt(since + 1000, 'before 1'));
|
|
247
|
-
first.stdout.write(lineAt(since + 2000, 'before 2'));
|
|
248
|
-
yield settle();
|
|
184
|
+
first.stdout.write((0, fake_logcat_1.lineAt)(since + 1000, 'before 1'));
|
|
185
|
+
first.stdout.write((0, fake_logcat_1.lineAt)(since + 2000, 'before 2'));
|
|
186
|
+
yield (0, fake_logcat_1.settle)();
|
|
249
187
|
first.exit();
|
|
250
|
-
yield until(() => logcat.procs.length === 2);
|
|
188
|
+
yield (0, fake_logcat_1.until)(() => logcat.procs.length === 2);
|
|
251
189
|
// The new stream starts with history the session already has, then a
|
|
252
190
|
// line of the same millisecond it hadn't seen, then new lines.
|
|
253
|
-
logcat.proc.stdout.write(lineAt(since + 1000, 'before 1'));
|
|
254
|
-
logcat.proc.stdout.write(lineAt(since + 2000, 'before 2'));
|
|
255
|
-
logcat.proc.stdout.write(lineAt(since + 2000, 'also at 2'));
|
|
256
|
-
logcat.proc.stdout.write(lineAt(since + 9000, 'after'));
|
|
257
|
-
yield settle();
|
|
191
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 1000, 'before 1'));
|
|
192
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 2000, 'before 2'));
|
|
193
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 2000, 'also at 2'));
|
|
194
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 9000, 'after'));
|
|
195
|
+
yield (0, fake_logcat_1.settle)();
|
|
258
196
|
yield logs.stop(sessionId);
|
|
259
197
|
(0, chai_1.expect)(textOf(yield deviceLogs(sessionId))).to.deep.equal([
|
|
260
198
|
'before 1',
|
|
@@ -271,8 +209,8 @@ describe("An Android session's Device logs", function () {
|
|
|
271
209
|
mux.addClient((r) => viewer.push(r.message), () => true);
|
|
272
210
|
yield logs.start({ sessionId, device: PHONE, since });
|
|
273
211
|
(0, chai_1.expect)(logcat.procs).to.have.length(1);
|
|
274
|
-
logcat.proc.stdout.write(lineAt(since + 1, 'shared'));
|
|
275
|
-
yield settle();
|
|
212
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 1, 'shared'));
|
|
213
|
+
yield (0, fake_logcat_1.settle)();
|
|
276
214
|
yield logs.stop(sessionId);
|
|
277
215
|
(0, chai_1.expect)(viewer).to.deep.equal(['shared']);
|
|
278
216
|
(0, chai_1.expect)(logcat.proc.killed).to.equal(false);
|
|
@@ -294,14 +232,14 @@ describe("An Android session's Device logs", function () {
|
|
|
294
232
|
for (let i = 0; i < total; i++) {
|
|
295
233
|
// Past the limit, five lines in six are errors: 2,500 errors, 500 others.
|
|
296
234
|
const level = i >= deviceLogBook_1.DEVICE_LOG_LINE_LIMIT && i % 6 !== 0 ? 'E' : 'D';
|
|
297
|
-
chunk += lineAt(since + 1000 + i, `line ${i}`, level);
|
|
235
|
+
chunk += (0, fake_logcat_1.lineAt)(since + 1000 + i, `line ${i}`, level);
|
|
298
236
|
if (i % 1000 === 999) {
|
|
299
237
|
logcat.proc.stdout.write(chunk);
|
|
300
238
|
chunk = '';
|
|
301
|
-
yield settle();
|
|
239
|
+
yield (0, fake_logcat_1.settle)();
|
|
302
240
|
}
|
|
303
241
|
}
|
|
304
|
-
yield settle();
|
|
242
|
+
yield (0, fake_logcat_1.settle)();
|
|
305
243
|
yield logs.stop(sessionId);
|
|
306
244
|
const rows = yield deviceLogs(sessionId);
|
|
307
245
|
(0, chai_1.expect)(rows).to.have.length(deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 1 + deviceLogBook_1.DEVICE_LOG_ERROR_LIMIT + 1);
|
|
@@ -311,10 +249,12 @@ describe("An Android session's Device logs", function () {
|
|
|
311
249
|
(0, chai_1.expect)(errors.every((r) => levelOf(r.message) === 'E')).to.equal(true);
|
|
312
250
|
(0, chai_1.expect)(rows[rows.length - 1].message).to.equal("Xenon: 1,000 device log lines were left out to keep this session's log within its limit.");
|
|
313
251
|
}));
|
|
314
|
-
it("only records this server's own
|
|
252
|
+
it("only records this server's own phones", () => {
|
|
315
253
|
(0, chai_1.expect)(logs.appliesTo(PHONE)).to.equal(true);
|
|
316
254
|
(0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { nodeId: 'a-node' }))).to.equal(false);
|
|
317
|
-
|
|
255
|
+
// An iPhone is recorded from its driver's log (session-device-logs-ios.spec.ts).
|
|
256
|
+
(0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { platform: 'ios' }))).to.equal(true);
|
|
257
|
+
(0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { platform: 'windows' }))).to.equal(false);
|
|
318
258
|
(0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { cloud: true }))).to.equal(false);
|
|
319
259
|
(0, chai_1.expect)(logs.appliesTo(undefined)).to.equal(false);
|
|
320
260
|
});
|
|
@@ -322,12 +262,19 @@ describe("An Android session's Device logs", function () {
|
|
|
322
262
|
logs.clock = null;
|
|
323
263
|
const since = Date.now() - 60000;
|
|
324
264
|
yield logs.start({ sessionId, device: PHONE, since });
|
|
325
|
-
logcat.proc.stdout.write(lineAt(since - 600000, 'before'));
|
|
326
|
-
logcat.proc.stdout.write(lineAt(since + 10, 'during'));
|
|
327
|
-
yield settle();
|
|
265
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since - 600000, 'before'));
|
|
266
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 10, 'during'));
|
|
267
|
+
yield (0, fake_logcat_1.settle)();
|
|
328
268
|
yield logs.stop(sessionId);
|
|
329
269
|
(0, chai_1.expect)(textOf(yield deviceLogs(sessionId))).to.deep.equal(['during']);
|
|
330
270
|
}));
|
|
271
|
+
it('says in the log when the session turned it off', () => __awaiter(this, void 0, void 0, function* () {
|
|
272
|
+
yield logs.noteOff(sessionId);
|
|
273
|
+
(0, chai_1.expect)((yield deviceLogs(sessionId)).map((r) => r.message)).to.deep.equal([
|
|
274
|
+
"Xenon: This session's device log wasn't kept: the test turned it off.",
|
|
275
|
+
]);
|
|
276
|
+
(0, chai_1.expect)(logcat.procs).to.have.length(0);
|
|
277
|
+
}));
|
|
331
278
|
it('stops however often it is told to', () => __awaiter(this, void 0, void 0, function* () {
|
|
332
279
|
yield logs.start({ sessionId, device: PHONE, since: Date.now() });
|
|
333
280
|
yield logs.stop(sessionId);
|
|
@@ -100,10 +100,10 @@ describe('ending a failed session', () => {
|
|
|
100
100
|
notify: false,
|
|
101
101
|
});
|
|
102
102
|
it('returns while the AI is still explaining the failure', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
103
|
-
(0, chai_1.expect)(yield within(
|
|
103
|
+
(0, chai_1.expect)(yield within(10000, end())).to.equal('ended');
|
|
104
104
|
// The AI is still asked, once the session has ended.
|
|
105
105
|
const analyzeFailure = AIService_1.AI_SERVICE.analyzeFailure;
|
|
106
|
-
for (let i = 0; i <
|
|
106
|
+
for (let i = 0; i < 500 && !analyzeFailure.called; i++) {
|
|
107
107
|
yield new Promise((r) => setTimeout(r, 20));
|
|
108
108
|
}
|
|
109
109
|
(0, chai_1.expect)(analyzeFailure.calledOnce).to.equal(true);
|
|
@@ -113,17 +113,17 @@ describe('ending a failed session', () => {
|
|
|
113
113
|
});
|
|
114
114
|
}));
|
|
115
115
|
it('has filed the session under its category when it returns', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
116
|
-
yield within(
|
|
116
|
+
yield within(10000, end());
|
|
117
117
|
const saved = yield row();
|
|
118
118
|
(0, chai_1.expect)(saved.status).to.equal(SessionStatus_1.SessionStatus.FAILED);
|
|
119
119
|
(0, chai_1.expect)(saved.failure_category).to.equal('ELEMENT_NOT_FOUND');
|
|
120
120
|
(0, chai_1.expect)(saved.ai_analysis).to.equal(null);
|
|
121
121
|
}));
|
|
122
122
|
it("saves the AI's explanation when it comes", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
123
|
-
yield within(
|
|
123
|
+
yield within(10000, end());
|
|
124
124
|
explain.resolve('Root Cause: the Login button was covered by a dialog.');
|
|
125
125
|
let saved = yield row();
|
|
126
|
-
for (let i = 0; i <
|
|
126
|
+
for (let i = 0; i < 500 && !saved.ai_analysis; i++) {
|
|
127
127
|
yield new Promise((r) => setTimeout(r, 20));
|
|
128
128
|
saved = yield row();
|
|
129
129
|
}
|
|
@@ -18,6 +18,7 @@ const supertest_1 = __importDefault(require("supertest"));
|
|
|
18
18
|
const typedi_1 = require("typedi");
|
|
19
19
|
const CleanupService_1 = require("../../src/services/CleanupService");
|
|
20
20
|
const SelectorVerificationJob_1 = require("../../src/services/SelectorVerificationJob");
|
|
21
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
21
22
|
const scratch_database_1 = require("../helpers/scratch-database");
|
|
22
23
|
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
23
24
|
/** The SQL statements `run` makes. */
|
|
@@ -145,6 +146,20 @@ describe("A session's logs and profiling are read through an index", function ()
|
|
|
145
146
|
(0, chai_1.expect)(scansOf(table, steps), steps.join('\n')).to.deep.equal([]);
|
|
146
147
|
}));
|
|
147
148
|
}
|
|
149
|
+
it("finds a session's newest device log lines without a scan, as a hub resumes after a restart", () => __awaiter(this, void 0, void 0, function* () {
|
|
150
|
+
class Reader extends SessionDeviceLogs_1.SessionDeviceLogs {
|
|
151
|
+
tail(sessionId) {
|
|
152
|
+
return this.storedTail(sessionId);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
let tail = [];
|
|
156
|
+
const steps = yield stepsOn(scratch, 'Log', yield sqlOf(scratch, () => __awaiter(this, void 0, void 0, function* () {
|
|
157
|
+
tail = yield new Reader().tail('ix-session');
|
|
158
|
+
})));
|
|
159
|
+
(0, chai_1.expect)(tail.map((l) => l.message)).to.deep.equal(['I/ActivityManager: start']);
|
|
160
|
+
(0, chai_1.expect)(steps, 'no Log statement was captured').to.not.be.empty;
|
|
161
|
+
(0, chai_1.expect)(scansOf('Log', steps), steps.join('\n')).to.deep.equal([]);
|
|
162
|
+
}));
|
|
148
163
|
it("deletes a session's commands, logs and profiling without a scan", () => __awaiter(this, void 0, void 0, function* () {
|
|
149
164
|
const ran = yield sqlOf(scratch, () => typedi_1.Container.get(CleanupService_1.CleanupService).purgeBuild('ix-build', false));
|
|
150
165
|
for (const table of ['SessionLog', 'Log', 'Profiling']) {
|
|
@@ -199,7 +214,7 @@ describe('The selector verification reads SessionLog through an index', function
|
|
|
199
214
|
});
|
|
200
215
|
}));
|
|
201
216
|
it("reads a selector's finds without a scan", () => __awaiter(this, void 0, void 0, function* () {
|
|
202
|
-
const socket = {
|
|
217
|
+
const socket = { emitToDashboardForSelector: () => __awaiter(this, void 0, void 0, function* () { return undefined; }) };
|
|
203
218
|
const ran = yield sqlOf(scratch, () => new SelectorVerificationJob_1.SelectorVerificationJob(scratch.db, socket).run());
|
|
204
219
|
const steps = yield stepsOn(scratch, 'SessionLog', ran);
|
|
205
220
|
(0, chai_1.expect)(steps, 'no SessionLog statement was captured').to.not.be.empty;
|