bitfab 0.64.3 → 0.64.4

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/cloudReplay.py CHANGED
@@ -53,6 +53,14 @@ ITEM_ERROR_FIELDS = (
53
53
  ITEM_ID_FIELDS = ("originalTraceId", "original_trace_id", "traceId", "trace_id")
54
54
  ITEM_ERROR_LINES = 20
55
55
  ITEM_ERROR_LENGTH = 500
56
+ SUMMARY_ERROR_ITEMS = 100
57
+ EXPERIMENT_FIELDS = {
58
+ "experimentId": ("experimentId", "experiment_id"),
59
+ "experimentUrl": ("experimentUrl", "experiment_url"),
60
+ "testRunId": ("testRunId", "test_run_id"),
61
+ "testRunUrl": ("testRunUrl", "test_run_url"),
62
+ "attempts": ("attempts",),
63
+ }
56
64
  UUID = re.compile(r"^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$")
57
65
  SHA = re.compile(r"^[0-9a-f]{40}$")
58
66
  GITHUB_REMOTE = re.compile(
@@ -60,6 +68,15 @@ GITHUB_REMOTE = re.compile(
60
68
  )
61
69
  LOG_TIMESTAMP = re.compile(r"^?\d{4}-\d\d-\d\dT[\d:.]+Z ?")
62
70
  EXPERIMENT_LINE = re.compile(rb"^\[replay\] Experiment ([0-9a-f-]{36}):")
71
+ PROGRESS_PREFIX = b"@@bitfab:progress "
72
+ PROGRESS_PAYLOAD_FIELDS = (
73
+ "input",
74
+ "result",
75
+ "originalOutput",
76
+ "original_output",
77
+ "selectiveReplay",
78
+ "selective_replay",
79
+ )
63
80
  SECRET_REFERENCE = re.compile(
64
81
  r"[\"']?([A-Za-z_][A-Za-z0-9_]*)[\"']?[ \t]*:[ \t]*[\"']?\$\{\{\s*secrets(?:\.([A-Za-z_][A-Za-z0-9_]*)|\[\s*[\"']([A-Za-z_][A-Za-z0-9_]*)[\"']\s*\])\s*\}\}"
65
82
  )
@@ -78,7 +95,7 @@ FOLLOW_FLAGS = (
78
95
  "--cloud-cleanup",
79
96
  )
80
97
  CLOUD_VALUE_FLAGS = (*FOLLOW_FLAGS, "--cloud-request-id", "--cloud-timeout")
81
- CLOUD_SWITCHES = ("--cloud", "--cloud-preview", "--cloud-detach")
98
+ CLOUD_SWITCHES = ("--cloud", "--cloud-preview", "--cloud-detach", "--cloud-full-output")
82
99
  RENAMED_FLAGS = {
83
100
  "--cloud-dry-run": "--cloud-preview",
84
101
  "--cloud-check": "--dry-run",
@@ -114,9 +131,17 @@ does not ignore) from the same directory, and this command prints the replay's o
114
131
  and exits with its exit code. Credential-like files are refused. Requires git and
115
132
  Python 3.10+ on macOS or Linux, with a github.com origin.
116
133
 
134
+ The replay's stderr comes back as it printed it. Its stdout result comes back as a
135
+ short summary instead of every item: the experiment ID and URL, how many traces were
136
+ replayed, same, changed, and errored, each errored trace with its error, and the exit
137
+ code. Every item's input and output are already saved in the experiment in Bitfab.
138
+
117
139
  Options added by --cloud:
118
140
  --cloud-preview List what the snapshot would contain and push nothing.
119
141
  --cloud-detach Return after dispatch instead of waiting.
142
+ --cloud-full-output Print the replay's whole stdout result, every item included,
143
+ instead of the summary. Large runs can exceed what the
144
+ GitHub job log holds.
120
145
  --cloud-timeout MIN Stop the replay after MIN minutes (1..7200).
121
146
  --cloud-request-id ID Recover a submission whose response was lost.
122
147
 
@@ -648,6 +673,8 @@ def replay_request(root, parsed):
648
673
  }
649
674
  if "--cloud-timeout" in parsed["cloud"]:
650
675
  request["timeoutMinutes"] = int(parsed["cloud"]["--cloud-timeout"])
676
+ if "--cloud-full-output" in parsed["switches"]:
677
+ request["fullOutput"] = True
651
678
  return request, files
652
679
 
653
680
 
@@ -871,9 +898,7 @@ def outcome(record):
871
898
  raise ValueError("Replay result does not match this execution")
872
899
  record["exitCode"] = result["exitCode"]
873
900
  parsed = last_json(result.get("stdout", "")) or {}
874
- test_run = result.get("testRunId") or parsed.get(
875
- "testRunId", parsed.get("test_run_id")
876
- )
901
+ test_run = result.get("testRunId") or experiment_fields(parsed).get("testRunId")
877
902
  if isinstance(test_run, str) and UUID.fullmatch(test_run):
878
903
  record["testRunId"] = test_run
879
904
  if result.get("stoppedEarly"):
@@ -903,6 +928,17 @@ def print_outcome(found):
903
928
  f"The replay also printed {result['stdoutOmittedBytes'] // (1024 * 1024)} MiB to stdout before its result, such as a library's logging; only the end was kept"
904
929
  )
