Repository navigation
Expand file tree
/
Copy pathbacktest-pipenv.py
More file actions
executable file
·1471 lines (1329 loc) · 79.8 KB
/
Copy pathbacktest-pipenv.py
File metadata and controls
executable file
·1471 lines (1329 loc) · 79.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""Drive the real socket-patch CLI and real Pipenv releases through hosted,
vendored and agent mode on native Pipfile.lock generations.
For the last stable release of every published Pipenv major the harness
bootstraps that exact release (uv venv for 2018+, a `python:3.6.15-slim`
Docker venv for the pre-2018 majors that no longer run on modern Pythons),
generates a native lock for a one-dependency project (urllib3 1.26.18, which
has a public free-tier Socket patch), then for each mode:
hosted scan --mode hosted -> pipenv install -> installed bytes == patch
vendored scan --mode vendored -> pipenv install -> installed bytes == patch
agent pipenv install -> scan --mode agent -> installed bytes == patch
agent-oot same as agent, but with Pipenv's DEFAULT out-of-tree virtualenv
(WORKON_HOME) instead of an in-project .venv
and checks --dry-run parity, idempotent re-scans, an untouched Pipfile, the
lock-driven install of a FRESH clone of the committed state, whether a WARM
venv (upstream urllib3 already installed) gets the patched wheel, tampered
hashes, `pipenv verify`, what `pipenv lock` does to the patched entry, `vex`,
the manifest-less `vex` of the fresh clone (manifest deleted; ledgers deleted
too; `--offline`; the lock reverted to the registry, `--no-verify` too), and
`rollback` restoring every byte. Pre-2018 releases are expected to be
REFUSED (old lock spec for 0–6, vendored for 7–11) without touching the lock.
Shapes mirror the depscan capture harness: direct, dev, category (2022+),
marker, marker-excluded, extras, transitive, crlf. Invocations vary how the
CLI is pointed at the project: in-dir (cwd = project, no --cwd), cwd-flag
(run from the output root with --cwd), subdir (project nested two levels down,
--cwd relative), symlink (cwd = a symlink to the project).
Needs network (PyPI + patch.socket.dev), uv, Docker (pre-2018 majors only) and
no Socket token.
scripts/backtest-pipenv.py --socket-patch target/debug/socket-patch \
--socket-patch-revision $(git rev-parse --short HEAD) --output /tmp/pipenv-compat
scripts/backtest-pipenv.py --render-doc-table /tmp/pipenv-compat/summary.json
"""
import argparse
import concurrent.futures
import hashlib
import json
import os
import re
import shlex
import shutil
import signal
import subprocess
import sys
import threading
import time
import traceback
import urllib.error
import urllib.request
import uuid as uuid_mod
from datetime import datetime, timezone
from pathlib import Path
VERSIONS = [
"0.2.8",
"3.6.2",
"4.1.4",
"5.4.2",
"6.2.9",
"7.9.10",
"8.3.2",
"9.1.0",
"10.1.2",
"11.10.4",
"2018.11.26",
"2020.11.15",
"2021.11.23",
"2022.12.19",
"2023.12.1",
"2024.4.1",
"2025.1.3",
"2026.8.0",
]
MODES = ["hosted", "vendored", "agent", "agent-oot"]
SHAPES = ["direct", "dev", "category", "marker", "marker-excluded", "extras", "transitive", "crlf"]
INVOCATIONS = ["in-dir", "cwd-flag", "subdir", "symlink"]
LEGACY_IMAGE = "python:3.6.15-slim"
PROJECT = """[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
urllib3 = "==1.26.18"
[dev-packages]
"""
# Pins for the transitive shape (urllib3 reached through requests).
TRANSITIVE_MODERN = {
"requests": "2.31.0",
"charset-normalizer": "3.3.2",
"idna": "3.6",
"certifi": "2024.2.2",
"urllib3": "1.26.18",
}
TRANSITIVE_LEGACY = {
"requests": "2.27.1",
"charset-normalizer": "2.0.12",
"idna": "3.6",
"certifi": "2024.2.2",
"urllib3": "1.26.18",
}
ORACLE = """import hashlib,json,pathlib,sys,sysconfig
root=pathlib.Path(sysconfig.get_paths()['purelib'])
out={}
for name in json.loads(sys.argv[1]):
p=root/name
if p.is_file():
d=p.read_bytes(); out[name]=hashlib.sha256(('blob %d\\0'%len(d)).encode()+d).hexdigest()
else:
out[name]=None
print(json.dumps(out))
"""
ABSENT = "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('urllib3') is None else 1)"
PATCH_UUID = "e828efa5-5c6d-43f3-9909-03f5ac232b98"
PURL_BASE = "pkg:pypi/urllib3@1.26.18"
LEGACY_TOOL_PACKAGES = [
"pip==9.0.3",
"pip-tools==1.11.0",
"setuptools==44.1.1",
"wheel==0.37.1",
"virtualenv==16.7.12",
"click==6.7",
"requests==2.27.1",
"pexpect==4.2.1",
"delegator.py==0.0.14",
]
LEGACY_VENV_PACKAGES = ["pip==9.0.3", "setuptools==44.1.1", "wheel==0.37.1"]
def major_of(version):
return int(version.split(".")[0])
def vtuple(v):
return tuple(int(x) for x in v.split("."))
def is_legacy(version):
return major_of(version) < 2018
# ---------------------------------------------------- v5 `--json` envelope
# Every command prints ONE envelope (CLI_CONTRACT.md "`scan` and `get` JSON
# (v5.0)"): `status` (camelCase), `dryRun`, `events[]` (each `{action, purl,
# uuid, errorCode, reason, error, details}`; hosted / vendored events carry
# `details.mode`, agent events none), `summary`, top-level `warnings[{code,
# detail}]` and `error{code, message}`. No `redirect.redirected`,
# `vendor.summary`, `apply.*` or legacy rollback arrays any more.
def leg_events(mode, envelope):
"""The events of one leg: `details.mode` hosted / vendored; every other
mode (agent, agent-oot) reads the mode-less agent events."""
leg = mode if mode in ("hosted", "vendored") else None
return [e for e in (envelope or {}).get("events") or [] if (e.get("details") or {}).get("mode") == leg]
def applied_count(mode, envelope, action=None):
"""Patches the leg wired: its `applied` events (a hosted re-run still
emits `applied` for the pins it re-confirms), or on a `--dry-run` the
`verified` previews. `action` counts another action instead."""
action = action or ("verified" if (envelope or {}).get("dryRun") else "applied")
return sum(1 for e in leg_events(mode, envelope) if e.get("action") == action)
def envelope_warnings(envelope):
"""Every coded record of the run as `{code, detail, action, purl}`: the
top-level `error`, each event carrying an `errorCode` (refusals, skips,
failures, vendor advisories) and each run-level warning."""
env = envelope or {}
rows = []
if isinstance(env.get("error"), dict) and env["error"].get("code"):
rows.append({"code": env["error"]["code"], "detail": env["error"].get("message") or ""})
for e in env.get("events") or []:
if e.get("errorCode"):
rows.append({"code": e["errorCode"], "detail": e.get("reason") or e.get("error") or "", "action": e.get("action"), "purl": e.get("purl")})
for w in env.get("warnings") or []:
if isinstance(w, dict) and w.get("code"):
rows.append({"code": w["code"], "detail": w.get("detail") or ""})
return rows
def envelope_codes(envelope):
"""The sorted distinct codes of `envelope_warnings`."""
return sorted({w["code"] for w in envelope_warnings(envelope)})
def save(path, data):
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n")
DEFAULT_TIMEOUT = int(os.environ.get("BACKTEST_TIMEOUT", "900"))
# A transport failure, never a functional one: pip/Pipenv giving up on PyPI
# ("too many 503 error responses", connection errors) or the CLI's own report
# of a request error or a patch API 5xx.
TRANSPORT_FAILURE = re.compile(
r"too many 5\d\d error responses|Max retries exceeded with url|"
r"NewConnectionError|ConnectTimeoutError|ReadTimeoutError|"
r"Temporary failure in name resolution|nodename nor servname provided|Connection reset by peer|RemoteDisconnected|"
r"error sending request for url \(|API request failed with status 5\d\d\b"
)
def has_transport_failure(case, payload):
"""Whether a failed case's error text or any of its logs shows a transport failure."""
if TRANSPORT_FAILURE.search(json.dumps(payload)):
return True
logs = sorted(case.glob("*.log*")) if case.is_dir() else []
return any(TRANSPORT_FAILURE.search(log.read_text(errors="replace")) for log in logs if log.is_file())
def retry_transport(run_case, job, case, root, attempts=3, sleep=time.sleep):
"""("row"|"error", payload) for one case, re-run from a clean case dir while
it fails for transport reasons. A failed attempt's logs are kept under
<root>/attempts/<case>/<n>/ and listed on the final payload."""
history = []
for attempt in range(1, attempts + 1):
try:
kind, payload = "row", run_case(job)
except Exception as e:
version, shape, mode, invocation = job
kind, payload = "error", {"pipenv": version, "shape": shape, "mode": mode, "invocation": invocation, "error": str(e)[-3000:], "trace": traceback.format_exc()[-1500:]}
failed = kind == "error" or not payload.get("passed")
if not failed or attempt == attempts or not has_transport_failure(case, payload):
if history:
payload["transportRetries"] = history
if case.is_dir():
save(case / "result.json", payload)
return kind, payload
evidence = root / "attempts" / case.name / str(attempt)
evidence.mkdir(parents=True, exist_ok=True)
for log in case.glob("*.log*") if case.is_dir() else []:
if log.is_file():
shutil.copy2(log, evidence / log.name)
history.append({"attempt": attempt, "evidence": evidence.relative_to(root).as_posix(), "error": (payload.get("error") or "")[-300:], "failedChecks": [k for k, ok in payload.get("checks", {}).items() if not ok]})
print(f"{case.name}: transport failure; retrying fresh case ({attempt}/{attempts})", flush=True)
sleep(10 * attempt)
class Run:
"""Run a command in its own process group, capture output, write a log.
A timeout kills the whole group (Docker clients, pip subprocesses, …).
"""
def __init__(self, cmd, cwd, env, log, timeout=None, container=None):
timeout = timeout or DEFAULT_TIMEOUT
self.cmd = [str(c) for c in cmd]
try:
with subprocess.Popen(
self.cmd,
cwd=str(cwd),
env=env,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True,
) as p:
try:
self.out, self.err = p.communicate(timeout=timeout)
self.rc = p.returncode
except subprocess.TimeoutExpired:
try:
os.killpg(p.pid, signal.SIGKILL)
except ProcessLookupError:
pass
out, err = p.communicate()
self.rc, self.out, self.err = 124, out or "", (err or "") + f"\nTIMEOUT after {timeout}s"
except OSError as e:
self.rc, self.out, self.err = 127, "", str(e)
finally:
if container:
subprocess.run(["docker", "rm", "-f", container], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=60)
Path(log).write_text(
"$ " + " ".join(shlex.quote(c) for c in self.cmd) + f"\n# cwd {cwd}\n# exit {self.rc}\n--- stdout\n{self.out}\n--- stderr\n{self.err}"
)
def ok(self):
return self.rc == 0
def json(self):
i = self.out.find("{")
if i < 0:
raise RuntimeError("no JSON in output: " + (self.out + self.err)[-2000:])
return json.loads(self.out[i:])
def json_or_empty(self):
try:
return self.json()
except Exception:
return {}
def tail(self, n=400):
return (self.out + self.err)[-n:]
def urllib3_entries(lock_bytes):
"""Every category's urllib3 entry of a Pipfile.lock, parsed, for a
semantic (key-order- and whitespace-insensitive) comparison."""
data = json.loads(lock_bytes.decode("utf-8-sig"))
return {cat: entries["urllib3"] for cat, entries in data.items() if cat != "_meta" and isinstance(entries, dict) and "urllib3" in entries}
def vex_statements_for(doc, purl_base):
"""The statements of an OpenVEX document naming `purl_base` (qualifiers
ignored) as a subcomponent."""
out = []
for st in (doc or {}).get("statements") or []:
ids = [c.get("@id") or "" for p in st.get("products") or [] for c in p.get("subcomponents") or []]
if any(i.split("?")[0] == purl_base for i in ids):
out.append(st)
return out
def vex_attests(rc, doc, purl_base, uuid, marker):
"""A standalone `vex` run attested `purl_base` not_affected via patch
`uuid` with the `(redirected)` / `(vendored)` provenance marker."""
part = f"Patched via Socket patch {uuid} ({marker})"
statements = vex_statements_for(doc, purl_base)
return rc == 0 and bool(statements) and all(
st.get("status") == "not_affected" and part in (st.get("impact_statement") or "") for st in statements
)
def hosted_uuid(text):
"""The patch uuid of the first patch.socket.dev URL in `text` (the LAST
uuid-shaped path segment: an earlier one may be a grant token)."""
m = re.search(r"https://patch\.socket\.dev/[^\s\"'#]+", text)
if not m:
return None
uuids = re.findall(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", m[0])
return uuids[-1] if uuids else None
def published_record(uuid):
"""`GET https://patches-api.socket.dev/patch/view/<uuid>` (the public proxy):
v5 hosted mode keeps no ledger, so this is the record a hosted pin names."""
url = f"https://patches-api.socket.dev/patch/view/{uuid}"
for attempt in range(1, 6):
try:
req = urllib.request.Request(url, headers={"Accept": "application/json", "User-Agent": "SocketPatchCLI-backtest/1.0"})
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode("utf-8"))
except (urllib.error.URLError, TimeoutError, ConnectionError):
if attempt == 5:
raise
time.sleep(10 * attempt)
def vex_nothing_named(rc, envelope, doc, purl_base):
"""A standalone `vex --json` run over a checkout that names the patch
nowhere (no manifest, no ledger, no wiring): `manifest_not_found`, exit 2."""
return (rc == 2 and not vex_statements_for(doc, purl_base)
and ((envelope or {}).get("error") or {}).get("code") == "manifest_not_found")
def vex_omits(rc, envelope, doc, purl_base, reason):
"""A standalone `vex --json` run did NOT attest `purl_base` and reported
it skipped with `reason` (exit 1: nothing else to attest)."""
events = [e for e in (envelope or {}).get("events") or [] if (e.get("purl") or "").split("?")[0] == purl_base]
return (
rc == 1
and not vex_statements_for(doc, purl_base)
and not any(e.get("action") == "verified" for e in events)
and any(e.get("action") == "skipped" and e.get("errorCode") == reason for e in events)
)
def require(r, what):
if not r.ok():
raise RuntimeError(f"{what} failed (exit {r.rc}):\n{(r.out + r.err)[-4000:]}")
return r
def base_env():
env = {
k: v
for k, v in os.environ.items()
if not k.startswith(("PYTHON", "PIP_", "PIPENV_", "SOCKET_", "UV_", "WORKON_HOME")) and k != "VIRTUAL_ENV"
}
env.update(
SOCKET_NO_CONFIG="1",
SOCKET_NO_UPDATE_CHECK="1",
SOCKET_TELEMETRY_DISABLED="1",
PIP_CONFIG_FILE=os.devnull,
PIP_DISABLE_PIP_VERSION_CHECK="1",
PIPENV_YES="1",
PIPENV_NOSPIN="1",
PIPENV_IGNORE_VIRTUALENVS="1",
PYTHONDONTWRITEBYTECODE="1",
)
return env
def pipenv_shim_dir(root, version, tool):
"""Expose only pipenv on PATH, without the tool venv's Python.
Different shape workers share this directory. Create the link atomically
and accept EEXIST only when another worker created the same shim.
"""
if is_legacy(version):
return root / "legacy-bin" / version
directory = root / "pipenv-bin" / version
directory.mkdir(parents=True, exist_ok=True)
link = directory / "pipenv"
target = tool / "bin/pipenv"
try:
link.symlink_to(target)
except FileExistsError:
if not link.is_symlink() or os.readlink(link) != str(target):
raise
return directory
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--socket-patch", type=Path, help="socket-patch CLI binary")
ap.add_argument("--socket-patch-revision", help="git revision the binary was built from (recorded)")
ap.add_argument("--output", type=Path)
ap.add_argument("--tools-root", type=Path, help="where Pipenv releases are (or get) bootstrapped; default <output>/tools-root")
ap.add_argument("--versions", nargs="+", default=VERSIONS)
ap.add_argument("--modes", nargs="+", default=MODES, choices=MODES)
ap.add_argument("--shapes", nargs="+", default=["direct"], choices=SHAPES)
ap.add_argument("--invocations", default="in-dir", help="comma-separated subset of " + ",".join(INVOCATIONS) + " (direct shape only)")
ap.add_argument("--jobs", type=int, default=4)
ap.add_argument("--render-doc-table", type=Path, metavar="SUMMARY_JSON")
args = ap.parse_args()
if args.render_doc_table:
summary = json.loads(args.render_doc_table.read_text())
print(render_doc_table(summary))
print()
print(render_table(summary))
return
if not (args.socket_patch and args.socket_patch_revision and args.output):
ap.error("--socket-patch, --socket-patch-revision and --output are required")
invocations = [i.strip() for i in args.invocations.split(",") if i.strip()]
for inv in invocations:
if inv not in INVOCATIONS:
ap.error(f"unknown invocation {inv!r}")
root = args.output.resolve()
root.mkdir(parents=True, exist_ok=True)
tools_root = (args.tools_root or (root / "tools-root")).resolve()
tools_root.mkdir(parents=True, exist_ok=True)
cli_path = args.socket_patch.resolve()
env = base_env()
provenance = {
"capturedAt": datetime.now(timezone.utc).isoformat(),
"cliRevision": args.socket_patch_revision,
"cliSha256": hashlib.sha256(cli_path.read_bytes()).hexdigest(),
"pipenvVersions": args.versions,
"modes": args.modes,
"shapes": args.shapes,
"invocations": invocations,
"host": os.uname().sysname + " " + os.uname().machine,
"legacyImage": LEGACY_IMAGE,
}
save(root / "provenance.json", provenance)
log_lock = threading.Lock()
def say(*a):
with log_lock:
print(*a, flush=True)
# ------------------------------------------------------------------ docker
def docker_cmd(cwd, extra_env, workdir=None, name=None):
"""`docker run` prefix that mirrors ROOTs + cwd at identical paths."""
mounts = []
seen = set()
for path in [tools_root, root, Path(cwd)]:
p = str(path)
if not any(p == s or p.startswith(s.rstrip("/") + "/") for s in seen):
mounts += ["-v", f"{p}:{p}"]
seen.add(p)
cmd = ["docker", "run", "--rm", "--name", name or ("pipenv-bt-" + uuid_mod.uuid4().hex[:12]), *mounts, "-w", str(workdir or cwd)]
for k, v in extra_env.items():
cmd += ["-e", f"{k}={v}"]
cmd.append(LEGACY_IMAGE)
return cmd
def legacy_run(cmd, cwd, extra_env, log, timeout=None, workdir=None):
name = "pipenv-bt-" + uuid_mod.uuid4().hex[:12]
full = docker_cmd(cwd, extra_env, workdir=workdir, name=name) + [str(c) for c in cmd]
return Run(full, cwd, env, log, timeout=timeout, container=name)
# ------------------------------------------------------------------ tools
def tool_dir(version):
return tools_root / ("legacy-tools" if is_legacy(version) else "tools") / version
def wrapper_dir(version):
return root / "legacy-bin" / version
def prepare_tool(version):
tool = tool_dir(version)
legacy = is_legacy(version)
logs = root / "bootstrap-logs"
logs.mkdir(exist_ok=True)
if legacy:
# Host-side `pipenv` that runs the legacy release inside Docker with
# the tool root, the output root and the caller's cwd bind-mounted at
# identical paths — the CLI's `pipenv --version` probe and the
# harness's own pipenv invocations both go through it.
wd = wrapper_dir(version)
wd.mkdir(parents=True, exist_ok=True)
mounts = " ".join(f"-v {shlex.quote(str(p))}:{shlex.quote(str(p))}" for p in [tools_root, root])
(wd / "pipenv").write_text(
"#!/bin/sh\n"
f"# Pipenv {version} inside {LEGACY_IMAGE}; tool root, output root and $PWD bind-mounted\n"
f'exec docker run --rm -i {mounts} -v "$PWD":"$PWD" -w "$PWD" \\\n'
f" -e PATH={shlex.quote(str(tool / 'bin'))}:/usr/local/bin:/usr/bin:/bin \\\n"
" -e PIPENV_VENV_IN_PROJECT -e PIPENV_YES=1 -e PIPENV_NOSPIN=1 -e PIP_DISABLE_PIP_VERSION_CHECK=1 -e WORKON_HOME -e PIPENV_PYTHON \\\n"
f" {LEGACY_IMAGE} {shlex.quote(str(tool / 'bin/pipenv'))} \"$@\"\n"
)
(wd / "pipenv").chmod(0o755)
if not (tool / "bin/pipenv").exists():
tool.parent.mkdir(parents=True, exist_ok=True)
require(legacy_run(["python", "-m", "venv", tool], tools_root, {"PIP_DISABLE_PIP_VERSION_CHECK": "1"}, logs / f"{version}-venv.log"), f"legacy venv {version}")
require(
legacy_run([tool / "bin/python", "-m", "pip", "install", f"pipenv=={version}", *LEGACY_TOOL_PACKAGES], tools_root, {"PIP_DISABLE_PIP_VERSION_CHECK": "1"}, logs / f"{version}-install.log"),
f"legacy pipenv {version} install",
)
elif not (tool / "bin/pipenv").exists():
major = major_of(version)
py = os.environ.get("BACKTEST_PY38", "3.8.20") if major <= 2022 else os.environ.get("BACKTEST_PY312", "3.12.13")
require(Run(["uv", "venv", "-q", "--python", py, tool], root, env, logs / f"{version}-venv.log"), "uv venv")
pkgs = [f"pipenv=={version}", "pip==24.0", "setuptools==69.5.1" if major >= 2023 else "setuptools==57.5.0"]
require(Run(["uv", "pip", "install", "-q", "--python", tool / "bin/python", *pkgs], root, env, logs / f"{version}-install.log"), "pipenv bootstrap")
return tool
# ------------------------------------------------------------- pipenv env
def pipenv_env(version, tool, in_project=True, workon_home=None):
e = dict(env)
e["PATH"] = str(pipenv_shim_dir(root, version, tool)) + os.pathsep + e.get("PATH", "")
e["PIPENV_PYTHON"] = str(tool / "bin/python")
if in_project:
e["PIPENV_VENV_IN_PROJECT"] = "1"
if workon_home is not None:
e["WORKON_HOME"] = str(workon_home)
return e
def pipenv_bin(version, tool):
return (wrapper_dir(version) / "pipenv") if is_legacy(version) else (tool / "bin/pipenv")
def run_pipenv(version, tool, args_, cwd, penv, log, timeout=None):
"""Run `pipenv <args>` for `version` in `cwd` (Docker for legacy)."""
if is_legacy(version):
extra = {k: v for k, v in penv.items() if k.startswith(("PIP", "WORKON_HOME"))}
extra["PATH"] = f"{tool / 'bin'}:/usr/local/bin:/usr/bin:/bin"
return legacy_run([tool / "bin/pipenv", *args_], cwd, extra, log, timeout=timeout)
return Run([tool / "bin/pipenv", *args_], cwd, penv, log, timeout=timeout)
def run_python(version, python, args_, cwd, penv, log, timeout=None):
"""Run the PROJECT interpreter (Docker for legacy: linux venv)."""
if is_legacy(version):
extra = {k: v for k, v in penv.items() if k.startswith(("PIP", "WORKON_HOME"))}
return legacy_run([python, *args_], cwd, extra, log, timeout=timeout)
return Run([python, *args_], cwd, penv, log, timeout=timeout)
def make_venv(version, tool, venv, cwd, log, packages=(), native=False):
"""Create the project venv.
Modern: uv venv on the tool's interpreter + pip 24 (+ packages).
Legacy: `native=True` builds the Docker (linux, python 3.6) venv that
Pipenv itself installs into; otherwise a host uv venv (python 3.8) so
the CLI can crawl an installed urllib3 the way the depscan captures did.
"""
if venv.exists():
shutil.rmtree(venv)
if is_legacy(version) and native:
venv.mkdir(parents=True)
require(legacy_run(["/usr/local/bin/python", "-m", "venv", venv], cwd, {"PIP_DISABLE_PIP_VERSION_CHECK": "1"}, log), "docker venv")
require(legacy_run([venv / "bin/python", "-m", "pip", "install", *LEGACY_VENV_PACKAGES, *packages], cwd, {"PIP_DISABLE_PIP_VERSION_CHECK": "1"}, str(log) + ".pip"), "docker venv bootstrap")
return
py = os.environ.get("BACKTEST_PY38", "3.8.20") if is_legacy(version) else tool / "bin/python"
require(Run(["uv", "venv", "-q", "--python", py, venv], cwd, env, log), "uv venv")
pkgs = ["pip==24.0", "setuptools==69.5.1", *packages]
require(Run(["uv", "pip", "install", "-q", "--python", venv / "bin/python", *pkgs], cwd, env, str(log) + ".pip"), "venv bootstrap")
def uninstall_urllib3(version, python, cwd, penv, log):
if is_legacy(version):
r = run_python(version, python, ["-m", "pip", "uninstall", "-y", "urllib3"], cwd, penv, log)
else:
r = Run(["uv", "pip", "uninstall", "-q", "--python", python, "urllib3"], cwd, env, log)
if not r.ok():
# Tolerate "not installed" as long as the package is truly absent.
require(run_python(version, python, ["-c", ABSENT], cwd, penv, str(log) + ".absent"), "urllib3 uninstall")
def oracle(version, python, names, cwd, penv, log):
# For the pre-2018 releases the oracle runs in a fresh container over a
# bind mount the host CLI just wrote through (stage + rename): Docker
# Desktop's shared file cache occasionally shows the directory without
# the renamed entry for a moment, so a missing file is re-read a few
# times before it counts (a real absence stays absent).
attempts = 4 if is_legacy(version) else 1
result = {}
for attempt in range(attempts):
r = run_python(version, python, ["-c", ORACLE, json.dumps(list(names))], cwd, penv, log)
result = json.loads(r.out.strip().splitlines()[-1]) if r.ok() and r.out.strip() else {}
if result and all(v is not None for v in result.values()):
return result
if attempt + 1 < attempts:
time.sleep(1.5)
return result
def urllib3_absent(version, python, cwd, penv, log):
return run_python(version, python, ["-c", ABSENT], cwd, penv, log).ok()
def install_args(version, shape, deploy=True):
# `--ignore-pipfile` arrives with Pipenv 3; `--deploy` with Pipenv 9.
a = ["install"] + (["--ignore-pipfile"] if major_of(version) >= 3 else [])
if deploy and major_of(version) >= 9:
a.append("--deploy")
if shape == "dev":
a.append("--dev")
if shape == "category":
a += ["--categories", "tests"]
return a
def sync_args(version, shape):
if major_of(version) < 2018:
return None
a = ["sync"]
if shape == "dev":
a.append("--dev")
if shape == "category":
a += ["--categories", "tests"]
return a
# ------------------------------------------------------------ fixtures
def pipfile_text(version, shape):
text = PROJECT
if shape == "dev":
text = PROJECT.replace('urllib3 = "==1.26.18"\n', "").replace("[dev-packages]", '[dev-packages]\nurllib3 = "==1.26.18"')
elif shape == "category":
text = PROJECT.replace("[packages]", "[tests]")
elif shape in ("marker", "marker-excluded", "extras"):
field = 'extras = ["socks"]' if shape == "extras" else 'markers = "python_version ' + (">" if shape == "marker-excluded" else "<") + " '4'\""
text = PROJECT.replace('"==1.26.18"', '{version="==1.26.18", ' + field + "}")
elif shape == "transitive":
pins = TRANSITIVE_LEGACY if is_legacy(version) else TRANSITIVE_MODERN
text = PROJECT.split("[packages]")[0] + "[packages]\n" + "".join(f'{n} = "=={v}"\n' for n, v in pins.items()) + "\n[dev-packages]\n"
return text
def content_hash(version, tool, project, penv, log):
code = (
"import pipenv,pipfile; print(pipfile.load('Pipfile').hash)"
if is_legacy(version)
else "from pipenv.project import Project; p=Project(); print(p.calculate_pipfile_hash() if hasattr(p,'calculate_pipfile_hash') else p.pipfile.calculate_hash())"
)
r = require(run_python(version, tool / "bin/python", ["-c", code], project, penv, log), "content hash")
return r.out.strip().splitlines()[-1]
def native_lock(version, tool, shape):
"""Generate (once) the native Pipfile.lock for version/shape."""
original = root / "original" / version / shape
if (original / "Pipfile.lock").exists():
return original
original.mkdir(parents=True, exist_ok=True)
(original / "Pipfile").write_text(pipfile_text(version, shape))
penv = pipenv_env(version, tool)
# Legacy Pipenv resolves through the project venv's pip-tools; give it one.
make_venv(version, tool, original / ".venv", original, original / "venv.log", native=True)
require(run_pipenv(version, tool, ["lock"], original, penv, original / "generation.log"), f"pipenv {version} lock ({shape})")
if shape == "transitive":
# Lock with every pin (so urllib3 is pinned to 1.26.18), then shrink
# the Pipfile to `requests` only and stamp its content hash so
# `--deploy` still accepts the lock.
pins = TRANSITIVE_LEGACY if is_legacy(version) else TRANSITIVE_MODERN
(original / "Pipfile").write_text(PROJECT.replace('urllib3 = "==1.26.18"', 'requests = "==' + pins["requests"] + '"'))
hashed = content_hash(version, tool, original, penv, original / "content-hash.log")
lock = json.loads((original / "Pipfile.lock").read_text())
lock["_meta"]["hash"]["sha256"] = hashed
(original / "Pipfile.lock").write_text(json.dumps(lock, indent=4, sort_keys=True) + "\n")
if shape == "crlf":
for name in ["Pipfile", "Pipfile.lock"]:
p = original / name
p.write_bytes(p.read_text().replace("\r\n", "\n").replace("\n", "\r\n").encode())
shutil.rmtree(original / ".venv", ignore_errors=True)
return original
# --------------------------------------------------------------- the CLI
def cli_invocation(case, project, invocation):
"""(cwd, extra args) for pointing the CLI at `project`."""
if invocation == "in-dir":
return project, []
if invocation == "cwd-flag":
return root, ["--cwd", str(project)]
if invocation == "subdir":
# project lives at <case>/nested/app; run from <case>/nested
return project.parent, ["--cwd", project.name]
if invocation == "symlink":
link = case / "link"
if not link.is_symlink():
link.symlink_to(project, target_is_directory=True)
return link, ["--cwd", str(link)]
raise ValueError(invocation)
def record_hashes(project, mode):
if mode == "hosted":
# v5 hosted mode keeps no ledger: the lock pin names the uuid.
uuid = hosted_uuid((project / "Pipfile.lock").read_text())
if uuid is None:
raise RuntimeError("the hosted Pipfile.lock pins no patch.socket.dev uuid")
recs = {PURL_BASE: published_record(uuid)}
elif mode == "vendored":
# Vendored mode never writes `.socket/manifest.json`: the ledger
# entry embeds the patch record.
entries = json.loads((project / ".socket/vendor/state.json").read_text())["entries"]
recs = {k: e["record"] for k, e in entries.items() if e.get("record")}
else:
recs = json.loads((project / ".socket/manifest.json").read_text())["patches"]
rec = next(iter(recs.values()))
return (
{n: i["afterHash"] for n, i in rec["files"].items()},
{n: i["beforeHash"] for n, i in rec["files"].items() if i.get("beforeHash")},
rec.get("uuid"),
)
def lock_entries(text):
"""Every (section, key, entry) for urllib3 in a Pipfile.lock text."""
try:
lock = json.loads(text)
except Exception:
return []
out = []
for section, entries in lock.items():
if section == "_meta" or not isinstance(entries, dict):
continue
for key, entry in entries.items():
if key.lower().replace("_", "-") == "urllib3":
out.append((section, key, entry))
return out
def source_keys(text):
return sorted({k for _, _, e in lock_entries(text) if isinstance(e, dict) for k in ("file", "path") if k in e})
# -------------------------------------------------------------- one case
def case_dir(job):
version, shape, mode, invocation = job
suffix = "" if invocation == "in-dir" else "-" + invocation
return root / "captures" / f"{version}-{shape}-{mode}{suffix}"
def backtest(job):
"""Run one case; persist its row (or error) as <case>/result.json."""
version, shape, mode, invocation = job
case = case_dir(job)
try:
row = backtest_case(job)
except Exception as e:
case.mkdir(parents=True, exist_ok=True)
save(case / "result.json", {"pipenv": version, "shape": shape, "mode": mode, "invocation": invocation, "passed": False, "error": str(e)[-3000:], "trace": traceback.format_exc()[-2000:]})
raise
save(case / "result.json", row)
return row
def backtest_case(job):
version, shape, mode, invocation = job
legacy = is_legacy(version)
major = major_of(version)
tool = tool_dir(version)
case = case_dir(job)
if case.exists():
shutil.rmtree(case)
case.mkdir(parents=True)
if major < 3 and shape == "transitive":
# Pipenv 0.x has no `pipfile` module to stamp the Pipfile content
# hash with, so the transitive Pipfile cannot be paired with the
# generated lock (`pipenv install` would re-lock on the mismatch).
return {"pipenv": version, "shape": shape, "mode": mode, "invocation": invocation, "pipfileSpec": None, "supported": False,
"expected": "skipped: Pipenv 0.x cannot stamp the transitive Pipfile's content hash", "checks": {}, "info": {}, "passed": True}
original = native_lock(version, tool, shape)
project = (case / "nested" / "app") if invocation == "subdir" else (case / "project")
project.mkdir(parents=True)
for name in ["Pipfile", "Pipfile.lock"]:
shutil.copyfile(original / name, project / name)
pristine_lock = (project / "Pipfile.lock").read_bytes()
pristine_pipfile = (project / "Pipfile").read_bytes()
spec = json.loads(pristine_lock.decode()).get("_meta", {}).get("pipfile-spec")
row = {"pipenv": version, "shape": shape, "mode": mode, "invocation": invocation, "pipfileSpec": spec, "supported": None, "expected": None, "checks": {}, "info": {}, "passed": None}
checks, info = row["checks"], row["info"]
def check(name, value, note=None):
checks[name] = bool(value)
if note is not None:
info[name] = note
return bool(value)
cwd, cli_args = cli_invocation(case, project, invocation)
def cli_run(penv_, *rest, log):
return Run([cli_bin, *rest, *cli_args, "--json", "--yes", "--no-telemetry"], cwd, penv_, case / log)
cli_bin = cli_path
venv = project / ".venv"
python = venv / "bin/python"
penv = pipenv_env(version, tool)
if mode in ("agent", "agent-oot") and major < 7 and shape in ("marker", "marker-excluded", "extras"):
# Pipenv 0.x–6.x install plain string pins only: inline-table
# entries are mis-handled (markers ignored, extras fail to
# install), so there is nothing meaningful for agent mode to
# measure; hosted/vendored rows for these releases are refusals
# judged before any install.
row["supported"] = False
row["expected"] = "skipped: Pipenv 0.x–6.x mishandle inline-table (markers/extras) Pipfile entries"
row["passed"] = True
return row
# --------------------------------------------------------- agent-oot
if mode == "agent-oot":
if major == 7:
# Pipenv 7 creates its out-of-tree venv through pew + virtualenv
# 16, whose seeding fails inside the python:3.6.15-slim harness
# image ("Can not use any platform or abi specific options");
# its in-project agent leg and the 8–11 out-of-tree legs cover
# the crawler on the legacy layout.
row["supported"] = False
row["expected"] = "skipped: Pipenv 7 cannot create its out-of-tree virtualenv in the harness image"
row["passed"] = True
return row
if major < 3:
row["supported"] = False
row["expected"] = "skipped: Pipenv 0.x has no `--venv` and no WORKON_HOME placement to discover"
row["passed"] = True
return row
workon = case / "venvs"
workon.mkdir()
oenv = pipenv_env(version, tool, in_project=False, workon_home=workon)
# Pipenv creates the venv itself here; pin its interpreter explicitly
# (2022.12.19 ignored PIPENV_PYTHON and picked the newest python3 on
# PATH, whose pkgutil no longer suits its vendored pip).
first = install_args(version, shape) + ([] if legacy else ["--python", str(tool / "bin/python")])
require(run_pipenv(version, tool, first, project, oenv, case / "install-upstream.log"), "pipenv install (out-of-tree)")
vp = run_pipenv(version, tool, ["--venv"], project, oenv, case / "venv-path.log")
candidates = [Path(line.strip()) for line in vp.out.splitlines() if line.strip().startswith("/")]
oot_venv = next((c for c in candidates if (c / "bin/python").exists()), None)
if oot_venv is None:
found = [d for d in workon.iterdir() if (d / "bin/python").exists()]
oot_venv = found[0] if found else None
info["ootVenv"] = str(oot_venv)
info["ootVenvNameMatchesWorkon"] = bool(oot_venv) and oot_venv.parent == workon
if not oot_venv:
raise RuntimeError("could not locate Pipenv's out-of-tree venv: " + vp.out + vp.err)
opython = oot_venv / "bin/python"
# 1. BARE scan: the CLI inherits Pipenv's configuration (WORKON_HOME)
# but not an activation (VIRTUAL_ENV) — exactly what `pipenv run` adds.
bare_env = dict(env)
bare_env["WORKON_HOME"] = str(workon)
bare_env["PATH"] = oenv["PATH"]
r1 = cli_run(bare_env, "scan", "--mode", "agent", "--dry-run", log="scan-bare-dryrun.log")
e1 = r1.json_or_empty()
pkgs = e1.get("packages") or []
u3 = [p for p in pkgs if "urllib3" in (p.get("purl") or "")]
# Legacy `apply.found`: distinct purls among the agent events of
# this dry run (the `verified` previews of the urllib3 selection).
found = len({e.get("purl") for e in leg_events("agent", e1) if e.get("purl")})
# The envelope names packages but not where they live. A crawler that
# found the out-of-tree venv scans exactly its distributions; the
# project-marker fallback scans `python3`-on-PATH instead (here the
# Pipenv tool venv, which may itself carry an urllib3 1.26.18).
cnt = run_python(version, opython, ["-c", "import os,sysconfig; print(sum(1 for d in os.listdir(sysconfig.get_paths()['purelib']) if d.endswith(('.dist-info', '.egg-info'))))"], project, oenv, case / "venv-dist-count.log")
venv_dists = int(cnt.out.strip().splitlines()[-1]) if cnt.ok() and cnt.out.strip() else None
scanned = e1.get("scannedPackages")
sees = bool(u3) and found >= 1 and venv_dists is not None and scanned is not None and abs(scanned - venv_dists) <= 2
info["bareScan"] = {"exit": r1.rc, "scannedPackages": scanned, "venvDistributions": venv_dists, "found": found, "paths": (e1.get("paths") or [])[:10], "urllib3Listed": bool(u3), "foreignInterpreterHit": bool(u3) and found >= 1 and not sees}
check("bareScanSeesPipenvVenv", sees, info["bareScan"])
# 2. apply: bare when the crawler saw the venv, else the way a user
# would — `pipenv run` (modern; exports VIRTUAL_ENV) or an explicit
# VIRTUAL_ENV (legacy: the CLI is a host binary, pipenv lives in Docker).
bare = bool(sees)
if bare:
info["applyPath"] = "bare"
r2 = cli_run(bare_env, "scan", "--mode", "agent", log="scan-apply.log")
elif legacy:
info["applyPath"] = "VIRTUAL_ENV"
venv_env = dict(bare_env, VIRTUAL_ENV=str(oot_venv))
r2 = cli_run(venv_env, "scan", "--mode", "agent", log="scan-apply.log")
else:
info["applyPath"] = "pipenv run"
r2 = Run([tool / "bin/pipenv", "run", cli_bin, "scan", "--mode", "agent", *cli_args, "--json", "--yes", "--no-telemetry"], cwd, oenv, case / "scan-apply.log")
e2 = r2.json_or_empty()
check("scanApplied", applied_count("agent", e2) == 1, {"exit": r2.rc, "applied": applied_count("agent", e2), "path": info["applyPath"], "tail": r2.tail(300) if not r2.ok() else None})
if not checks["scanApplied"]:
row["passed"] = False
return row
after, before, _ = record_hashes(project, "agent")
res = oracle(version, opython, after, project, oenv, case / "oracle-1.log")
check("installedBytesPatched", bool(after) and all(res.get(n) == h for n, h in after.items()), res)
# 3. a repeat install / sync must not revert the in-place patch
ri = run_pipenv(version, tool, install_args(version, shape), project, oenv, case / "install-again.log")
res = oracle(version, opython, after, project, oenv, case / "oracle-2.log")
check("survivesRepeatInstall", ri.ok() and all(res.get(n) == h for n, h in after.items()), {"exit": ri.rc, "oracle": res})
sa = sync_args(version, shape)
if sa:
rs = run_pipenv(version, tool, sa, project, oenv, case / "sync.log")
res = oracle(version, opython, after, project, oenv, case / "oracle-3.log")
check("survivesSync", rs.ok() and all(res.get(n) == h for n, h in after.items()), {"exit": rs.rc, "oracle": res})
# 4. rollback the same way the patch was applied
if bare:
rb = cli_run(bare_env, "rollback", log="rollback.log")
elif legacy:
rb = cli_run(dict(bare_env, VIRTUAL_ENV=str(oot_venv)), "rollback", log="rollback.log")
else:
rb = Run([tool / "bin/pipenv", "run", cli_bin, "rollback", *cli_args, "--json", "--yes", "--no-telemetry"], cwd, oenv, case / "rollback.log")
res = oracle(version, opython, after, project, oenv, case / "oracle-rollback.log")
check("rollbackRestoresUpstream", rb.ok() and bool(before) and all(res.get(n) == h for n, h in before.items()), {"exit": rb.rc, "oracle": res})
mf = project / ".socket/manifest.json"
check("rollbackClearsManifest", not mf.exists() or json.loads(mf.read_text()).get("patches") in ({}, None))
check("lockUntouched", (project / "Pipfile.lock").read_bytes() == pristine_lock and (project / "Pipfile").read_bytes() == pristine_pipfile)
row["supported"] = True
row["passed"] = all(checks.values())
return row
# ------------------------------------------------------------- agent
if mode == "agent":
make_venv(version, tool, venv, project, case / "venv.log", native=True)
require(run_pipenv(version, tool, install_args(version, shape), project, penv, case / "install-upstream.log"), "pipenv install (upstream)")
if shape == "marker-excluded":
check("excludedStaysAbsent", urllib3_absent(version, python, project, penv, case / "absence.log"))
r = cli_run(penv, "scan", "--mode", "agent", log="scan.log")
e = r.json_or_empty()
save(case / "cli-output.json", e)
check("nothingApplied", applied_count("agent", e) == 0, {"exit": r.rc, "applied": applied_count("agent", e)})
check("lockUntouched", (project / "Pipfile.lock").read_bytes() == pristine_lock)
row["supported"] = True
row["expected"] = "marker excludes urllib3: nothing installed, nothing to patch"
row["passed"] = all(checks.values())
return row
r = cli_run(penv, "scan", "--mode", "agent", log="scan.log")
info["scanExit"] = r.rc
envelope = r.json()
save(case / "cli-output.json", envelope)
applied = applied_count("agent", envelope)
check("appliedExactlyOne", applied == 1, {"applied": applied, "status": envelope.get("status"), "events": [{k: e.get(k) for k in ("action", "purl", "errorCode")} for e in leg_events("agent", envelope)][:4]})
if not checks["appliedExactlyOne"]:
row["passed"] = False
return row
check("lockUntouched", (project / "Pipfile.lock").read_bytes() == pristine_lock and (project / "Pipfile").read_bytes() == pristine_pipfile)
after, before, uuid = record_hashes(project, "agent")
info["uuid"] = uuid
res = oracle(version, python, after, project, penv, case / "oracle-1.log")
check("installedBytesPatched", bool(after) and all(res.get(n) == h for n, h in after.items()), res)
r2 = cli_run(penv, "scan", "--mode", "agent", log="rescan.log")
e2 = r2.json_or_empty()
res = oracle(version, python, after, project, penv, case / "oracle-rescan.log")
check("rescanIdempotent", r2.ok() and all(res.get(n) == h for n, h in after.items()) and (project / "Pipfile.lock").read_bytes() == pristine_lock, {"exit": r2.rc, "applied": applied_count("agent", e2), "status": e2.get("status")})
ri = run_pipenv(version, tool, install_args(version, shape), project, penv, case / "install-again.log")
res = oracle(version, python, after, project, penv, case / "oracle-2.log")
check("survivesRepeatInstall", ri.ok() and all(res.get(n) == h for n, h in after.items()), {"exit": ri.rc, "oracle": res, "tail": ri.tail(300) if not ri.ok() else None})
sa = sync_args(version, shape)
if sa:
rs = run_pipenv(version, tool, sa, project, penv, case / "sync.log")
res = oracle(version, python, after, project, penv, case / "oracle-3.log")
check("survivesSync", rs.ok() and all(res.get(n) == h for n, h in after.items()), {"exit": rs.rc, "oracle": res, "tail": rs.tail(300) if not rs.ok() else None})
vx = Run([cli_bin, "vex", "--product", "pkg:pypi/pipenv-backtest-fixture@0.1.0", *cli_args, "--no-telemetry"], cwd, penv, case / "vex.log")
info["vex"] = vex_info(vx)
rb = cli_run(penv, "rollback", log="rollback.log")
erb = rb.json_or_empty()
res = oracle(version, python, after, project, penv, case / "oracle-rollback.log")
check("rollbackExit0", rb.ok(), rb.tail(600) if not rb.ok() else None)
check("rollbackRestoresUpstreamBytes", bool(before) and all(res.get(n) == h for n, h in before.items()), res)
mf = project / ".socket/manifest.json"
check("rollbackClearsManifest", not mf.exists() or json.loads(mf.read_text()).get("patches") in ({}, None))
check("rollbackKeepsLock", (project / "Pipfile.lock").read_bytes() == pristine_lock and (project / "Pipfile").read_bytes() == pristine_pipfile)
info["rollbackEnvelope"] = {k: erb.get(k) for k in ("status", "summary", "hosted", "error", "warnings") if k in erb}
row["supported"] = True
row["passed"] = all(checks.values())
return row
# ------------------------------------------------- hosted / vendored
# Fresh-clone scenario first: nothing installed, lock only. An EMPTY
# in-project venv keeps the crawl hermetic (the project-marker fallback
# would otherwise walk this machine's global interpreters).
make_venv(version, tool, venv, project, case / "venv-empty.log")
r0 = cli_run(penv, "scan", "--mode", mode, log="scan-lockonly.log")
e0 = r0.json_or_empty()
codes0 = envelope_codes(e0)
info["lockOnly"] = {"exit": r0.rc, "applied": applied_count(mode, e0), "lockfileOnlyPackages": e0.get("lockfileOnlyPackages"), "codes": codes0}
check("lockOnlyApplies", applied_count(mode, e0) == 1, info["lockOnly"])
if applied_count(mode, e0) == 1:
# The CI re-run shape: the checkout already carries the committed
# reference and nothing is installed — the re-scan must stay green
# (hosted re-confirms; vendored reports already_vendored), not
# `package_not_installed`, and the lock must not change.
lock1 = (project / "Pipfile.lock").read_bytes()
r1 = cli_run(penv, "scan", "--mode", mode, log="scan-lockonly-rescan.log")
e1 = r1.json_or_empty()
codes1 = envelope_codes(e1)
ok1 = r1.ok() and e1.get("status") == "success" and (project / "Pipfile.lock").read_bytes() == lock1 and "package_not_installed" not in codes1
check("lockOnlyRescanGreen", ok1, {"exit": r1.rc, "status": e1.get("status"), "codes": codes1})
shutil.rmtree(project / ".socket", ignore_errors=True)