benchkit-cli 0.1.0__py3-none-any.whl

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.
Files changed (149) hide show
  1. benchkit/__init__.py +1 -0
  2. benchkit/_pi_proxy.py +219 -0
  3. benchkit/answer_key_guard.ts +49 -0
  4. benchkit/artifacts.py +87 -0
  5. benchkit/benchmarks/__init__.py +105 -0
  6. benchkit/benchmarks/aider_polyglot.py +405 -0
  7. benchkit/benchmarks/arc.py +52 -0
  8. benchkit/benchmarks/banking77.py +149 -0
  9. benchkit/benchmarks/base.py +19 -0
  10. benchkit/benchmarks/boolq.py +49 -0
  11. benchkit/benchmarks/chartqa.py +133 -0
  12. benchkit/benchmarks/code_verifier.py +77 -0
  13. benchkit/benchmarks/downloads.py +166 -0
  14. benchkit/benchmarks/evalplus.py +288 -0
  15. benchkit/benchmarks/git_surgery.py +485 -0
  16. benchkit/benchmarks/gpqa.py +56 -0
  17. benchkit/benchmarks/gsm8k.py +97 -0
  18. benchkit/benchmarks/hellaswag.py +49 -0
  19. benchkit/benchmarks/humaneval.py +103 -0
  20. benchkit/benchmarks/ifeval.py +56 -0
  21. benchkit/benchmarks/ifeval_instructions.py +477 -0
  22. benchkit/benchmarks/mbpp.py +60 -0
  23. benchkit/benchmarks/mc_arena.py +479 -0
  24. benchkit/benchmarks/mcq.py +94 -0
  25. benchkit/benchmarks/medfailbench.py +537 -0
  26. benchkit/benchmarks/mmlu.py +54 -0
  27. benchkit/benchmarks/mmlu_pro.py +62 -0
  28. benchkit/benchmarks/mmvp.py +115 -0
  29. benchkit/benchmarks/ocrbench.py +96 -0
  30. benchkit/benchmarks/openbookqa.py +52 -0
  31. benchkit/benchmarks/patcheval.py +929 -0
  32. benchkit/benchmarks/piqa.py +52 -0
  33. benchkit/benchmarks/ruler.py +172 -0
  34. benchkit/benchmarks/ruler_generator.py +535 -0
  35. benchkit/benchmarks/sanity.py +174 -0
  36. benchkit/benchmarks/treejs_arena.py +268 -0
  37. benchkit/benchmarks/truthfulqa.py +52 -0
  38. benchkit/benchmarks/utils.py +25 -0
  39. benchkit/benchmarks/vision.py +230 -0
  40. benchkit/benchmarks/vstar.py +108 -0
  41. benchkit/benchmarks/winogrande.py +52 -0
  42. benchkit/benchmarks/xstest.py +116 -0
  43. benchkit/browser.py +429 -0
  44. benchkit/catalog.py +121 -0
  45. benchkit/cli.py +1000 -0
  46. benchkit/client.py +1413 -0
  47. benchkit/controls.py +87 -0
  48. benchkit/datasets/MMLU_LICENSE.txt +21 -0
  49. benchkit/datasets/README.md +142 -0
  50. benchkit/datasets/arc_challenge.jsonl +1172 -0
  51. benchkit/datasets/banking77.jsonl +3080 -0
  52. benchkit/datasets/banking77_intents.json +79 -0
  53. benchkit/datasets/boolq.jsonl +3270 -0
  54. benchkit/datasets/code_verifier.jsonl +500 -0
  55. benchkit/datasets/gpqa_diamond.jsonl +198 -0
  56. benchkit/datasets/gsm8k.jsonl +1319 -0
  57. benchkit/datasets/hellaswag.jsonl +1000 -0
  58. benchkit/datasets/humaneval.jsonl +164 -0
  59. benchkit/datasets/ifeval.jsonl +541 -0
  60. benchkit/datasets/mbpp.jsonl +500 -0
  61. benchkit/datasets/mc_blocks_1_20_1.jsonl +1003 -0
  62. benchkit/datasets/medfailbench_hard30.jsonl +30 -0
  63. benchkit/datasets/medfailbench_hard30.md +13 -0
  64. benchkit/datasets/openbookqa.jsonl +500 -0
  65. benchkit/datasets/piqa.jsonl +1838 -0
  66. benchkit/datasets/sanity_code.jsonl +6 -0
  67. benchkit/datasets/truthfulqa.jsonl +817 -0
  68. benchkit/datasets/winogrande.jsonl +1267 -0
  69. benchkit/datasets/xstest.csv +451 -0
  70. benchkit/decision.py +194 -0
  71. benchkit/demo.py +451 -0
  72. benchkit/dockerfiles.py +247 -0
  73. benchkit/engine.py +1270 -0
  74. benchkit/evaluation.py +195 -0
  75. benchkit/events.py +141 -0
  76. benchkit/executor.py +96 -0
  77. benchkit/git_surgery/backport-release-stack/setup.sh +328 -0
  78. benchkit/git_surgery/backport-release-stack/verify.sh +119 -0
  79. benchkit/git_surgery/bisect-the-regression/setup.sh +49 -0
  80. benchkit/git_surgery/bisect-the-regression/verify.sh +34 -0
  81. benchkit/git_surgery/rebase-conflict-chain/setup.sh +65 -0
  82. benchkit/git_surgery/rebase-conflict-chain/verify.sh +43 -0
  83. benchkit/git_surgery/recover-complex-stash/setup.sh +152 -0
  84. benchkit/git_surgery/recover-complex-stash/verify.sh +95 -0
  85. benchkit/git_surgery/recover-lost-work/setup.sh +40 -0
  86. benchkit/git_surgery/recover-lost-work/verify.sh +36 -0
  87. benchkit/git_surgery/repair-force-pushed-remote/setup.sh +171 -0
  88. benchkit/git_surgery/repair-force-pushed-remote/verify.sh +83 -0
  89. benchkit/git_surgery/revert-merge-with-followups/setup.sh +198 -0
  90. benchkit/git_surgery/revert-merge-with-followups/verify.sh +98 -0
  91. benchkit/git_surgery/secret-in-history/README.md +27 -0
  92. benchkit/git_surgery/secret-in-history/setup.sh +107 -0
  93. benchkit/git_surgery/secret-in-history/verify.sh +106 -0
  94. benchkit/git_surgery/split-the-mega-commit/setup.sh +50 -0
  95. benchkit/git_surgery/split-the-mega-commit/verify.sh +48 -0
  96. benchkit/git_surgery/untangle-nested-submodules/setup.sh +193 -0
  97. benchkit/git_surgery/untangle-nested-submodules/verify.sh +90 -0
  98. benchkit/history.py +474 -0
  99. benchkit/jobs.py +332 -0
  100. benchkit/leakage.py +51 -0
  101. benchkit/looping.py +873 -0
  102. benchkit/mc_render.py +274 -0
  103. benchkit/mc_viewer/dist/atlas.png +0 -0
  104. benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
  105. benchkit/mc_viewer/dist/version.json +4 -0
  106. benchkit/mc_viewer/dist/viewer.html +23 -0
  107. benchkit/mc_viewer/dist/viewer.js +2 -0
  108. benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +25 -0
  109. benchkit/mc_viewer/dist/worker.js +2 -0
  110. benchkit/mc_viewer/dist/worker.js.LICENSE.txt +19 -0
  111. benchkit/metrics.py +100 -0
  112. benchkit/ollama.py +60 -0
  113. benchkit/perf.py +353 -0
  114. benchkit/perf_report.py +207 -0
  115. benchkit/perturbations.py +208 -0
  116. benchkit/pi_agent.py +476 -0
  117. benchkit/pi_package/package-lock.json +1827 -0
  118. benchkit/pi_package/package.json +8 -0
  119. benchkit/report.py +142 -0
  120. benchkit/report_markdown.py +645 -0
  121. benchkit/results.py +688 -0
  122. benchkit/retry.py +163 -0
  123. benchkit/runner.py +769 -0
  124. benchkit/sandbox.py +1167 -0
  125. benchkit/template_check.py +291 -0
  126. benchkit/templates/arena.html +504 -0
  127. benchkit/templates/history.html +58 -0
  128. benchkit/templates/perf.html +509 -0
  129. benchkit/templates/report.html +769 -0
  130. benchkit/tui/__init__.py +5 -0
  131. benchkit/tui/app.py +87 -0
  132. benchkit/tui/app.tcss +512 -0
  133. benchkit/tui/formatting.py +84 -0
  134. benchkit/tui/screens/__init__.py +1 -0
  135. benchkit/tui/screens/connect.py +191 -0
  136. benchkit/tui/screens/detail.py +288 -0
  137. benchkit/tui/screens/help.py +84 -0
  138. benchkit/tui/screens/modals.py +544 -0
  139. benchkit/tui/screens/results.py +361 -0
  140. benchkit/tui/screens/run.py +988 -0
  141. benchkit/tui/screens/setup.py +733 -0
  142. benchkit/tui/theme.py +135 -0
  143. benchkit/tui/widgets.py +120 -0
  144. benchkit_cli-0.1.0.dist-info/METADATA +294 -0
  145. benchkit_cli-0.1.0.dist-info/RECORD +149 -0
  146. benchkit_cli-0.1.0.dist-info/WHEEL +5 -0
  147. benchkit_cli-0.1.0.dist-info/entry_points.txt +3 -0
  148. benchkit_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  149. benchkit_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ seed="${1:?usage: setup.sh SEED [WORKSPACE]}"