905
930
  print(result.get("stdout", ""), end="", flush=True)
931
+ summary = last_json(result.get("stdout", "")) or {}
932
+ if summary.get("cloudSummary") is True:
933
+ progress(
934
+ "Printed a summary of the result; every item's input and output are in the experiment"
935
+ + (
936
+ f" at {summary['experimentUrl']}"
937
+ if summary.get("experimentUrl")
938
+ else ""
939
+ )
940
+ + ". Add --cloud-full-output to print every item here instead"
941
+ )
906
942
  if record.get("stoppedEarly"):
907
943
  progress(
908
944
  f"Replay stopped early: {record['stoppedEarly']}. Traces that finished are saved"
@@ -1203,7 +1239,11 @@ def execute():
1203
1239
  print(OUTPUT_BEGIN, flush=True)
1204
1240
  try:
1205
1241
  code, stdout, omitted, stopped = run_command(
1206
- directory, args, request.get("timeoutMinutes"), experiment
1242
+ directory,
1243
+ args,
1244
+ request.get("timeoutMinutes"),
1245
+ experiment,
1246
+ summarize=request.get("fullOutput") is not True,
1207
1247
  )
1208
1248
  finally:
1209
1249
  print(OUTPUT_END, flush=True)
@@ -1286,10 +1326,75 @@ def item_errors(items):
1286
1326
  text = " ".join(str(error).split())
1287
1327
  if len(text) > ITEM_ERROR_LENGTH:
1288
1328
  text = text[:ITEM_ERROR_LENGTH] + "..."
1289
- errors.append((trace, text))
1329
+ errored = {"originalTraceId": trace, "error": text}
1330
+ if isinstance(item.get("attempt"), int):
1331
+ errored["attempt"] = item["attempt"]
1332
+ errors.append(errored)
1290
1333
  return errors
1291
1334
 
1292
1335
 
1336
+ def item_field(item, camel, snake):
1337
+ return item[camel] if camel in item else item.get(snake)
1338
+
1339
+
1340
+ def canonical(value):
1341
+ return json.dumps(value, separators=(",", ":"))
1342
+
1343
+
1344
+ def experiment_fields(parsed):
1345
+ fields = {}
1346
+ for name, spellings in EXPERIMENT_FIELDS.items():
1347
+ value = next(
1348
+ (parsed[key] for key in spellings if parsed.get(key) is not None), None
1349
+ )
1350
+ if value is not None:
1351
+ fields[name] = value
1352
+ return fields
1353
+
1354
+
1355
+ def replay_summary(parsed):
1356
+ items = [item for item in parsed["items"] if isinstance(item, dict)]
1357
+ replayed = [item for item in items if not carried_over(item)]
1358
+ errors = item_errors(replayed)
1359
+ counts = {
1360
+ "replayed": len(replayed),
1361
+ "carriedOver": len(items) - len(replayed),
1362
+ "same": 0,
1363
+ "changed": 0,
1364
+ "matchedExpected": 0,
1365
+ "missedExpected": 0,
1366
+ "errored": len(errors),
1367
+ }
1368
+ for item in replayed:
1369
+ if item_errored(item):
1370
+ continue
1371
+ equal = canonical(item.get("result")) == canonical(
1372
+ item_field(item, "originalOutput", "original_output")
1373
+ )
1374
+ if item_field(item, "ingestionType", "ingestion_type") == "seeded":
1375
+ counts["matchedExpected" if equal else "missedExpected"] += 1
1376
+ else:
1377
+ counts["same" if equal else "changed"] += 1
1378
+ summary = {
1379
+ "cloudSummary": True,
1380
+ **experiment_fields(parsed),
1381
+ "counts": counts,
1382
+ "erroredItems": errors[:SUMMARY_ERROR_ITEMS],
1383
+ }
1384
+ if len(errors) > SUMMARY_ERROR_ITEMS:
1385
+ summary["erroredItemsOmitted"] = len(errors) - SUMMARY_ERROR_ITEMS
1386
+ return summary
1387
+
1388
+
1389
+ def result_summary(stdout):
1390
+ parsed = last_json(stdout) or {}
1391
+ if parsed.get("cloudSummary") is True:
1392
+ return parsed
1393
+ if isinstance(parsed.get("items"), list):
1394
+ return replay_summary(parsed)
1395
+ return None
1396
+
1397
+
1293
1398
  def item_errored(item):
1294
1399
  return isinstance(item, dict) and any(
1295
1400
  item.get(field) is not None for field in ITEM_ERROR_FIELDS
@@ -1306,8 +1411,24 @@ def raise_interrupt(signum, frame):
1306
1411
  raise KeyboardInterrupt
1307
1412
 
1308
1413
 
1309
- def forward_stderr(stream, experiment):
1414
+ def without_item_payload(line):
1415
+ try:
1416
+ event = json.loads(line[len(PROGRESS_PREFIX) :])
1417
+ except ValueError:
1418
+ return line
1419
+ item = event.get("item") if isinstance(event, dict) else None
1420
+ if not isinstance(item, dict):
1421
+ return line
1422
+ event["item"] = {
1423
+ key: value for key, value in item.items() if key not in PROGRESS_PAYLOAD_FIELDS
1424
+ }
1425
+ return PROGRESS_PREFIX + json.dumps(event).encode() + b"\n"
1426
+
1427
+
1428
+ def forward_stderr(stream, experiment, summarize):
1310
1429
  for line in iter(stream.readline, b""):
1430
+ if summarize and line.startswith(PROGRESS_PREFIX):
1431
+ line = without_item_payload(line)
1311
1432
  sys.stdout.write(line.decode(errors="replace"))
1312
1433
  sys.stdout.flush()
1313
1434
  if "id" not in experiment:
@@ -1333,6 +1454,15 @@ def last_json(text):
1333
1454
  return trailing_json(text)[1]
1334
1455
 
1335
1456
 
1457
+ def relayed_stdout(output, code, summarize):
1458
+ if summarize:
1459
+ output.seek(0)
1460
+ parsed = last_json(output.read().decode(errors="replace"))
1461
+ if isinstance(parsed, dict) and isinstance(parsed.get("items"), list):
1462
+ return json.dumps({**replay_summary(parsed), "exitCode": code}) + "\n", 0
1463
+ return carried_stdout(output)
1464
+
1465
+
1336
1466
  def carried_stdout(output):
1337
1467
  start = max(0, os.fstat(output.fileno()).st_size - OUTPUT_LIMIT)
1338
1468
  output.seek(start)
@@ -1365,34 +1495,34 @@ def annotate(result):
1365
1495
 
1366
1496
 
1367
1497
  def summary_markdown(result):
1368
- parsed = last_json(result["stdout"]) or {}
1369
- test_run = result.get("testRunId") or parsed.get(
1370
- "testRunId", parsed.get("test_run_id")
1371
- )
1372
- items = parsed.get("items") if isinstance(parsed.get("items"), list) else []
1373
- replayed = [item for item in items if not carried_over(item)]
1374
- errors = item_errors(replayed)
1498
+ summary = result_summary(result["stdout"]) or {}
1499
+ counts = summary.get("counts", {})
1500
+ test_run = result.get("testRunId") or summary.get("testRunId")
1501
+ errors = summary.get("erroredItems", [])
1375
1502
  lines = [f"Commit: `{result['commitSha']}`", f"Exit code: {result['exitCode']}"]
1376
1503
  if test_run:
1377
1504
  lines.insert(0, f"Test run: `{test_run}`")
1378
1505
  if result.get("stoppedEarly"):
1379
1506
  lines.append(f"Stopped early: {result['stoppedEarly']}")
1380
- elif items:
1381
- lines.append(f"Replayed: {len(replayed)}, errored: {len(errors)}")
1507
+ elif counts.get("replayed") or counts.get("carriedOver"):
1508
+ lines.append(
1509
+ f"Replayed: {counts['replayed']}, same: {counts['same']}, changed: {counts['changed']}, errored: {counts['errored']}"
1510
+ )
1382
1511
  text = "### Bitfab replay\n\n" + "\n\n".join(lines) + "\n"
1383
1512
  if errors:
1384
1513
  shown = [
1385
- f"trace {trace}: {error}" for trace, error in errors[:ITEM_ERROR_LINES]
1514
+ f"trace {error['originalTraceId']}: {error['error']}"
1515
+ for error in errors[:ITEM_ERROR_LINES]
1386
1516
  ]
1387
- if len(errors) > len(shown):
1388
- shown.append(f"and {len(errors) - len(shown)} more errored items")
1517
+ if counts.get("errored", 0) > len(shown):
1518
+ shown.append(f"and {counts['errored'] - len(shown)} more errored items")
1389
1519
  text += "\n#### Errored items\n\n" + "".join(
1390
1520
  f"- `{line.replace('`', chr(39))}`\n" for line in shown
1391
1521
  )
1392
1522
  return text
1393
1523
 
1394
1524
 
1395
- def run_command(directory, args, timeout, experiment):
1525
+ def run_command(directory, args, timeout, experiment, *, summarize=True):
1396
1526
  stopped = None
1397
1527
  with tempfile.TemporaryFile() as output:
1398
1528
  with subprocess.Popen(
@@ -1404,7 +1534,9 @@ def run_command(directory, args, timeout, experiment):
1404
1534
  start_new_session=True,
1405
1535
  ) as child:
1406
1536
  reader = threading.Thread(
1407
- target=forward_stderr, args=(child.stderr, experiment), daemon=True
1537
+ target=forward_stderr,
1538
+ args=(child.stderr, experiment, summarize),
1539
+ daemon=True,
1408
1540
  )
1409
1541
  reader.start()
1410
1542
  try:
@@ -1431,9 +1563,9 @@ def run_command(directory, args, timeout, experiment):
1431
1563
  raise
1432
1564
  finally:
1433
1565
  reader.join(timeout=5)
1434
- code = child.returncode
1435
- text, omitted = carried_stdout(output)
1436
- return (code if code >= 0 else 1), text, omitted, stopped
1566
+ code = child.returncode if child.returncode >= 0 else 1
1567
+ text, omitted = relayed_stdout(output, code, summarize)
1568
+ return code, text, omitted, stopped
1437
1569
 
1438
1570
 
1439
1571
  def stop(child):
@@ -6,7 +6,7 @@ import {
6
6
  replayContextReady,
7
7
  runWithReplayContext,
8
8
  unrefTimer
9
- } from "./chunk-JIPKI6RB.js";
9
+ } from "./chunk-ZPVUSLIU.js";
10
10
  import {
11
11
  runWithCaptureSuppressed,
12
12
  warnOnce,
@@ -1884,4 +1884,4 @@ export {
1884
1884
  warnJudgeAssertionsDeprecated,
1885
1885
  replay
1886
1886
  };
1887
- //# sourceMappingURL=chunk-2YDH4SPS.js.map
1887
+ //# sourceMappingURL=chunk-I6B5R2FP.js.map
@@ -2,7 +2,7 @@ import {
2
2
  BitfabError,
3
3
  warnOnce,
4
4
  writeOnce
5
- } from "./chunk-RC5FXV76.js";
5
+ } from "./chunk-XASGXGQN.js";
6
6
 
7
7
  // src/codeChange.ts
8
8
  var NUL = String.fromCharCode(0);
@@ -316,7 +316,7 @@ async function seedFromRegistry(registry, pipeline, cases, options = {}) {
316
316
  sessionId: seedCase.sessionId
317
317
  })
