@keepsnappy/agent 0.1.0-alpha.12 → 0.1.0-alpha.14
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 +53 -55
- package/dist/appium-driver.js +334 -84
- package/dist/keepsnappy.js +456 -931
- package/examples/farm/flows/{perflab → keepsnappy}/begin-run.yaml +1 -1
- package/examples/farm/flows/keepsnappy/end-run.yaml +17 -0
- package/examples/farm/flows/keepsnappy/next-token.js +7 -0
- package/examples/farm/flows/{perflab/run-perflab.yaml → keepsnappy/run-keepsnappy.yaml} +1 -1
- package/examples/farm/flows/{perflab → keepsnappy}/send-markers.js +3 -3
- package/examples/farm/flows/keepsnappy/step-markers.yaml +13 -0
- package/examples/farm/flows/keepsnappy/tokens.js +2 -0
- package/examples/farm/workflows/{perflab-measure.yml → keepsnappy-measure.yml} +17 -17
- package/examples/farm/workflows/perf-device-run.fragment.yml +5 -5
- package/examples/github-actions.yml +14 -14
- package/examples/keepsnappy.yml +2 -4
- package/examples/maestro-steps/e2e-community-and-live-chat.yaml +10 -10
- package/maestro/{perflab → keepsnappy}/send-markers.js +3 -3
- package/maestro/{perflab → keepsnappy}/step.js +6 -6
- package/maestro/keepsnappy/step.yaml +11 -0
- package/package.json +1 -1
- package/examples/farm/flows/perflab/end-run.yaml +0 -16
- package/examples/farm/flows/perflab/next-token.js +0 -7
- package/examples/farm/flows/perflab/step-markers.yaml +0 -13
- package/examples/farm/flows/perflab/tokens.js +0 -2
- package/maestro/perflab/step.yaml +0 -11
package/dist/appium-driver.js
CHANGED
|
@@ -5,7 +5,7 @@ import { isAbsolute, resolve } from "node:path";
|
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
|
|
7
7
|
// packages/appium-driver/src/run.ts
|
|
8
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { parseArgs } from "node:util";
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ import { basename } from "node:path";
|
|
|
17
17
|
var ALERT_POLL_MS = 250;
|
|
18
18
|
var RUN_DEFAULTS = { ttl: 3600, profile: 1, diag: 1 };
|
|
19
19
|
var END_GC_DEFAULT = true;
|
|
20
|
-
var route = (scheme) => `${scheme}devtools/
|
|
20
|
+
var route = (scheme) => `${scheme}devtools/keepsnappy`;
|
|
21
21
|
function parseSdkArgs(args) {
|
|
22
22
|
return args.split("&").filter(Boolean).map((kv) => {
|
|
23
23
|
const i = kv.indexOf("=");
|
|
@@ -32,8 +32,9 @@ function linkArgs(args = "") {
|
|
|
32
32
|
return [...fields].map(([k, v]) => `${k}=${v}`).join("&");
|
|
33
33
|
}
|
|
34
34
|
var activationLink = (scheme, token, args = "") => `${route(scheme)}?activate=1&${linkArgs(args)}&runToken=${token}`;
|
|
35
|
-
var flushLink = (scheme) => `${route(scheme)}?flush=1&activate=0`;
|
|
35
|
+
var flushLink = (scheme, ack) => `${route(scheme)}?flush=1&activate=0${ack === undefined ? "" : `&ack=${ack}`}`;
|
|
36
36
|
var gcLink = (scheme, n) => `${route(scheme)}?cmd=gc&n=${n}`;
|
|
37
|
+
var memReadLink = (scheme, n) => `${route(scheme)}?cmd=memread&n=${n}`;
|
|
37
38
|
async function acceptAlert(driver, labels, waitMs) {
|
|
38
39
|
const deadline = driver.now() + waitMs;
|
|
39
40
|
for (;; ) {
|
|
@@ -506,13 +507,14 @@ class Driver {
|
|
|
506
507
|
// packages/appium-driver/src/platform.ts
|
|
507
508
|
var modelOf = (device) => device.replace(/-[^-]*$/, "");
|
|
508
509
|
var versionOf = (device) => device.replace(/^.*-/, "");
|
|
509
|
-
var FLUSH_ID = "
|
|
510
|
-
var ACK_ID = "
|
|
510
|
+
var FLUSH_ID = "keepsnappy.flushed";
|
|
511
|
+
var ACK_ID = "keepsnappy.ack";
|
|
511
512
|
var GC_ANSWER_WAIT_MS = 1e4;
|
|
512
513
|
var GC_POLL_MS = 100;
|
|
513
|
-
|
|
514
|
+
var MEMREAD_ANSWER_WAIT_MS = 3000;
|
|
515
|
+
function parseBroadcastAnswer(out, n, cmd = "gc") {
|
|
514
516
|
const data = /data="([^"]*)"/.exec(String(out ?? ""))?.[1];
|
|
515
|
-
return data !== undefined && data.startsWith(
|
|
517
|
+
return data !== undefined && data.startsWith(`${cmd}:${n} `) ? data : null;
|
|
516
518
|
}
|
|
517
519
|
var NO_WAIT_MS = 0;
|
|
518
520
|
var NEW_COMMAND_TIMEOUT_S = 300;
|
|
@@ -534,25 +536,27 @@ var IOS = {
|
|
|
534
536
|
appArg: (bundleId) => ({ bundleId }),
|
|
535
537
|
openLink: (driver, url, state) => openLink(driver, url, state),
|
|
536
538
|
acceptAlert: (driver, labels, waitMs) => acceptAlert(driver, labels, waitMs),
|
|
537
|
-
flush: { lookup: { using: "accessibility id", value: FLUSH_ID }, statusAttribute: "label" },
|
|
539
|
+
flush: { lookup: { using: "accessibility id", value: FLUSH_ID }, statusAttribute: "label", detailAttribute: "value" },
|
|
538
540
|
uploadFields: { ios_keychain_support: "true" },
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const want = `type == "XCUIElementTypeStaticText" AND name == "${ACK_ID}" AND label BEGINSWITH "gc:${n} "`;
|
|
542
|
-
const deadline = driver.now() + GC_ANSWER_WAIT_MS;
|
|
543
|
-
for (;; ) {
|
|
544
|
-
const el = await driver.find("-ios predicate string", want);
|
|
545
|
-
if (el)
|
|
546
|
-
return driver.cmd("GET", `/element/${el}/attribute/label`).then(String).catch(() => {
|
|
547
|
-
driver.log.push({ ...driver.context, cmd: "gc-ack-label", t: driver.now(), ms: 0, status: 1 });
|
|
548
|
-
return `gc:${n} ok`;
|
|
549
|
-
});
|
|
550
|
-
if (driver.now() > deadline)
|
|
551
|
-
return null;
|
|
552
|
-
await driver.sleep(GC_POLL_MS);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
541
|
+
gcCommand: (driver, n, scheme, state) => iosCommand(driver, gcLink(scheme, n), "gc", n, state, GC_ANSWER_WAIT_MS),
|
|
542
|
+
memReadCommand: (driver, n, scheme, state) => iosCommand(driver, memReadLink(scheme, n), "memread", n, state, MEMREAD_ANSWER_WAIT_MS)
|
|
555
543
|
};
|
|
544
|
+
async function iosCommand(driver, link, cmd, n, state, waitMs) {
|
|
545
|
+
await openLink(driver, link, state);
|
|
546
|
+
const want = `type == "XCUIElementTypeStaticText" AND name == "${ACK_ID}" AND label BEGINSWITH "${cmd}:${n} "`;
|
|
547
|
+
const deadline = driver.now() + waitMs;
|
|
548
|
+
for (;; ) {
|
|
549
|
+
const el = await driver.find("-ios predicate string", want);
|
|
550
|
+
if (el)
|
|
551
|
+
return driver.cmd("GET", `/element/${el}/attribute/label`).then(String).catch(() => {
|
|
552
|
+
driver.log.push({ ...driver.context, cmd: `${cmd}-ack-label`, t: driver.now(), ms: 0, status: 1 });
|
|
553
|
+
return `${cmd}:${n} ok`;
|
|
554
|
+
});
|
|
555
|
+
if (driver.now() > deadline)
|
|
556
|
+
return null;
|
|
557
|
+
await driver.sleep(GC_POLL_MS);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
556
560
|
var ANDROID = {
|
|
557
561
|
name: "android",
|
|
558
562
|
defaultDevice: "Google Pixel 7-13.0",
|
|
@@ -590,6 +594,7 @@ var ANDROID = {
|
|
|
590
594
|
flush: {
|
|
591
595
|
lookup: { using: "-android uiautomator", value: `new UiSelector().description("${FLUSH_ID}")` },
|
|
592
596
|
statusAttribute: "text",
|
|
597
|
+
detailAttribute: "hint",
|
|
593
598
|
fallback: { lookup: { using: "xpath", value: `//*[@content-desc="${FLUSH_ID}"]` }, setting: "allowInvisibleElements" }
|
|
594
599
|
},
|
|
595
600
|
async readScreen(driver) {
|
|
@@ -606,16 +611,18 @@ var ANDROID = {
|
|
|
606
611
|
singleFindFirst: true,
|
|
607
612
|
anyOfIds: androidAnyOfIds,
|
|
608
613
|
uploadFields: {},
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
action: `${driver.bundleId}.perflab.COMMAND`,
|
|
612
|
-
package: driver.bundleId,
|
|
613
|
-
extras: [["s", "cmd", "gc"], ["i", "n", String(n)]],
|
|
614
|
-
flags: "0x10000000"
|
|
615
|
-
});
|
|
616
|
-
return parseBroadcastAnswer(out, n);
|
|
617
|
-
}
|
|
614
|
+
gcCommand: (driver, n) => androidCommand(driver, "gc", n),
|
|
615
|
+
memReadCommand: (driver, n) => androidCommand(driver, "memread", n)
|
|
618
616
|
};
|
|
617
|
+
async function androidCommand(driver, cmd, n) {
|
|
618
|
+
const out = await driver.execute("mobile: broadcast", {
|
|
619
|
+
action: `${driver.bundleId}.keepsnappy.COMMAND`,
|
|
620
|
+
package: driver.bundleId,
|
|
621
|
+
extras: [["s", "cmd", cmd], ["i", "n", String(n)]],
|
|
622
|
+
flags: "0x10000000"
|
|
623
|
+
});
|
|
624
|
+
return parseBroadcastAnswer(out, n, cmd);
|
|
625
|
+
}
|
|
619
626
|
var PLATFORMS = { ios: IOS, android: ANDROID };
|
|
620
627
|
function platformOf(name) {
|
|
621
628
|
if (!Object.hasOwn(PLATFORMS, name))
|
|
@@ -656,7 +663,7 @@ function sessionCaps(i) {
|
|
|
656
663
|
...(i.platform ?? IOS).caps(i.device),
|
|
657
664
|
"appium:app": i.appUrl,
|
|
658
665
|
"bstack:options": {
|
|
659
|
-
projectName: "
|
|
666
|
+
projectName: "keepsnappy",
|
|
660
667
|
buildName: i.buildName,
|
|
661
668
|
sessionName: i.sessionName,
|
|
662
669
|
...tunnel,
|
|
@@ -770,6 +777,15 @@ var SDK_FLUSH_WORST_CASE_MS = 315000;
|
|
|
770
777
|
var FLUSH_MARGIN_MS = 30000;
|
|
771
778
|
var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
|
|
772
779
|
var FLUSH_POLL_MS = 500;
|
|
780
|
+
var FLUSH_RESEND_MS = 90000;
|
|
781
|
+
var FLUSH_NOT_ACKNOWLEDGED = "not acknowledged (re-sent once)";
|
|
782
|
+
var FLUSH_UNREADABLE = "not acknowledged (unreadable answer)";
|
|
783
|
+
var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
|
|
784
|
+
var UNREADABLE_POLLS = 3;
|
|
785
|
+
var FLUSH_END_RUN_FAILED = "not acknowledged (end-run failed)";
|
|
786
|
+
var ACK_ECHO = / ?ack=([A-Za-z0-9_-]+)$/;
|
|
787
|
+
var FLUSH_ERROR = "error";
|
|
788
|
+
var FLUSH_OFF = "off";
|
|
773
789
|
var FLUSH_FALLBACK_EVERY = 10;
|
|
774
790
|
var RESCUE_EVERY_MS = 3000;
|
|
775
791
|
var noop = async () => {
|
|
@@ -790,6 +806,8 @@ class Session {
|
|
|
790
806
|
endGc;
|
|
791
807
|
gcN = 0;
|
|
792
808
|
stepDepth = 0;
|
|
809
|
+
stepLog = [];
|
|
810
|
+
onStepChange = null;
|
|
793
811
|
refinds = 0;
|
|
794
812
|
get staleRefinds() {
|
|
795
813
|
return this.refinds;
|
|
@@ -818,6 +836,18 @@ class Session {
|
|
|
818
836
|
this.actionGc = answer === `gc:${n} unavailable` ? "unavailable" : "unsupported";
|
|
819
837
|
return null;
|
|
820
838
|
}
|
|
839
|
+
memReadOff = false;
|
|
840
|
+
async memRead(n) {
|
|
841
|
+
if (n === null || this.memReadOff || this.linkState.linkMode === "safari")
|
|
842
|
+
return false;
|
|
843
|
+
const start = this.now();
|
|
844
|
+
const answer = await this.driver.platform.memReadCommand(this.driver, n, this.scheme, this.linkState).catch(() => null);
|
|
845
|
+
const ok = answer === `memread:${n} ok`;
|
|
846
|
+
this.driver.log.push({ ...this.driver.context, cmd: "memread", t: this.now(), ms: this.now() - start, status: ok ? 0 : 1, ...answer !== null ? { answer } : {} });
|
|
847
|
+
if (!ok)
|
|
848
|
+
this.memReadOff = true;
|
|
849
|
+
return ok;
|
|
850
|
+
}
|
|
821
851
|
now() {
|
|
822
852
|
return this.driver.now();
|
|
823
853
|
}
|
|
@@ -842,19 +872,30 @@ class Session {
|
|
|
842
872
|
async step(name, action, checks = noop, once = noop) {
|
|
843
873
|
await this.ready();
|
|
844
874
|
this.driver.context = { ...this.driver.context, step: name };
|
|
875
|
+
const record = { journey: this.driver.context.journey, iteration: this.driver.context.iteration, name, depth: this.stepDepth, t: this.now(), ms: null };
|
|
876
|
+
this.stepLog.push(record);
|
|
845
877
|
this.stepDepth++;
|
|
846
878
|
try {
|
|
879
|
+
if (record.depth === 0)
|
|
880
|
+
this.onStepChange?.();
|
|
847
881
|
const gcN = this.stepDepth === 1 ? await this.gc() : null;
|
|
882
|
+
record.t = this.now();
|
|
848
883
|
this.marker(name, "start", gcN);
|
|
849
884
|
await action();
|
|
850
885
|
await checks();
|
|
851
886
|
await once();
|
|
852
887
|
this.marker(name, "end");
|
|
853
888
|
await checks();
|
|
889
|
+
} catch (e) {
|
|
890
|
+
record.failed = true;
|
|
891
|
+
throw e;
|
|
854
892
|
} finally {
|
|
893
|
+
record.ms = this.now() - record.t;
|
|
855
894
|
this.stepDepth--;
|
|
856
895
|
const { step: _done, ...rest } = this.driver.context;
|
|
857
896
|
this.driver.context = rest;
|
|
897
|
+
if (record.depth === 0)
|
|
898
|
+
this.onStepChange?.();
|
|
858
899
|
}
|
|
859
900
|
}
|
|
860
901
|
isVisible(sel) {
|
|
@@ -1143,24 +1184,73 @@ class Session {
|
|
|
1143
1184
|
await this.waitVisible(home, 30000);
|
|
1144
1185
|
return this.openLink(activationLink(this.scheme, token, args));
|
|
1145
1186
|
}
|
|
1187
|
+
lastFlush = null;
|
|
1188
|
+
flushSeq = 0;
|
|
1146
1189
|
async endRun() {
|
|
1147
1190
|
if (this.markers.some((m) => m.phase === "start"))
|
|
1148
|
-
await this.gc(true);
|
|
1191
|
+
await this.memRead(await this.gc(true));
|
|
1149
1192
|
await sendMarkers(this.fetch, this.api, this.markers, { sleep: (ms) => this.driver.sleep(ms), now: () => this.now() });
|
|
1150
|
-
|
|
1193
|
+
const evidence = { linkAt: this.now(), resentAt: null, ackAt: null, gaveUpAt: null };
|
|
1194
|
+
this.lastFlush = evidence;
|
|
1195
|
+
const acks = new Set;
|
|
1196
|
+
const link = () => {
|
|
1197
|
+
const ack = String(++this.flushSeq);
|
|
1198
|
+
acks.add(ack);
|
|
1199
|
+
return flushLink(this.scheme, ack);
|
|
1200
|
+
};
|
|
1201
|
+
await this.openLink(link());
|
|
1202
|
+
let answer = await this.waitFlushed(evidence.linkAt + FLUSH_RESEND_MS, acks);
|
|
1203
|
+
if (answer === null || "unreadable" in answer) {
|
|
1204
|
+
evidence.resentAt = this.now();
|
|
1205
|
+
if (answer !== null)
|
|
1206
|
+
acks.clear();
|
|
1207
|
+
await this.openLink(link());
|
|
1208
|
+
answer = await this.waitFlushed(evidence.resentAt + FLUSH_WAIT_MS, acks) ?? answer;
|
|
1209
|
+
}
|
|
1210
|
+
if (answer === null || "unreadable" in answer) {
|
|
1211
|
+
evidence.gaveUpAt = this.now();
|
|
1212
|
+
if (answer === null)
|
|
1213
|
+
return FLUSH_NOT_ACKNOWLEDGED;
|
|
1214
|
+
evidence.raw = answer.unreadable;
|
|
1215
|
+
return FLUSH_UNREADABLE;
|
|
1216
|
+
}
|
|
1217
|
+
evidence.ackAt = this.now();
|
|
1218
|
+
return answer.status;
|
|
1219
|
+
}
|
|
1220
|
+
async waitFlushed(deadline, acks) {
|
|
1151
1221
|
const flush = this.driver.platform.flush;
|
|
1152
|
-
|
|
1222
|
+
let unreadable = null;
|
|
1223
|
+
let unreadableRun = 0;
|
|
1153
1224
|
for (let poll = 1;this.now() < deadline; poll++) {
|
|
1154
1225
|
let el = await this.driver.find(flush.lookup.using, flush.lookup.value);
|
|
1155
1226
|
if (!el && flush.fallback && poll % FLUSH_FALLBACK_EVERY === 0)
|
|
1156
1227
|
el = await this.findFallback(flush.fallback);
|
|
1157
1228
|
if (el) {
|
|
1158
1229
|
const label = await this.driver.cmd("GET", `/element/${el}/attribute/${flush.statusAttribute}`).catch(() => null);
|
|
1159
|
-
|
|
1230
|
+
const raw = await this.driver.cmd("GET", `/element/${el}/attribute/${flush.detailAttribute}`).then((value) => ({ value }), () => null);
|
|
1231
|
+
const detail = typeof raw?.value === "string" ? raw.value : null;
|
|
1232
|
+
const echoed = detail === null ? null : ACK_ECHO.exec(detail);
|
|
1233
|
+
if (raw === null) {
|
|
1234
|
+
unreadable = { status: label, detail: null };
|
|
1235
|
+
if (++unreadableRun >= UNREADABLE_POLLS)
|
|
1236
|
+
return { unreadable };
|
|
1237
|
+
} else if (!echoed || acks.has(echoed[1])) {
|
|
1238
|
+
if (this.lastFlush)
|
|
1239
|
+
this.lastFlush.detail = detail === null ? null : detail.replace(ACK_ECHO, "");
|
|
1240
|
+
if (typeof label === "string" && FLUSH_STATUSES.has(label))
|
|
1241
|
+
return { status: label };
|
|
1242
|
+
unreadable = { status: label, detail };
|
|
1243
|
+
if (++unreadableRun >= UNREADABLE_POLLS)
|
|
1244
|
+
return { unreadable };
|
|
1245
|
+
} else {
|
|
1246
|
+
unreadableRun = 0;
|
|
1247
|
+
}
|
|
1248
|
+
} else {
|
|
1249
|
+
unreadableRun = 0;
|
|
1160
1250
|
}
|
|
1161
1251
|
await this.driver.sleep(FLUSH_POLL_MS);
|
|
1162
1252
|
}
|
|
1163
|
-
return null;
|
|
1253
|
+
return unreadable ? { unreadable } : null;
|
|
1164
1254
|
}
|
|
1165
1255
|
async findFallback(f) {
|
|
1166
1256
|
await this.driver.cmd("POST", "/appium/settings", { settings: { [f.setting]: true } });
|
|
@@ -1178,7 +1268,7 @@ function parseTokensJs(text) {
|
|
|
1178
1268
|
const m = text.match(new RegExp(`output\\.${name}\\s*=\\s*(.+);\\s*$`, "m"));
|
|
1179
1269
|
return m ? JSON.parse(m[1]) : undefined;
|
|
1180
1270
|
};
|
|
1181
|
-
return { api: pick("
|
|
1271
|
+
return { api: pick("keepsnappyApi") ?? "", tokens: pick("keepsnappyTokens") ?? {} };
|
|
1182
1272
|
}
|
|
1183
1273
|
function plan(journeys, repeat, tokens) {
|
|
1184
1274
|
const out = [];
|
|
@@ -1190,13 +1280,17 @@ function plan(journeys, repeat, tokens) {
|
|
|
1190
1280
|
}
|
|
1191
1281
|
|
|
1192
1282
|
// packages/appium-driver/src/run.ts
|
|
1283
|
+
var TERMINATION_BUDGET = { captureMs: 20000, endMs: 1e4, hardMs: 45000 };
|
|
1284
|
+
var EXIT_CODE = { SIGTERM: 143, SIGINT: 130 };
|
|
1285
|
+
var realProcessHooks = { on: (s, h) => void process.on(s, h), off: (s, h) => void process.off(s, h), exit: (c) => process.exit(c) };
|
|
1193
1286
|
var realRunDeps = () => ({
|
|
1194
1287
|
env: process.env,
|
|
1195
1288
|
fetch,
|
|
1196
1289
|
now: Date.now,
|
|
1197
1290
|
sleep: realSleep,
|
|
1198
1291
|
print: (l) => console.log(l),
|
|
1199
|
-
error: (l) => console.error(l)
|
|
1292
|
+
error: (l) => console.error(l),
|
|
1293
|
+
process: realProcessHooks
|
|
1200
1294
|
});
|
|
1201
1295
|
var DRIVER_OPTIONS = {
|
|
1202
1296
|
"journey-module": { type: "string" },
|
|
@@ -1222,6 +1316,16 @@ function parseDriverArgs(argv, module) {
|
|
|
1222
1316
|
function journeyModulePath(argv) {
|
|
1223
1317
|
return parseArgs({ args: [...argv], options: { "journey-module": { type: "string" } }, strict: false }).values["journey-module"];
|
|
1224
1318
|
}
|
|
1319
|
+
function writeAtomic(file, text) {
|
|
1320
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
1321
|
+
writeFileSync(tmp, text);
|
|
1322
|
+
renameSync(tmp, file);
|
|
1323
|
+
}
|
|
1324
|
+
async function captureFailure(driver, out) {
|
|
1325
|
+
const source = driver.cmd("GET", "/source").then((s) => void (typeof s === "string" && writeFileSync(join(out, "failure-source.xml"), s)), () => null);
|
|
1326
|
+
const screen = driver.cmd("GET", "/screenshot").then((png) => void (typeof png === "string" && writeFileSync(join(out, "failure-screen.png"), Buffer.from(png, "base64"))), () => null);
|
|
1327
|
+
await Promise.all([source, screen]);
|
|
1328
|
+
}
|
|
1225
1329
|
async function runDriver(argv, module, deps = realRunDeps()) {
|
|
1226
1330
|
const a = parseDriverArgs(argv, module);
|
|
1227
1331
|
const user = deps.env.BROWSERSTACK_USERNAME;
|
|
@@ -1246,66 +1350,210 @@ async function runDriver(argv, module, deps = realRunDeps()) {
|
|
|
1246
1350
|
const auth = basicAuth(user, key);
|
|
1247
1351
|
mkdirSync(out, { recursive: true });
|
|
1248
1352
|
const { api, tokens } = parseTokensJs(readFileSync(String(a["tokens-js"]), "utf8"));
|
|
1249
|
-
const appUrl = await resolveApp(String(a.app), String(a["app-custom-id"] ?? ""), auth, deps.fetch, platform);
|
|
1250
|
-
deps.print(`app: ${appUrl}`);
|
|
1251
1353
|
const log = [];
|
|
1252
1354
|
const driver = new Driver({ hub: HUB, log, headers: { Authorization: auth }, bundleId: module.app.bundleId, platform, fetch: deps.fetch, now: deps.now, sleep: deps.sleep });
|
|
1253
1355
|
const t = new Session({ driver, scheme: module.app.scheme, fetch: deps.fetch, api, actionGc: a["action-gc"] === "on", endGc: a["end-gc"] === "on" });
|
|
1254
|
-
const sessionName = module.sessionName ?? "
|
|
1356
|
+
const sessionName = module.sessionName ?? "keepsnappy (appium)";
|
|
1255
1357
|
const device = String(a.device);
|
|
1256
1358
|
const ctx = { fetch: deps.fetch, tokens, journeys, repeat, args: a };
|
|
1257
|
-
|
|
1258
|
-
|
|
1359
|
+
let startedAt = deps.now();
|
|
1360
|
+
let endedAt = null;
|
|
1361
|
+
let appUrl = null;
|
|
1362
|
+
let buildId = null;
|
|
1363
|
+
let status = "running";
|
|
1364
|
+
let signal = null;
|
|
1259
1365
|
let error = null;
|
|
1366
|
+
let finalFlush = null;
|
|
1367
|
+
let flushNotAcknowledged = [];
|
|
1368
|
+
let flushError = [];
|
|
1369
|
+
let flushOff = [];
|
|
1260
1370
|
const iterations = [];
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1371
|
+
let terminating = false;
|
|
1372
|
+
const logFile = join(out, "driver-log.json");
|
|
1373
|
+
const persist = () => {
|
|
1374
|
+
const last = t.stepLog.at(-1);
|
|
1375
|
+
const body = {
|
|
1376
|
+
driver: "appium",
|
|
1377
|
+
status,
|
|
1378
|
+
device,
|
|
1379
|
+
appUrl,
|
|
1380
|
+
session: driver.sessionId,
|
|
1381
|
+
buildId,
|
|
1382
|
+
linkMode: t.linkState.linkMode ?? null,
|
|
1383
|
+
actionGc: t.actionGc,
|
|
1384
|
+
endGc: t.endGc,
|
|
1385
|
+
linkError: t.linkState.linkError ?? null,
|
|
1386
|
+
startedAt,
|
|
1387
|
+
endedAt,
|
|
1388
|
+
total_ms: (endedAt ?? deps.now()) - startedAt,
|
|
1389
|
+
error: error?.message ?? null,
|
|
1390
|
+
signal,
|
|
1391
|
+
lastStep: last ? { name: last.name, t: last.t, journey: last.journey ?? null, iteration: last.iteration ?? null } : null,
|
|
1392
|
+
flushNotAcknowledged,
|
|
1393
|
+
flushError,
|
|
1394
|
+
flushOff,
|
|
1395
|
+
finalFlush,
|
|
1396
|
+
steps: t.stepLog,
|
|
1397
|
+
iterations,
|
|
1398
|
+
commands: log
|
|
1399
|
+
};
|
|
1400
|
+
writeAtomic(logFile, `${JSON.stringify(body, null, 2)}
|
|
1401
|
+
`);
|
|
1402
|
+
};
|
|
1403
|
+
const progress = () => {
|
|
1404
|
+
if (terminating)
|
|
1405
|
+
return;
|
|
1406
|
+
try {
|
|
1407
|
+
persist();
|
|
1408
|
+
} catch (e) {
|
|
1409
|
+
deps.error(`::warning::driver-log.json not written: ${e instanceof Error ? e.message : String(e)}`);
|
|
1277
1410
|
}
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
await t.endRun().catch(() => null);
|
|
1282
|
-
const reason = error ? error.message.slice(0, 250) : `${sessionName} done`;
|
|
1283
|
-
await driver.execute(`browserstack_executor: ${JSON.stringify({ action: "setSessionStatus", arguments: { status: error ? "failed" : "passed", reason } })}`).catch(() => null);
|
|
1284
|
-
}
|
|
1285
|
-
const endedAt = deps.now();
|
|
1286
|
-
await driver.end();
|
|
1287
|
-
const detail = driver.sessionId ? await sessionDetail(driver.sessionId, auth, deps.fetch) : {};
|
|
1288
|
-
const buildId = detail.build_hashed_id ?? null;
|
|
1289
|
-
writeFileSync(join(out, `browserstack.${String(a.platform)}.json`), `${JSON.stringify({
|
|
1411
|
+
};
|
|
1412
|
+
let dashboard = null;
|
|
1413
|
+
const writeBuild = (failure) => writeAtomic(join(out, `browserstack.${String(a.platform)}.json`), `${JSON.stringify({
|
|
1290
1414
|
driver: "appium",
|
|
1291
|
-
status:
|
|
1415
|
+
status: failure === null ? "passed" : "failed",
|
|
1292
1416
|
buildId,
|
|
1293
|
-
dashboard
|
|
1294
|
-
devices: [{ device, sessions: [{ id: driver.sessionId, ...
|
|
1417
|
+
dashboard,
|
|
1418
|
+
devices: [{ device, sessions: [{ id: driver.sessionId, ...failure === null ? {} : { error: { message: failure } } }] }]
|
|
1295
1419
|
}, null, 2)}
|
|
1296
1420
|
`);
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1421
|
+
const readDetail = async () => {
|
|
1422
|
+
const detail = driver.sessionId ? await sessionDetail(driver.sessionId, auth, deps.fetch) : {};
|
|
1423
|
+
buildId = detail.build_hashed_id ?? null;
|
|
1424
|
+
dashboard = detail.public_url ?? detail.browser_url ?? null;
|
|
1425
|
+
};
|
|
1426
|
+
const setSessionStatus = (failed, reason) => driver.execute(`browserstack_executor: ${JSON.stringify({ action: "setSessionStatus", arguments: { status: failed ? "failed" : "passed", reason } })}`).catch(() => null);
|
|
1427
|
+
const proc = deps.process;
|
|
1428
|
+
const budget = { ...TERMINATION_BUDGET, ...deps.term };
|
|
1429
|
+
const timers = new Set;
|
|
1430
|
+
const within = async (ms, work) => {
|
|
1431
|
+
let timer;
|
|
1432
|
+
await Promise.race([work.catch(() => null), new Promise((resolve) => timers.add(timer = setTimeout(resolve, ms)))]);
|
|
1433
|
+
clearTimeout(timer);
|
|
1434
|
+
};
|
|
1435
|
+
const leave = (code) => {
|
|
1436
|
+
for (const timer of timers)
|
|
1437
|
+
clearTimeout(timer);
|
|
1438
|
+
timers.clear();
|
|
1439
|
+
proc?.exit(code);
|
|
1440
|
+
};
|
|
1441
|
+
const onSignal = (sig) => async () => {
|
|
1442
|
+
if (terminating)
|
|
1443
|
+
return leave(EXIT_CODE[sig]);
|
|
1444
|
+
terminating = true;
|
|
1445
|
+
signal = sig;
|
|
1446
|
+
status = "terminated";
|
|
1447
|
+
error ??= new Error("terminated");
|
|
1448
|
+
endedAt = deps.now();
|
|
1449
|
+
const stopped = t.stepLog.at(-1);
|
|
1450
|
+
deps.error(`::error::terminated by ${sig}${stopped ? ` in step ${stopped.name}` : ""}`);
|
|
1451
|
+
try {
|
|
1452
|
+
persist();
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
deps.error(`::warning::driver-log.json not written: ${e instanceof Error ? e.message : String(e)}`);
|
|
1455
|
+
}
|
|
1456
|
+
timers.add(setTimeout(() => leave(EXIT_CODE[sig]), budget.hardMs));
|
|
1457
|
+
try {
|
|
1458
|
+
if (driver.sessionId) {
|
|
1459
|
+
await within(budget.captureMs, captureFailure(driver, out));
|
|
1460
|
+
await within(budget.endMs, (async () => {
|
|
1461
|
+
await setSessionStatus(true, `terminated by ${sig}`.slice(0, 250));
|
|
1462
|
+
await driver.end();
|
|
1463
|
+
await readDetail();
|
|
1464
|
+
})());
|
|
1465
|
+
writeBuild(`terminated by ${sig}`);
|
|
1466
|
+
}
|
|
1467
|
+
endedAt = deps.now();
|
|
1468
|
+
persist();
|
|
1469
|
+
} catch (e) {
|
|
1470
|
+
deps.error(`::warning::termination cleanup failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1471
|
+
} finally {
|
|
1472
|
+
leave(EXIT_CODE[sig]);
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
const handlers = ["SIGTERM", "SIGINT"].map((sig) => [sig, onSignal(sig)]);
|
|
1476
|
+
for (const [sig, handler] of handlers)
|
|
1477
|
+
proc?.on(sig, handler);
|
|
1478
|
+
t.onStepChange = progress;
|
|
1479
|
+
progress();
|
|
1480
|
+
try {
|
|
1481
|
+
appUrl = await resolveApp(String(a.app), String(a["app-custom-id"] ?? ""), auth, deps.fetch, platform);
|
|
1482
|
+
deps.print(`app: ${appUrl}`);
|
|
1483
|
+
const caps = sessionCaps({ appUrl, device, buildName: String(a["build-name"]), sessionName, appiumVersion: String(a["appium-version"]), platform, localIdentifier: a["local-identifier"] });
|
|
1484
|
+
startedAt = deps.now();
|
|
1485
|
+
try {
|
|
1486
|
+
await driver.start(caps);
|
|
1487
|
+
deps.print(`session ${driver.sessionId}`);
|
|
1488
|
+
progress();
|
|
1489
|
+
await driver.cmd("POST", "/appium/settings", { settings: platform.settings }).catch(() => null);
|
|
1490
|
+
await module.setup?.(t, ctx);
|
|
1491
|
+
await runIterations(t, module, ctx, iterations, progress);
|
|
1492
|
+
} catch (e) {
|
|
1493
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
1494
|
+
deps.error(`::error::${error.message}`);
|
|
1495
|
+
progress();
|
|
1496
|
+
if (driver.sessionId)
|
|
1497
|
+
await captureFailure(driver, out);
|
|
1498
|
+
}
|
|
1499
|
+
if (driver.sessionId) {
|
|
1500
|
+
driver.context = { phase: "final-flush" };
|
|
1501
|
+
t.lastFlush = null;
|
|
1502
|
+
progress();
|
|
1503
|
+
try {
|
|
1504
|
+
finalFlush = { flushed: await t.endRun(), flush: t.lastFlush };
|
|
1505
|
+
} catch (e) {
|
|
1506
|
+
const started = t.lastFlush ?? { linkAt: null, resentAt: null, ackAt: null, gaveUpAt: null };
|
|
1507
|
+
finalFlush = { flushed: FLUSH_END_RUN_FAILED, flush: { ...started, gaveUpAt: deps.now(), error: e instanceof Error ? e.message : String(e) } };
|
|
1508
|
+
}
|
|
1509
|
+
progress();
|
|
1510
|
+
await setSessionStatus(!!error, error ? error.message.slice(0, 250) : `${sessionName} done`);
|
|
1511
|
+
}
|
|
1512
|
+
const flushedAs = (flushStatus, tokenOnly = false) => [
|
|
1513
|
+
...iterations.filter((r) => r.flushed === flushStatus && (!tokenOnly || r.token)).map((r) => `${r.journey} #${r.iteration}`),
|
|
1514
|
+
...finalFlush?.flushed === flushStatus && (!tokenOnly || iterations.some((r) => r.token)) ? ["final flush"] : []
|
|
1515
|
+
];
|
|
1516
|
+
flushNotAcknowledged = [...flushedAs(FLUSH_NOT_ACKNOWLEDGED), ...flushedAs(FLUSH_UNREADABLE), ...flushedAs(FLUSH_END_RUN_FAILED)];
|
|
1517
|
+
flushError = flushedAs(FLUSH_ERROR);
|
|
1518
|
+
flushOff = flushedAs(FLUSH_OFF, true);
|
|
1519
|
+
if (flushNotAcknowledged.length)
|
|
1520
|
+
deps.error(`::error::flush not acknowledged (no answer after the re-send, an unreadable answer, or a failed end-run): ${flushNotAcknowledged.join(", ")}`);
|
|
1521
|
+
if (flushError.length)
|
|
1522
|
+
deps.error(`::error::flush answered error (data not uploaded): ${flushError.join(", ")}`);
|
|
1523
|
+
if (flushOff.length)
|
|
1524
|
+
deps.error(`::error::flush answered off in a run with a token (upload not configured, nothing uploaded): ${flushOff.join(", ")}`);
|
|
1525
|
+
endedAt = deps.now();
|
|
1526
|
+
await driver.end();
|
|
1527
|
+
await readDetail();
|
|
1528
|
+
const failed = !!(error || flushNotAcknowledged.length || flushError.length || flushOff.length);
|
|
1529
|
+
if (terminating)
|
|
1530
|
+
return EXIT_CODE[signal ?? "SIGTERM"];
|
|
1531
|
+
writeBuild(error?.message ?? null);
|
|
1532
|
+
status = failed ? "error" : "done";
|
|
1533
|
+
persist();
|
|
1534
|
+
deps.print(`driver-log.json: ${log.length} commands, ${iterations.length} iterations, ${Math.round((endedAt - startedAt) / 1000)} s`);
|
|
1535
|
+
for (const r of iterations)
|
|
1536
|
+
deps.print(` ${r.journey} #${r.iteration}: ${r.total_ms ?? "?"} ms, flushed=${r.flushed}`);
|
|
1537
|
+
return failed ? 1 : 0;
|
|
1538
|
+
} catch (e) {
|
|
1539
|
+
if (!terminating) {
|
|
1540
|
+
status = "error";
|
|
1541
|
+
error ??= e instanceof Error ? e : new Error(String(e));
|
|
1542
|
+
endedAt = deps.now();
|
|
1543
|
+
progress();
|
|
1544
|
+
}
|
|
1545
|
+
throw e;
|
|
1546
|
+
} finally {
|
|
1547
|
+
for (const [sig, handler] of handlers)
|
|
1548
|
+
proc?.off(sig, handler);
|
|
1549
|
+
}
|
|
1303
1550
|
}
|
|
1304
|
-
async function runIterations(t, module, ctx, iterations) {
|
|
1551
|
+
async function runIterations(t, module, ctx, iterations, progress) {
|
|
1305
1552
|
const { driver } = t;
|
|
1306
1553
|
for (const it of plan(ctx.journeys, ctx.repeat, ctx.tokens)) {
|
|
1307
1554
|
const rec = { journey: it.journey, iteration: it.iteration, token: it.token !== "" };
|
|
1308
1555
|
iterations.push(rec);
|
|
1556
|
+
progress();
|
|
1309
1557
|
driver.context = { journey: it.journey, iteration: it.iteration, phase: "begin" };
|
|
1310
1558
|
const t0 = t.now();
|
|
1311
1559
|
rec.alert = await t.beginRun({ token: it.token, home: module.home, ...module.sdkArgs ? { args: module.sdkArgs } : {} });
|
|
@@ -1321,8 +1569,10 @@ async function runIterations(t, module, ctx, iterations) {
|
|
|
1321
1569
|
rec.ready = { n: waits.length, ms: waits.reduce((x, y) => x + y, 0) };
|
|
1322
1570
|
driver.context = { ...driver.context, phase: "end" };
|
|
1323
1571
|
rec.flushed = await t.endRun();
|
|
1572
|
+
rec.flush = t.lastFlush;
|
|
1324
1573
|
const t3 = t.now();
|
|
1325
1574
|
Object.assign(rec, { t_start: t0, begin_ms: t1 - t0, journey_ms: t2 - t1, end_ms: t3 - t2, total_ms: t3 - t0 });
|
|
1575
|
+
progress();
|
|
1326
1576
|
}
|
|
1327
1577
|
}
|
|
1328
1578
|
|