4
+ workspace="${2:-/workspace/untangle-nested-submodules}"
5
+ slug="$(printf '%s' "$seed:submodules" | sha256sum | cut -c1-12)"
6
+ mkdir -p "$workspace"
7
+ workspace="$(cd "$workspace" && pwd)"
8
+ cd "$workspace"
9
+ export GIT_AUTHOR_NAME="BenchKit Generator" GIT_AUTHOR_EMAIL="generator@benchkit.invalid"
10
+ export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
11
+ # Submodules are cloned from local paths; Git >= 2.38.1 refuses that unless asked.
12
+ export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=protocol.file.allow GIT_CONFIG_VALUE_0=always
13
+ clock=1700900000
14
+ tick() {
15
+ clock=$((clock + 60))
16
+ export GIT_AUTHOR_DATE="@$clock +0000" GIT_COMMITTER_DATE="@$clock +0000"
17
+ }
18
+ configure() {
19
+ git -C "$1" config user.name "BenchKit Generator"
20
+ git -C "$1" config user.email "generator@benchkit.invalid"
21
+ git -C "$1" config commit.gpgSign false
22
+ git -C "$1" config core.autocrlf false
23
+ }
24
+ commit() {
25
+ local repo="$1"
26
+ shift
27
+ tick
28
+ git -C "$repo" add -A
29
+ git -C "$repo" commit -q "$@"
30
+ }
31
+ scratch="$(mktemp -d)"
32
+ trap 'rm -rf "$scratch"' EXIT
33
+
34
+ mkdir -p remotes
35
+ git init -q --bare -b main remotes/codec.git
36
+ git init -q --bare -b main remotes/engine.git
37
+
38
+ # codec: the innermost library.
39
+ git init -q -b main "$scratch/codec"
40
+ configure "$scratch/codec"
41
+ git -C "$scratch/codec" remote add origin "$workspace/remotes/codec.git"
42
+ cat > "$scratch/codec/codec.py" <<'PY'
43
+ """Length-prefixed frame codec."""
44
+
45
+
46
+ def encode(payload: str) -> str:
47
+ return f"{len(payload)}:{payload}"
48
+
49
+
50
+ def decode(frame: str) -> str:
51
+ size, _, payload = frame.partition(":")
52
+ return payload[: int(size)]
53
+ PY
54
+ commit "$scratch/codec" -m "codec: length-prefixed frames"
55
+ cat >> "$scratch/codec/codec.py" <<'PY'
56
+
57
+
58
+ def decode_all(stream: str) -> list[str]:
59
+ frames = []
60
+ while stream:
61
+ size, _, rest = stream.partition(":")
62
+ frames.append(rest[: int(size)].strip())
63
+ stream = rest[int(size) :]
64
+ return frames
65
+ PY
66
+ commit "$scratch/codec" -m "codec: decode frame streams"
67
+ tick
68
+ git -C "$scratch/codec" push -q origin main
69
+ git -C "$scratch/codec" switch -q -c rc
70
+ printf '\nVERSION = "2.0-rc"\n' >> "$scratch/codec/codec.py"
71
+ commit "$scratch/codec" -m "codec: 2.0 release candidate"
72
+ tick
73
+ git -C "$scratch/codec" push -q origin rc
74
+ codec_v1="$(git -C "$scratch/codec" rev-parse main~1)"
75
+ codec_v2="$(git -C "$scratch/codec" rev-parse main)"
76
+ codec_rc="$(git -C "$scratch/codec" rev-parse rc)"
77
+
78
+ # engine: vendors codec as a submodule.
79
+ git init -q -b main "$scratch/engine"
80
+ configure "$scratch/engine"
81
+ git -C "$scratch/engine" remote add origin "$workspace/remotes/engine.git"
82
+ git -C "$scratch/engine" submodule add -q ../codec.git vendor/codec
83
+ git -C "$scratch/engine/vendor/codec" checkout -q "$codec_v1"
84
+ cat > "$scratch/engine/engine.py" <<'PY'
85
+ """Message engine built on the vendored codec."""
86
+
87
+ import sys
88
+ from pathlib import Path
89
+
90
+ sys.path.insert(0, str(Path(__file__).parent / "vendor" / "codec"))
91
+
92
+ import codec # noqa: E402
93
+
94
+
95
+ def roundtrip(message: str) -> str:
96
+ return codec.decode(codec.encode(message))
97
+ PY
98
+ commit "$scratch/engine" -m "engine: roundtrip messages through codec"
99
+ git -C "$scratch/engine/vendor/codec" checkout -q "$codec_v2"
100
+ cat >> "$scratch/engine/engine.py" <<'PY'
101
+
102
+
103
+ def unpack(stream: str) -> list[str]:
104
+ return codec.decode_all(stream)
105
+ PY
106
+ commit "$scratch/engine" -m "engine: use codec stream decoding"
107
+ tick
108
+ git -C "$scratch/engine" push -q origin main
109
+ engine_v1="$(git -C "$scratch/engine" rev-parse main~1)"
110
+ engine_v2="$(git -C "$scratch/engine" rev-parse main)"
111
+ git -C "$scratch/engine" switch -q -c codec-rc
112
+ git -C "$scratch/engine/vendor/codec" checkout -q "$codec_rc"
113
+ commit "$scratch/engine" -m "engine: try codec 2.0 release candidate"
114
+ tick
115
+ git -C "$scratch/engine" push -q origin codec-rc
116
+
117
+ # app: the superproject.
118
+ git init -q -b main app
119
+ configure app
120
+ printf '__pycache__/\n' > app/.gitignore
121
+ git -C app submodule add -q ../remotes/engine.git libs/engine
122
+ git -C app/libs/engine checkout -q "$engine_v1"
123
+ git -C app add libs/engine
124
+ git -C app submodule update -q --init --recursive
125
+ cat > app/main.py <<'PY'
126
+ import sys
127
+ from pathlib import Path
128
+
129
+ sys.path.insert(0, str(Path(__file__).parent / "libs" / "engine"))
130
+
131
+ import engine # noqa: E402
132
+
133
+
134
+ def handle(message: str) -> str:
135
+ return engine.roundtrip(message).upper()
136
+ PY
137
+ cat > app/test_app.py <<'PY'
138
+ import unittest
139
+
140
+ from main import handle
141
+
142
+
143
+ class AppTests(unittest.TestCase):
144
+ def test_handle(self):
145
+ self.assertEqual(handle("ping"), "PING")
146
+ PY
147
+ printf '# Messaging app %s\n\nClone with `--recurse-submodules`; run `python3 -m unittest discover -v`.\n' "$slug" > app/README.md
148
+ commit app -m "bootstrap messaging app $slug"
149
+
150
+ # The top commit points libs/engine at a commit that only exists in this
151
+ # checkout: the same change as engine's published main, committed locally.
152
+ engine_dir=app/libs/engine
153
+ configure "$engine_dir"
154
+ configure "$engine_dir/vendor/codec"
155
+ git -C "$engine_dir/vendor/codec" checkout -q "$codec_v2"
156
+ git -C "$engine_dir" checkout -q "$engine_v2" -- engine.py
157
+ commit "$engine_dir" -m "engine: use codec stream decoding"
158
+ cat >> app/main.py <<'PY'
159
+
160
+
161
+ def handle_stream(stream: str) -> list[str]:
162
+ return [frame.upper() for frame in engine.unpack(stream)]
163
+ PY
164
+ cat >> app/test_app.py <<'PY'
165
+
166
+
167
+ class StreamTests(unittest.TestCase):
168
+ def test_stream(self):
169
+ from main import handle_stream
170
+
171
+ self.assertEqual(handle_stream("2:hi3:you"), ["HI", "YOU"])
172
+
173
+ def test_frames_keep_whitespace(self):
174
+ from main import handle_stream
175
+
176
+ self.assertEqual(handle_stream("3: hi2:yo"), [" HI", "YO"])
177
+ PY
178
+ commit app -m "Handle message streams with engine and codec 2"
179
+
180
+ # Uncommitted work in the nested codec checkout stops stripping frame payloads.
181
+ python3 - "$engine_dir/vendor/codec/codec.py" <<'PY'
182
+ import sys
183
+ from pathlib import Path
184
+
185
+ path = Path(sys.argv[1])
186
+ text = path.read_text()
187
+ old = " frames.append(rest[: int(size)].strip())\n"
188
+ new = " frames.append(rest[: int(size)])\n"
189
+ if old not in text:
190
+ raise SystemExit("setup: codec stream loop not found")
191
+ path.write_text(text.replace(old, new))
192
+ PY
193
+ unset GIT_AUTHOR_DATE GIT_COMMITTER_DATE
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env bash
2
+ set -u
3
+ seed="${1:?usage: verify.sh SEED [WORKSPACE]}"
4
+ workspace="${2:-/workspace/untangle-nested-submodules}"
5
+ emit() { printf '%s\t%s\t%s\n' "$1" "$2" "$3"; }
6
+ export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_COUNT=1
7
+ export GIT_CONFIG_KEY_0=protocol.file.allow GIT_CONFIG_VALUE_0=always
8
+ ids="codec_commit engine_commit top_commit_repaired superproject_update recursive_checkout"
9
+ app="$workspace/app"
10
+ if ! git -C "$app" rev-parse --git-dir >/dev/null 2>&1; then
11
+ for id in $ids; do emit "$id" 0 "superproject missing"; done
12
+ emit trap 1 "superproject replaced"
13
+ exit 0
14
+ fi
15
+ reference_root="$(mktemp -d)"
16
+ reference="$reference_root/reference"
17
+ bash "$(dirname "$0")/setup.sh" "$seed" "$reference" >/dev/null
18
+ codec_v2="$(git -C "$reference/remotes/codec.git" rev-parse main)"
19
+ codec_rc="$(git -C "$reference/remotes/codec.git" rev-parse rc)"
20
+ engine_v2="$(git -C "$reference/remotes/engine.git" rev-parse main)"
21
+ engine_rc="$(git -C "$reference/remotes/engine.git" rev-parse codec-rc)"
22
+ bootstrap="$(git -C "$reference/app" rev-parse HEAD~1)"
23
+ top_subject="$(git -C "$reference/app" log -1 --format=%s HEAD)"
24
+ top_files="$(git -C "$reference/app" ls-tree -r HEAD | grep -v $'\tlibs/engine$')"
25
+ app_gitmodules="$(git -C "$reference/app" rev-parse HEAD:.gitmodules)"
26
+ engine_gitmodules="$(git -C "$reference/remotes/engine.git" rev-parse main:.gitmodules)"
27
+ fixed_codec="$(git hash-object "$reference/app/libs/engine/vendor/codec/codec.py")"
28
+ rm -rf "$reference_root"
29
+
30
+ subject() { git -C "$1" log -1 --format=%s "$2" 2>/dev/null; }
31
+ parents() { git -C "$1" rev-list --parents -n 1 "$2" 2>/dev/null | cut -d' ' -f2- ; }
32
+ changed() { git -C "$1" diff-tree --no-commit-id --name-only -r "$2" 2>/dev/null | tr '\n' ' '; }
33
+ gitlink() { git -C "$1" ls-tree "$2" "$3" 2>/dev/null | awk '$1 == "160000" {print $3}'; }
34
+
35
+ codec_remote="$workspace/remotes/codec.git"
36
+ codec_tip="$(git -C "$codec_remote" rev-parse -q --verify refs/heads/main 2>/dev/null)"
37
+ codec=0
38
+ [[ "$(parents "$codec_remote" "$codec_tip")" == "$codec_v2" \
39
+ && "$(subject "$codec_remote" "$codec_tip")" == "Fix codec frame whitespace" \
40
+ && "$(changed "$codec_remote" "$codec_tip")" == "codec.py " \
41
+ && "$(git -C "$codec_remote" rev-parse "$codec_tip:codec.py" 2>/dev/null)" == "$fixed_codec" \
42
+ && "$(git -C "$codec_remote" rev-parse -q --verify refs/heads/rc 2>/dev/null)" == "$codec_rc" ]] && codec=1
43
+ emit codec_commit "$codec" "codec_main=$codec_tip"
44
+
45
+ engine_remote="$workspace/remotes/engine.git"
46
+ engine_tip="$(git -C "$engine_remote" rev-parse -q --verify refs/heads/main 2>/dev/null)"
47
+ engine=0
48
+ [[ "$codec" == 1 \
49
+ && "$(parents "$engine_remote" "$engine_tip")" == "$engine_v2" \
50
+ && "$(subject "$engine_remote" "$engine_tip")" == "Pick up codec whitespace fix" \
51
+ && "$(changed "$engine_remote" "$engine_tip")" == "vendor/codec " \
52
+ && "$(gitlink "$engine_remote" "$engine_tip" vendor/codec)" == "$codec_tip" \
53
+ && "$(git -C "$engine_remote" rev-parse -q --verify refs/heads/codec-rc 2>/dev/null)" == "$engine_rc" ]] && engine=1
54
+ emit engine_commit "$engine" "engine_main=$engine_tip"
55
+
56
+ head="$(git -C "$app" rev-parse -q --verify refs/heads/main 2>/dev/null)"
57
+ repaired="$(git -C "$app" rev-parse -q --verify "$head^" 2>/dev/null)"
58
+ top=0
59
+ [[ "$(parents "$app" "$repaired")" == "$bootstrap" \
60
+ && "$(subject "$app" "$repaired")" == "$top_subject" \
61
+ && "$(gitlink "$app" "$repaired" libs/engine)" == "$engine_v2" \
62
+ && "$(git -C "$app" ls-tree -r "$repaired" 2>/dev/null | grep -v $'\tlibs/engine$')" == "$top_files" ]] && top=1
63
+ emit top_commit_repaired "$top" "top_engine=$(gitlink "$app" "$repaired" libs/engine)"
64
+
65
+ update=0
66
+ [[ "$top" == 1 && "$engine" == 1 \
67
+ && "$(parents "$app" "$head")" == "$repaired" \
68
+ && "$(subject "$app" "$head")" == "Pick up codec whitespace fix" \
69
+ && "$(changed "$app" "$head")" == "libs/engine " \
70
+ && "$(gitlink "$app" "$head" libs/engine)" == "$engine_tip" ]] && update=1
71
+ emit superproject_update "$update" "head_engine=$(gitlink "$app" "$head" libs/engine)"
72
+
73
+ checkout=0
74
+ clone_root="$(mktemp -d)"
75
+ if [[ -n "$head" ]] && git clone -q --no-checkout "$app" "$clone_root/app" >/dev/null 2>&1 \
76
+ && git -C "$clone_root/app" checkout -q --detach "$head" >/dev/null 2>&1 \
77
+ && git -C "$clone_root/app" submodule update -q --init --recursive >/dev/null 2>&1 \
78
+ && [[ "$(git -C "$clone_root/app" rev-parse HEAD:.gitmodules)" == "$app_gitmodules" \
79
+ && "$(git -C "$clone_root/app/libs/engine" rev-parse HEAD:.gitmodules)" == "$engine_gitmodules" ]]; then
80
+ (cd "$clone_root/app" && python3 -B -m unittest discover) >/dev/null 2>&1 && checkout=1
81
+ fi
82
+ rm -rf "$clone_root"
83
+ emit recursive_checkout "$checkout" "fresh_recursive_clone_tests=$checkout"
84
+
85
+ trap=0
86
+ [[ -n "$(gitlink "$app" "$head" libs/engine)" ]] || trap=1
87
+ git -C "$app" ls-tree -r --name-only "$head" 2>/dev/null | grep -q '^libs/engine/' && trap=1
88
+ [[ "$(git -C "$app" rev-parse "$head:.gitmodules" 2>/dev/null)" == "$app_gitmodules" ]] || trap=1
89
+ git -C "$app" merge-base --is-ancestor "$bootstrap" "$head" 2>/dev/null || trap=1
90
+ emit trap "$trap" "gitlink_removed_files_copied_or_history_lost=$trap"