-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfetch.py
More file actions
147 lines (124 loc) · 4.95 KB
/
Copy pathfetch.py
File metadata and controls
147 lines (124 loc) · 4.95 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
"""Fetch the conformance suites this core is held to.
The suites are large, from a hundred megabytes to several gigabytes, and they are
not carried in this repository. This brings down only what is needed: a partial
clone that skips blob history and a sparse checkout that takes only the
directories `suites.json` names.
They are pinned by commit. A build that resolves whatever upstream happens to
hold today is not reproducible, and a change made upstream would turn this
repository red with no commit of its own to explain it. The weekly workflow is
where a newer suite is tried, and it proposes a bump rather than taking one.
"""
import json
import os
import subprocess
import sys
from collections.abc import Mapping
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parent
DEFINITION = ROOT / "suites.json"
PROBE_TIMEOUT = 60
"""Seconds to wait on a question upstream should answer immediately."""
FETCH_TIMEOUT = 3600
"""Seconds to wait on a transfer that is measured in gigabytes."""
def definitions(path: Path | str | None = None) -> list[dict[str, Any]]:
"""Every suite this core declares, as written down.
The default is resolved on the call rather than bound into the signature. A
default evaluated at import time freezes the original path into the function,
so pointing `DEFINITION` elsewhere would appear to work and would quietly go
on reading the original file.
"""
with Path(path or DEFINITION).open() as handle:
suites: list[dict[str, Any]] = json.load(handle)["suites"]
return suites
def checkout_command(
suite: Mapping[str, Any], directory: Path | str, commit: str | None = None
) -> list[list[str]]:
"""The git steps that bring one suite down, without its history or blobs."""
wanted = commit or suite["commit"]
where = str(directory)
return [
["git", "init", "-q", where],
["git", "-C", where, "remote", "add", "origin", suite["repository"]],
["git", "-C", where, "sparse-checkout", "init", "--cone"],
["git", "-C", where, "sparse-checkout", "set", *suite["sparse"]],
[
"git",
"-C",
where,
"fetch",
"-q",
"--depth=1",
"--filter=blob:none",
"origin",
wanted,
],
["git", "-C", where, "checkout", "-q", "FETCH_HEAD"],
]
def _git_environment() -> dict[str, str]:
"""Git that never stops to ask a question.
A prompt for credentials waits for a terminal that a scheduled job does not
have, so the job hangs rather than failing. Refusing the prompt turns that
into an error the caller can report.
"""
return {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
def latest_commit(suite: Mapping[str, Any], timeout: int = PROBE_TIMEOUT) -> str | None:
"""What upstream is at now, or nothing if it cannot be reached in time."""
try:
found = subprocess.run(
["git", "ls-remote", suite["repository"], "HEAD"],
capture_output=True,
text=True,
check=False,
timeout=timeout,
env=_git_environment(),
)
except subprocess.TimeoutExpired:
return None
if found.returncode or not found.stdout.strip():
return None
return found.stdout.split()[0]
def fetch(
suite: Mapping[str, Any],
directory: Path | str,
commit: str | None = None,
quiet: bool = True,
timeout: int = FETCH_TIMEOUT,
) -> Path:
"""Bring one suite down into a directory, returning where its tests live."""
directory = Path(directory)
directory.mkdir(parents=True, exist_ok=True)
for step in checkout_command(suite, directory, commit):
try:
done = subprocess.run(
step,
capture_output=quiet,
text=True,
check=False,
timeout=timeout,
env=_git_environment(),
)
except subprocess.TimeoutExpired:
raise SystemExit(
f"fetching {suite['name']} gave up after {timeout}s at {' '.join(step)}"
) from None
if done.returncode:
raise SystemExit(f"fetching {suite['name']} failed at {' '.join(step)}\n{done.stderr}")
return Path(directory) / str(suite["path"])
def main(argv: list[str], definition: Path | str | None = None) -> int:
directory = (
Path(argv[0])
if argv and not argv[0].startswith("-")
else (Path.home() / ".cache" / "conformance-suites")
)
use_latest = "--latest" in argv
for suite in definitions(definition):
commit = latest_commit(suite) if use_latest else suite["commit"]
if commit is None:
print(f" {suite['name']}: cannot reach {suite['repository']}")
return 1
where = fetch(suite, directory / suite["name"], commit)
print(f"{suite['name']} {commit} {where}")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))