@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 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`) makes "More work" exit `3`. The CLI exits `2` for a configuration problem (bad or missing `keepsnappy.yml`, 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; 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:
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` on "More work", `2` for configuration, `1` when the API fails or nothing could be
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
@@ -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.16";
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
- deps.error(`::error::flush answered error (data not uploaded): ${flushError.join(", ")}`);
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();