@keepsnappy/agent 0.1.0-alpha.16 → 0.1.0-alpha.17
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 +3 -2
- package/dist/appium-driver.js +71 -6
- package/dist/keepsnappy.js +716 -194
- package/examples/farm/workflows/keepsnappy-measure.yml +3 -1
- package/examples/github-actions.yml +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Annotated example: `examples/keepsnappy.yml`. Every setting:
|
|
|
70
70
|
|
|
71
71
|
Unknown settings and wrong types are errors, all listed together.
|
|
72
72
|
|
|
73
|
-
Results and exit codes: results never change the exit code by default (ruling D9, no blocking PR checks): `0` also for "More work". `--fail-on-worse` (needs `--wait`)
|
|
73
|
+
Results and exit codes: results never change the exit code by default (ruling D9, no blocking PR checks): `0` also for "More work". `--fail-on-worse` (needs `--wait`) fails the job (exit `3`) only when the verdict computed on that run set is Worse and blocking. Closing a run set never computes its verdict (decision 0031: the operator computes verdicts on demand), so with no computed verdict `--fail-on-worse` checks nothing: the CLI prints `Verdict not computed yet (computed on demand): --fail-on-worse checked nothing` and exits `0`. The CLI exits `2` for a configuration problem (bad or missing `keepsnappy.yml`, unknown or misspelt flags, a glob matching nothing, `--wait` without a branch or key) and `1` for an infrastructure problem (cannot read the device log, an API request failing, an API URL that does not answer as the KeepSnappy API, `--upload` that sent no run: "Blocked"; results stay in the out dir). Failed runs are marked invalid in `runs.ndjson` / `metrics.csv` and ignored by the statistics. The ending summary reads like:
|
|
74
74
|
|
|
75
75
|
```
|
|
76
76
|
7/8 runs valid (dropped: 1 test-failed: exit 1)
|
|
@@ -145,7 +145,8 @@ runner. It writes:
|
|
|
145
145
|
|
|
146
146
|
Running it again is safe: finished runs are left alone, the set is not reprocessed, the same result is printed. A batch
|
|
147
147
|
that arrives after a run was finished is added by the server and the run re-processed at once. Exit codes: `0` for any
|
|
148
|
-
result, `3` with `--fail-on-worse`
|
|
148
|
+
result, `3` with `--fail-on-worse` when the verdict computed on that run set is Worse and blocking (with no computed
|
|
149
|
+
verdict it checks nothing and says so, exit `0`), `2` for configuration, `1` when the API fails or nothing could be
|
|
149
150
|
judged ("Blocked": no iteration sent any data).
|
|
150
151
|
|
|
151
152
|
Slack: set `KEEPSNAPPY_SLACK_WEBHOOK` (an incoming-webhook URL kept as a CI secret) and `farm close --wait` posts the
|
package/dist/appium-driver.js
CHANGED
|
@@ -303,6 +303,10 @@ class WdError extends Error {
|
|
|
303
303
|
this.wdError = wdError;
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
|
+
var IOS_BOTTOM_INSET_PT = 34;
|
|
307
|
+
var GESTURE_ZONE_MARGIN_PT = 16;
|
|
308
|
+
var ANDROID_NAV_BAR_FALLBACK_DP = 48;
|
|
309
|
+
var GESTURE_ZONE_MARGIN_DP = 16;
|
|
306
310
|
|
|
307
311
|
class Driver {
|
|
308
312
|
bundleId;
|
|
@@ -317,6 +321,8 @@ class Driver {
|
|
|
317
321
|
deviceUnit;
|
|
318
322
|
context = {};
|
|
319
323
|
viewport = null;
|
|
324
|
+
pxPerDp = null;
|
|
325
|
+
bottomLimit = null;
|
|
320
326
|
rects = new Map;
|
|
321
327
|
constructor(o) {
|
|
322
328
|
this.bundleId = o.bundleId;
|
|
@@ -381,6 +387,29 @@ class Driver {
|
|
|
381
387
|
this.viewport ??= await this.cmd("GET", "/window/rect");
|
|
382
388
|
return this.viewport;
|
|
383
389
|
}
|
|
390
|
+
async density() {
|
|
391
|
+
if (this.pxPerDp === null) {
|
|
392
|
+
const dpi = Number(await this.execute("mobile: getDisplayDensity"));
|
|
393
|
+
if (!(dpi > 0))
|
|
394
|
+
throw new Error(`mobile: getDisplayDensity answered ${dpi}, not a density`);
|
|
395
|
+
this.pxPerDp = dpi / 160;
|
|
396
|
+
}
|
|
397
|
+
return this.pxPerDp;
|
|
398
|
+
}
|
|
399
|
+
async safeBottomY() {
|
|
400
|
+
if (this.bottomLimit === null) {
|
|
401
|
+
const { height } = await this.screen();
|
|
402
|
+
if (this.platform.name === "ios") {
|
|
403
|
+
this.bottomLimit = height - IOS_BOTTOM_INSET_PT - GESTURE_ZONE_MARGIN_PT;
|
|
404
|
+
} else {
|
|
405
|
+
const dp = await this.density();
|
|
406
|
+
const bar = await this.execute("mobile: getSystemBars").then((b) => Number(b?.navigationBar?.height), () => NaN);
|
|
407
|
+
const inset = bar > 0 ? bar : ANDROID_NAV_BAR_FALLBACK_DP * dp;
|
|
408
|
+
this.bottomLimit = Math.round(height - inset - GESTURE_ZONE_MARGIN_DP * dp);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return this.bottomLimit;
|
|
412
|
+
}
|
|
384
413
|
async onScreen(el, full = false) {
|
|
385
414
|
let r;
|
|
386
415
|
try {
|
|
@@ -479,6 +508,7 @@ class Driver {
|
|
|
479
508
|
await this.cmd("DELETE", "/actions").catch(() => null);
|
|
480
509
|
}
|
|
481
510
|
async drag(from, to, durationMs) {
|
|
511
|
+
to = { x: to.x, y: Math.min(to.y, await this.safeBottomY()) };
|
|
482
512
|
await this.cmd("POST", "/actions", {
|
|
483
513
|
actions: [
|
|
484
514
|
{
|
|
@@ -676,7 +706,10 @@ function sessionCaps(i) {
|
|
|
676
706
|
sessionName: i.sessionName,
|
|
677
707
|
...tunnel,
|
|
678
708
|
idleTimeout: 300,
|
|
679
|
-
appiumVersion: i.appiumVersion
|
|
709
|
+
appiumVersion: i.appiumVersion,
|
|
710
|
+
video: i.capture ?? false,
|
|
711
|
+
deviceLogs: i.capture ?? false,
|
|
712
|
+
networkLogs: false
|
|
680
713
|
}
|
|
681
714
|
};
|
|
682
715
|
}
|
|
@@ -693,7 +726,7 @@ import { dirname, join, relative, resolve } from "node:path";
|
|
|
693
726
|
import { fileURLToPath } from "node:url";
|
|
694
727
|
var DRIVER_NAME = "appium";
|
|
695
728
|
function driverVersion() {
|
|
696
|
-
const built = "0.1.0-alpha.
|
|
729
|
+
const built = "0.1.0-alpha.17";
|
|
697
730
|
if (built)
|
|
698
731
|
return built;
|
|
699
732
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -824,6 +857,26 @@ var FLUSH_POLL_MS = 500;
|
|
|
824
857
|
var FLUSH_RESEND_MS = 90000;
|
|
825
858
|
var ACTIVATION_SETTLE_MS = 2000;
|
|
826
859
|
var FLUSH_NOT_ACKNOWLEDGED = "not acknowledged (re-sent once)";
|
|
860
|
+
var flushLoss = (detail) => {
|
|
861
|
+
if (!detail)
|
|
862
|
+
return "no detail";
|
|
863
|
+
const count = (re) => Number(re.exec(detail)?.[1] ?? 0);
|
|
864
|
+
const lost = [];
|
|
865
|
+
const rejected = count(/^rejected \d+: (\d+) batch/);
|
|
866
|
+
if (!detail.startsWith("batches=") && !rejected && !detail.startsWith("lines of the run dropped before sending")) {
|
|
867
|
+
lost.push(`lines not sent (${detail.replace(/ (?:dropped|artifacts)=.*$/, "")})`);
|
|
868
|
+
}
|
|
869
|
+
if (count(/ dropped=(\d+)/))
|
|
870
|
+
lost.push(`lines dropped=${count(/ dropped=(\d+)/)}`);
|
|
871
|
+
if (rejected)
|
|
872
|
+
lost.push(`line batches rejected=${rejected}`);
|
|
873
|
+
const files = /failed=(\d+) pending=(\d+) local=(\d+) rejected=(\d+)/.exec(detail);
|
|
874
|
+
if (files)
|
|
875
|
+
lost.push(`profile files not uploaded=${files.slice(1).reduce((n, x) => n + Number(x), 0)}`);
|
|
876
|
+
if (count(/ lost stream=(\d+)/))
|
|
877
|
+
lost.push(`stream segments lost=${count(/ lost stream=(\d+)/)}`);
|
|
878
|
+
return lost.length ? lost.join(", ") : detail;
|
|
879
|
+
};
|
|
827
880
|
var FLUSH_UNREADABLE = "not acknowledged (unreadable answer)";
|
|
828
881
|
var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
|
|
829
882
|
var UNREADABLE_POLLS = 3;
|
|
@@ -1357,7 +1410,8 @@ var DRIVER_OPTIONS = {
|
|
|
1357
1410
|
out: { type: "string", default: "farm-run" },
|
|
1358
1411
|
platform: { type: "string", default: "ios" },
|
|
1359
1412
|
"action-gc": { type: "string", default: "off" },
|
|
1360
|
-
"end-gc": { type: "string", default: END_GC_DEFAULT ? "on" : "off" }
|
|
1413
|
+
"end-gc": { type: "string", default: END_GC_DEFAULT ? "on" : "off" },
|
|
1414
|
+
capture: { type: "string", default: "off" }
|
|
1361
1415
|
};
|
|
1362
1416
|
function parseDriverArgs(argv, module) {
|
|
1363
1417
|
const values = parseArgs({ args: [...argv], options: { ...module.options ?? {}, ...DRIVER_OPTIONS } }).values;
|
|
@@ -1397,6 +1451,9 @@ async function runDriver(argv, module, deps = realRunDeps()) {
|
|
|
1397
1451
|
throw new Error("--action-gc must be on or off");
|
|
1398
1452
|
if (a["end-gc"] !== "on" && a["end-gc"] !== "off")
|
|
1399
1453
|
throw new Error("--end-gc must be on or off");
|
|
1454
|
+
if (a.capture !== "on" && a.capture !== "off")
|
|
1455
|
+
throw new Error("--capture must be on or off");
|
|
1456
|
+
const capture = a.capture === "on";
|
|
1400
1457
|
const out = String(a.out);
|
|
1401
1458
|
const auth = basicAuth(user, key);
|
|
1402
1459
|
mkdirSync(out, { recursive: true });
|
|
@@ -1427,6 +1484,8 @@ async function runDriver(argv, module, deps = realRunDeps()) {
|
|
|
1427
1484
|
driver: "appium",
|
|
1428
1485
|
status,
|
|
1429
1486
|
device,
|
|
1487
|
+
unit: driver.deviceUnit ?? null,
|
|
1488
|
+
capture,
|
|
1430
1489
|
appUrl,
|
|
1431
1490
|
session: driver.sessionId,
|
|
1432
1491
|
buildId,
|
|
@@ -1531,13 +1590,14 @@ async function runDriver(argv, module, deps = realRunDeps()) {
|
|
|
1531
1590
|
try {
|
|
1532
1591
|
appUrl = await resolveApp(String(a.app), String(a["app-custom-id"] ?? ""), auth, deps.fetch, platform);
|
|
1533
1592
|
deps.print(`app: ${appUrl}`);
|
|
1534
|
-
const caps = sessionCaps({ appUrl, device, buildName: String(a["build-name"]), sessionName, appiumVersion: String(a["appium-version"]), platform, localIdentifier: a["local-identifier"] });
|
|
1593
|
+
const caps = sessionCaps({ appUrl, device, buildName: String(a["build-name"]), sessionName, appiumVersion: String(a["appium-version"]), platform, localIdentifier: a["local-identifier"], capture });
|
|
1535
1594
|
startedAt = deps.now();
|
|
1536
1595
|
try {
|
|
1537
1596
|
await driver.start(caps);
|
|
1538
1597
|
deps.print(`session ${driver.sessionId}`);
|
|
1539
1598
|
progress();
|
|
1540
1599
|
await driver.cmd("POST", "/appium/settings", { settings: platform.settings }).catch(() => null);
|
|
1600
|
+
await driver.safeBottomY().catch(() => null);
|
|
1541
1601
|
await module.setup?.(t, ctx);
|
|
1542
1602
|
await runIterations(t, module, ctx, iterations, progress);
|
|
1543
1603
|
} catch (e) {
|
|
@@ -1569,8 +1629,13 @@ async function runDriver(argv, module, deps = realRunDeps()) {
|
|
|
1569
1629
|
flushOff = flushedAs(FLUSH_OFF, true);
|
|
1570
1630
|
if (flushNotAcknowledged.length)
|
|
1571
1631
|
deps.error(`::error::flush not acknowledged (no answer after the re-send, an unreadable answer, or a failed end-run): ${flushNotAcknowledged.join(", ")}`);
|
|
1572
|
-
if (flushError.length)
|
|
1573
|
-
|
|
1632
|
+
if (flushError.length) {
|
|
1633
|
+
const losses = [
|
|
1634
|
+
...iterations.filter((r) => r.flushed === FLUSH_ERROR).map((r) => `${r.journey} #${r.iteration} (${flushLoss(r.flush?.detail)})`),
|
|
1635
|
+
...finalFlush?.flushed === FLUSH_ERROR ? [`final flush (${flushLoss(finalFlush.flush?.detail)})`] : []
|
|
1636
|
+
];
|
|
1637
|
+
deps.error(`::error::flush answered error, data lost: ${losses.join(", ")}`);
|
|
1638
|
+
}
|
|
1574
1639
|
if (flushOff.length)
|
|
1575
1640
|
deps.error(`::error::flush answered off in a run with a token (upload not configured, nothing uploaded): ${flushOff.join(", ")}`);
|
|
1576
1641
|
endedAt = deps.now();
|