Skip to content

Commit d840219

Browse files
committed
improved runner
1 parent 6dfd614 commit d840219

File tree

2 files changed

+152
-42
lines changed

2 files changed

+152
-42
lines changed

scripts/runall.py

Lines changed: 152 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
WHITE = "\033[97m"
2929
YELLOW = "\033[93m"
3030
RESET = "\033[0m"
31+
BOLD = "\033[1m"
3132
FEINT = "\033[2m"
3233
ITALIC = "\033[3m"
3334
BLINK = "\033[6m"
@@ -38,7 +39,7 @@
3839

3940
@lru_cache(maxsize=None)
4041
def aoc_available_puzzles(
41-
year: int | None = None, seconds: float | None = None
42+
year: t.Optional[int] = None, seconds: t.Optional[float] = None
4243
) -> t.Union[dict[int, list[int]], list[int]]:
4344
"""
4445
Returns a dict of available puzzles by year or the list of available puzzles for the given year.
@@ -71,7 +72,16 @@ def aoc_available_puzzles(
7172
return puzzles
7273

7374

74-
AOC_TARGET_DIR = os.environ.get("AOC_TARGET_DIR", "target")
75+
class Env:
76+
"""
77+
Variables that can be overridden by environment variables.
78+
"""
79+
80+
AOC_TARGET_DIR = os.environ.get("AOC_TARGET_DIR", "target")
81+
CARGO_TARGET_DIR = os.environ.get("CARGO_TARGET_DIR", "target")
82+
CC = os.environ.get("CC", "cc")
83+
CXX = os.environ.get("CXX", "c++")
84+
7585

7686
LANGUAGES = {
7787
"Rust": "src/year{year}/day{day}/day{day}.rs",
@@ -82,7 +92,7 @@ def aoc_available_puzzles(
8292
"JavaScript": "src/year{year}/day{day}/day{day}.js",
8393
"Ruby": "src/year{year}/day{day}/day{day}.rb",
8494
"Perl": "src/year{year}/day{day}/day{day}.pl",
85-
"Bash": "src/year{year}/day{day}/day{day}.sh",
95+
"Bash": "src/year{year}/day{day}/day{day}.bash",
8696
"Java": "{AOC_TARGET_DIR}/build/year{year}/day{day}.class",
8797
"Go": "{AOC_TARGET_DIR}/build/year{year}/day{day}_go",
8898
"C#": "{AOC_TARGET_DIR}/build/year{year}/day{day}_cs.exe",
@@ -113,8 +123,96 @@ def aoc_available_puzzles(
113123
"Ruby": {"Ruby": "ruby"},
114124
"Perl": {"Perl": "perl"},
115125
"Tcl": {"Tcl": "tclsh"},
126+
"Bash": {"Bash": "bash"},
116127
}
117128

129+
LANGUAGES_VERSIONS = {
130+
"Rust": "rustc --version",
131+
"Go": "go version",
132+
"C": "{CC} --version",
133+
"C++": "{CXX} --version",
134+
"Swift": "swiftc -version",
135+
"Java": "javac -version",
136+
"C#": "mcs --version",
137+
"Python": "{interpreter} -VV",
138+
"Ruby": "{interpreter} --version",
139+
"Lua": "{interpreter} -v",
140+
"JavaScript": {"NodeJS": "{interpreter} -v"},
141+
"Perl": "{interpreter} -v",
142+
"Tcl": "echo 'puts [info patchlevel]' | {interpreter}",
143+
"Bash": "{interpreter} --version",
144+
}
145+
146+
147+
def get_language_version(puzzle_lang: str) -> str:
148+
for language, variants in INTERPRETERS.items():
149+
# language = Python
150+
# variants = Py3.11, Py3.12, ...
151+
for variant, interpreters in variants.items():
152+
if variant == puzzle_lang:
153+
# language should be in the "show version" dict
154+
if language not in LANGUAGES_VERSIONS:
155+
continue
156+
157+
# interpreter could be a string or an iterable of strings
158+
if isinstance(interpreters, str):
159+
interpreters = (interpreters,)
160+
161+
# get the first available interpreter
162+
for interpreter in interpreters:
163+
interpreter = interpreter.format(**vars(Env))
164+
if os.path.isfile(shutil.which(interpreter)):
165+
break
166+
else:
167+
return ""
168+
169+
cmd = LANGUAGES_VERSIONS[language]
170+
if isinstance(cmd, dict):
171+
for v, cmd_v in cmd.items():
172+
if v == variant:
173+
cmd = cmd_v
174+
break
175+
else:
176+
# variant has no "show version" command
177+
return ""
178+
179+
cmd = cmd.format(interpreter=interpreter)
180+
cmd = cmd.format(**vars(Env))
181+
try:
182+
lang_version = (
183+
subprocess.check_output(
184+
cmd,
185+
shell=True,
186+
stderr=subprocess.DEVNULL,
187+
)
188+
.decode()
189+
.strip()
190+
.splitlines()[0]
191+
)
192+
return lang_version
193+
except Exception:
194+
return ""
195+
196+
if puzzle_lang in LANGUAGES_VERSIONS:
197+
cmd = LANGUAGES_VERSIONS[puzzle_lang]
198+
cmd = cmd.format(**vars(Env))
199+
try:
200+
lang_version = (
201+
subprocess.check_output(
202+
cmd,
203+
shell=True,
204+
stderr=subprocess.DEVNULL,
205+
)
206+
.decode()
207+
.strip()
208+
.splitlines()[0]
209+
)
210+
return lang_version
211+
except Exception:
212+
return ""
213+
214+
return ""
215+
118216

119217
def get_cache(cache_file: Path = None):
120218
"""Retrieve the cache instance from memory or load it from disk."""
@@ -211,14 +309,15 @@ def run(
211309
nb_expected: int,
212310
year: int,
213311
day: int,
312+
quiet: bool,
214313
) -> t.Dict[str, t.Any]:
215314
"""
216315
TODO
217316
"""
218317

219318
if lang == "Rust":
220319
cmd = []
221-
target = os.environ.get("CARGO_TARGET_DIR", "target")
320+
target = Env.CARGO_TARGET_DIR
222321

223322
f = Path(f"{prog.parent}/{prog.stem}/{target}/release/{prog.stem}")
224323
if f.is_file():
@@ -248,12 +347,14 @@ def run(
248347
cmd.append(file.absolute().as_posix())
249348
cmd.append("--elapsed")
250349

251-
env = os.environ
350+
env = os.environ.copy()
252351
env["NO_COLOR"] = "1"
253352
cmdline = " ".join(map(str, cmd))
254353
cmdline = cmdline.replace(Path(__file__).parent.parent.as_posix() + "/", "")
255354
cmdline = cmdline.replace(Path.home().as_posix(), "~")
256-
print(f"{FEINT}{cmdline}{RESET}", end=TRANSIENT)
355+
356+
if not quiet:
357+
print(f"{FEINT}{cmdline}{RESET}", end=TRANSIENT)
257358

258359
elapsed_measurement = "process"
259360
start = time.time_ns()
@@ -281,7 +382,7 @@ def run(
281382
elif elapsed2.endswith("s"):
282383
elapsed = int(10**9 * float(elapsed2.removesuffix("s")))
283384
else:
284-
print(elapsed, elapsed2, "unknown suffix")
385+
print(elapsed, elapsed2, "unknown time suffix")
285386
exit()
286387

287388
elapsed_measurement = "internal"
@@ -303,7 +404,7 @@ def run(
303404

304405
result = {"elapsed": elapsed, "status": status, "answers": answers}
305406

306-
run_log = Path(AOC_TARGET_DIR) / "run.log"
407+
run_log = Path(Env.AOC_TARGET_DIR) / "run.log"
307408

308409
with run_log.open("at") as f:
309410
line = f"{datetime.now()} {lang} {cmd} {elapsed / 1e9} {elapsed_measurement} {status} '{answers}'"
@@ -317,7 +418,7 @@ def make(year: int, source: Path, dest: Path, language: str, disable_language: t
317418
if not source.is_file():
318419
return
319420

320-
build_dir = Path(f"{AOC_TARGET_DIR}/build/year{year}")
421+
build_dir = Path(f"{Env.AOC_TARGET_DIR}/build/year{year}")
321422
build_dir.mkdir(parents=True, exist_ok=True)
322423

323424
output = build_dir / dest
@@ -326,10 +427,10 @@ def make(year: int, source: Path, dest: Path, language: str, disable_language: t
326427
return
327428

328429
if language == "C":
329-
cmd = "cc -std=c11"
430+
cmd = "{CC} -std=c11".format(CC=Env.CC)
330431
cmdline = f"{cmd} -o {output} -Wall -Wextra -Werror -O3 -DSTANDALONE -I{source.parent} {source}"
331432
elif language == "C++":
332-
cmd = "c++ -std=c++23"
433+
cmd = "{CXX} -std=c++23".format(CXX=Env.CXX)
333434
cmdline = f"{cmd} -o {output} -Wall -Wextra -Werror -O3 -DSTANDALONE -I{source.parent} {source}"
334435
elif language == "Java":
335436
cmdline = f"javac -d {build_dir} {source}"
@@ -497,6 +598,7 @@ def run_day(
497598
dry_run: bool,
498599
terminal_columns: int,
499600
wait: float,
601+
quiet: bool,
500602
):
501603
elapsed = defaultdict(list)
502604

@@ -519,7 +621,7 @@ def run_day(
519621
results = set()
520622

521623
for lang, (pattern, interpreter) in languages.items():
522-
prog = Path(pattern.format(year=year, day=mday, AOC_TARGET_DIR=AOC_TARGET_DIR))
624+
prog = Path(pattern.format(year=year, day=mday, AOC_TARGET_DIR=Env.AOC_TARGET_DIR))
523625
key = ":".join(map(str, (year, day, crc, prog, lang.lower())))
524626

525627
if not prog.is_file():
@@ -540,20 +642,21 @@ def run_day(
540642
if not in_cache and not dry_run:
541643
nb_expected = 1 if day == 25 else 2
542644

543-
e = run(prog, lang, interpreter, file, day_answers.get(crc), nb_expected, year, day)
645+
e = run(prog, lang, interpreter, file, day_answers.get(crc), nb_expected, year, day, quiet)
544646

545647
if e:
546648
update_cache(key, prog, "solutions", e)
547649

548650
if wait is not None:
549-
print(f"{CR}{CLEAR_EOL}waiting {wait:g}s...", end="")
651+
if not quiet:
652+
print(f"{CR}{CLEAR_EOL}waiting {wait:g}s...", end="")
550653
time.sleep(wait)
551654

552655
if not e:
553656
continue
554657

555658
if (e["status"] == "unknown" and day_answers.get(crc)) or e["status"] in ("error", "failed"):
556-
resolve_script = Path(AOC_TARGET_DIR) / f"resolve_{e['status']}.sh"
659+
resolve_script = Path(Env.AOC_TARGET_DIR) / f"resolve_{e['status']}.sh"
557660

558661
if not globals().get(resolve_script):
559662
with resolve_script.open("wt") as f:
@@ -582,7 +685,9 @@ def run_day(
582685
f" {GRAY}{'☽' if in_cache else ' '}"
583686
)
584687

585-
if terminal_columns < 60:
688+
if quiet:
689+
pass
690+
elif terminal_columns < 60:
586691
print(line)
587692
elif terminal_columns < 130:
588693
print(line, f"{status_color}{str(answers)}{RESET}")
@@ -621,7 +726,7 @@ def lookup(interpreter: str) -> str:
621726

622727
if "{" in interpreter:
623728
# interpreter = interpreter.format_map(globals())
624-
interpreter = interpreter.format(AOC_TARGET_DIR=AOC_TARGET_DIR)
729+
interpreter = interpreter.format(AOC_TARGET_DIR=Env.AOC_TARGET_DIR)
625730

626731
if "/" not in interpreter and "\\" not in interpreter:
627732
interpreter = shutil.which(interpreter)
@@ -749,6 +854,7 @@ def main():
749854
parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=28))
750855

751856
parser.add_argument("-v", "--verbose", action="store_true", help="verbose")
857+
parser.add_argument("-q", "--quiet", action="store_true", help="quiet")
752858
parser.add_argument("--cache", type=Path, help="cache database")
753859
parser.add_argument("--working-dir", type=Path, help=argparse.SUPPRESS)
754860

@@ -856,7 +962,7 @@ def main():
856962
# load inputs and answers
857963
inputs, answers = load_data(filter_year, args.filter_user, args.exclude, args.verified)
858964

859-
for script in Path(AOC_TARGET_DIR).glob("resolve_*.sh"):
965+
for script in Path(Env.AOC_TARGET_DIR).glob("resolve_*.sh"):
860966
script.unlink()
861967

862968
# here we go!
@@ -880,7 +986,7 @@ def main():
880986
day_solutions += Path(f"src/year{year}").glob(f"day{day}_*")
881987

882988
for mday in day_solutions:
883-
if prev_shown_year != year:
989+
if not args.quiet and prev_shown_year != year:
884990
if prev_shown_year != 0:
885991
line = (
886992
"==========================" # prefix
@@ -905,17 +1011,19 @@ def main():
9051011
args.dry_run,
9061012
terminal_columns,
9071013
args.wait,
1014+
args.quiet,
9081015
)
9091016

9101017
if elapsed:
911-
if nb_samples > 1:
912-
print(
913-
f"{CR}{CLEAR_EOL}--> ",
914-
" | ".join((f"{lang} : {t:.3f}s" for lang, t in elapsed.items())),
915-
f"{FEINT}({nb_samples} input{'s' if nb_samples > 1 else ''}){RESET}",
916-
)
917-
else:
918-
print(end=f"{CR}{CLEAR_EOL}")
1018+
if not args.quiet:
1019+
if nb_samples > 1:
1020+
print(
1021+
f"{CR}{CLEAR_EOL}--> ",
1022+
" | ".join((f"{lang} : {t:.3f}s" for lang, t in elapsed.items())),
1023+
f"{FEINT}({nb_samples} input{'s' if nb_samples > 1 else ''}){RESET}",
1024+
)
1025+
else:
1026+
print(end=f"{CR}{CLEAR_EOL}")
9191027

9201028
for lang, e in elapsed.items():
9211029
stats_elapsed[year, day, mday, lang] = (e, nb_samples)
@@ -945,22 +1053,24 @@ def main():
9451053
if average < 0.001:
9461054
average = f"{average:10.6f} s"
9471055
else:
948-
average = f"{average:7.3f} s"
949-
950-
lines.append(
951-
(
952-
t,
953-
f"{YELLOW}{lang:<10}{RESET}"
954-
f" : {GREEN}{t:9.3f}s{RESET} for {WHITE}{n:3}{RESET} puzzle{'s' if n > 1 else ' '},"
955-
f" average: {GREEN}{average}{RESET}",
956-
)
1056+
average = f"{average:7.3f} s "
1057+
1058+
stats = (
1059+
f"{YELLOW}{lang:<10}{RESET}"
1060+
f" : {GREEN}{t:9.3f}s{RESET} for {WHITE}{n:3}{RESET} puzzle{'s' if n > 1 else ' '},"
1061+
f" average: {GREEN}{average}{RESET}"
9571062
)
1063+
ver = get_language_version(lang)
1064+
if ver:
1065+
stats += f" ∞ {ITALIC}{ver}{RESET}"
1066+
1067+
lines.append((t, stats))
9581068
total_time += t
9591069
nb_solutions += n
9601070

961-
print()
962-
print("ELAPSED TIME:")
963-
print("\n".join(map(itemgetter(1), sorted(lines))))
1071+
print(f"{CR}{CLEAR_EOL}")
1072+
print(f"{BOLD}ELAPSED TIME:{RESET}")
1073+
print("\n".join(map(itemgetter(1), sorted(lines, key=itemgetter(1)))))
9641074
print(
9651075
"total "
9661076
f" : {GREEN}{total_time:9.3f}s{RESET}"
@@ -1009,11 +1119,11 @@ def main():
10091119
f"{YELLOW}{lang1:<7}{RESET}"
10101120
f" vs. {YELLOW}{lang2:<7}{RESET}:"
10111121
f" {GREEN}{t1 / n:7.3f}s{RESET} vs. {GREEN}{t2 / n:7.3f}s{RESET}"
1012-
f" (x {faster:4.1f} faster) on {WHITE}{n:3}{RESET} puzzle{'s' if n > 1 else ''}",
1122+
f" (x {faster:5.1f} faster) on {WHITE}{n:3}{RESET} puzzle{'s' if n > 1 else ''}",
10131123
)
10141124
)
10151125
print()
1016-
print("LANGUAGES COMPARISON:")
1126+
print(f"{BOLD}LANGUAGES COMPARISON:{RESET}")
10171127
print("\n".join(map(itemgetter(2), sorted(lines))))
10181128

10191129
elif len(languages) > 1:
@@ -1025,7 +1135,7 @@ def main():
10251135
for problem in problems:
10261136
print(problem)
10271137

1028-
for i, f in enumerate(Path(AOC_TARGET_DIR).glob("resolve_*.sh")):
1138+
for i, f in enumerate(Path(Env.AOC_TARGET_DIR).glob("resolve_*.sh")):
10291139
if i == 0:
10301140
print("\nFix errors then run:")
10311141
print(f" {RED}{f}{RESET}")
File renamed without changes.

0 commit comments

Comments
 (0)