318
318
  );
319
- const { flushTraces: flushTraces2 } = await import("./http-D5YRJ57X.js");
319
+ const { flushTraces: flushTraces2 } = await import("./http-Y6T6T2AP.js");
320
320
  await flushTraces2(3e4);
321
321
  return { pipeline, traceFunctionKey, traceIds };
322
322
  }
@@ -1007,4 +1007,4 @@ export {
1007
1007
  runRegistryMain,
1008
1008
  runIfEntrypoint
1009
1009
  };
1010
- //# sourceMappingURL=chunk-XNJ5AGAF.js.map
1010
+ //# sourceMappingURL=chunk-OEHQPSOB.js.map
@@ -17,7 +17,7 @@ import {
17
17
  serializeValue,
18
18
  toJsonSafe,
19
19
  toJsonSafeReport
20
- } from "./chunk-2YDH4SPS.js";
20
+ } from "./chunk-I6B5R2FP.js";
21
21
  import {
22
22
  BitfabError,
23
23
  CONTENT_OFF_KEY,
@@ -36,7 +36,7 @@ import {
36
36
  readEnv,
37
37
  recordCallerTraceMetadata,
38
38
  retireTraceMetadata
39
- } from "./chunk-JIPKI6RB.js";
39
+ } from "./chunk-ZPVUSLIU.js";
40
40
  import {
41
41
  __privateAdd,
42
42
  __privateGet,
@@ -6253,7 +6253,7 @@ var Bitfab = class {
6253
6253
  await runWithSeedContext({ traceId }, async () => target(...args));
6254
6254
  } finally {
6255
6255
  try {
6256
- const { flushTraces: flushTraces2 } = await import("./http-KBTWSYL7.js");
6256
+ const { flushTraces: flushTraces2 } = await import("./http-53RWIBXJ.js");
6257
6257
  await flushTraces2(3e4);
6258
6258
  } finally {
6259
6259
  unrecorded = activeTraceStates.delete(traceId);
@@ -6312,7 +6312,7 @@ var Bitfab = class {
6312
6312
  `Function is wrapped with trace function key '${wrappedKey}' but replay was called with '${traceFunctionKey}'. Pass matching keys, or pass the unwrapped function to replay it under the explicit key.`
6313
6313
  );
6314
6314
  }
6315
- const { replay: doReplay } = await import("./replay-PFHS24KV.js");
6315
+ const { replay: doReplay } = await import("./replay-SIKHFPKA.js");
6316
6316
  return doReplay(
6317
6317
  this.httpClient,
6318
6318
  this.serviceUrl,
@@ -6658,7 +6658,7 @@ async function seedFromRegistry(registry, pipeline, cases, options = {}) {
6658
6658
  sessionId: seedCase.sessionId
6659
6659
  })
6660
6660
  );
6661
- const { flushTraces: flushTraces2 } = await import("./http-KBTWSYL7.js");
6661
+ const { flushTraces: flushTraces2 } = await import("./http-53RWIBXJ.js");
6662
6662
  await flushTraces2(3e4);
6663
6663
  return { pipeline, traceFunctionKey, traceIds };
6664
6664
  }
@@ -6703,4 +6703,4 @@ export {
6703
6703
  reseedFromRegistry,
6704
6704
  seedFromRegistry
6705
6705
  };
6706
- //# sourceMappingURL=chunk-UZU2PNO7.js.map
6706
+ //# sourceMappingURL=chunk-UMZDRAAK.js.map
@@ -181,7 +181,7 @@ function encodeRequestBody(body) {
181
181
  }
182
182
 
183
183
  // src/version.generated.ts
184
- var __version__ = "0.64.3";
184
+ var __version__ = "0.64.4";
185
185
  var __packageName__ = "bitfab";
186
186
 
187
187
  // src/errors.ts
@@ -2500,4 +2500,4 @@ export {
2500
2500
  HttpClient,
2501
2501
  experimentIdWireFields
2502
2502
  };
2503
- //# sourceMappingURL=chunk-RC5FXV76.js.map
2503
+ //# sourceMappingURL=chunk-XASGXGQN.js.map