aoc-2022/venv/Lib/site-packages/aocd/runner.py

253 lines
9.1 KiB
Python
Raw Normal View History

# coding: utf-8
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import itertools
import logging
import os
import sys
import tempfile
import time
from argparse import ArgumentParser
from collections import OrderedDict
from datetime import datetime
import pebble.concurrent
import pkg_resources
from functools import partial
from termcolor import colored
from .exceptions import AocdError
from .models import AOCD_CONFIG_DIR
from .models import _load_users
from .models import Puzzle
from .utils import AOC_TZ
from .utils import _cli_guess
# from https://adventofcode.com/about
# every problem has a solution that completes in at most 15 seconds on ten-year-old hardware
DEFAULT_TIMEOUT = 60
log = logging.getLogger(__name__)
def main():
entry_points = pkg_resources.iter_entry_points(group="adventofcode.user")
plugins = OrderedDict([(ep.name, ep) for ep in entry_points])
aoc_now = datetime.now(tz=AOC_TZ)
years = range(2015, aoc_now.year + int(aoc_now.month == 12))
days = range(1, 26)
users = _load_users()
log_levels = "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
parser = ArgumentParser(description="AoC runner")
parser.add_argument("-p", "--plugins", nargs="+", choices=plugins)
parser.add_argument("-y", "--years", type=int, nargs="+", choices=years)
parser.add_argument("-d", "--days", type=int, nargs="+", choices=days)
parser.add_argument("-u", "--users", nargs="+", choices=users, type=partial(_cli_guess, choices=users))
parser.add_argument("-t", "--timeout", type=int, default=DEFAULT_TIMEOUT)
parser.add_argument("-s", "--no-submit", action="store_true", help="disable autosubmit")
parser.add_argument("-r", "--reopen", action="store_true", help="open browser on NEW solves")
parser.add_argument("-q", "--quiet", action="store_true", help="capture output from runner")
parser.add_argument("--log-level", default="WARNING", choices=log_levels)
args = parser.parse_args()
if not users:
path = os.path.join(AOCD_CONFIG_DIR, "tokens.json")
print(
"There are no datasets available to use.\n"
"Either export your AOC_SESSION or put some auth "
"tokens into {}".format(path),
file=sys.stderr,
)
sys.exit(1)
if not plugins:
print(
"There are no plugins available. Install some package(s) with a registered 'adventofcode.user' entry-point.\n"
"See https://github.com/wimglenn/advent-of-code-sample for an example plugin package structure.",
file=sys.stderr,
)
sys.exit(1)
logging.basicConfig(level=getattr(logging, args.log_level))
rc = run_for(
plugins=args.plugins or list(plugins),
years=args.years or years,
days=args.days or days,
datasets={k: users[k] for k in (args.users or users)},
timeout=args.timeout,
autosubmit=not args.no_submit,
reopen=args.reopen,
capture=args.quiet,
)
sys.exit(rc)
def _timeout_wrapper(f, capture=False, timeout=DEFAULT_TIMEOUT, *args, **kwargs):
func = pebble.concurrent.process(daemon=False, timeout=timeout)(_process_wrapper)
return func(f, capture, *args, **kwargs)
def _process_wrapper(f, capture=False, *args, **kwargs):
# allows to run f in a process which can be killed if it misbehaves
prev_stdout = sys.stdout
prev_stderr = sys.stderr
if capture:
hush = open(os.devnull, "w")
sys.stdout = sys.stderr = hush
try:
result = f(*args, **kwargs)
finally:
if capture:
sys.stdout = prev_stdout
sys.stderr = prev_stderr
hush.close()
return result
def run_with_timeout(entry_point, timeout, progress, dt=0.1, capture=False, **kwargs):
spinner = itertools.cycle(r"\|/-")
line = elapsed = format_time(0)
t0 = time.time()
func = entry_point.load()
future = _timeout_wrapper(func, capture=capture, timeout=timeout, **kwargs)
while not future.done():
if progress is not None:
line = "\r" + elapsed + " " + progress + " " + next(spinner)
sys.stderr.write(line)
sys.stderr.flush()
time.sleep(dt)
elapsed = format_time(time.time() - t0, timeout)
walltime = time.time() - t0
try:
a, b = future.result()
except Exception as err:
a = b = ""
error = repr(err)[:50]
else:
error = ""
# longest correct answer seen so far has been 32 chars
a = str(a)[:50]
b = str(b)[:50]
if progress is not None:
sys.stderr.write("\r" + " " * len(line) + "\r")
sys.stderr.flush()
return a, b, walltime, error
def format_time(t, timeout=DEFAULT_TIMEOUT):
if t < timeout / 4:
color = "green"
elif t < timeout / 2:
color = "yellow"
else:
color = "red"
runtime = colored("{: 7.2f}s".format(t), color)
return runtime
def run_one(year, day, input_data, entry_point, timeout=DEFAULT_TIMEOUT, progress=None, capture=False):
prev = os.getcwd()
scratch = tempfile.mkdtemp(prefix="{}-{:02d}-".format(year, day))
os.chdir(scratch)
assert not os.path.exists("input.txt")
try:
with open("input.txt", "w") as f:
f.write(input_data)
a, b, walltime, error = run_with_timeout(
entry_point=entry_point,
timeout=timeout,
year=year,
day=day,
data=input_data,
progress=progress,
capture=capture,
)
finally:
os.unlink("input.txt")
os.chdir(prev)
try:
os.rmdir(scratch)
except Exception as err:
log.warning("failed to remove scratch %s (%s: %s)", scratch, type(err), err)
return a, b, walltime, error
def run_for(plugins, years, days, datasets, timeout=DEFAULT_TIMEOUT, autosubmit=True, reopen=False, capture=False):
aoc_now = datetime.now(tz=AOC_TZ)
all_entry_points = pkg_resources.iter_entry_points(group="adventofcode.user")
entry_points = {ep.name: ep for ep in all_entry_points if ep.name in plugins}
it = itertools.product(years, days, plugins, datasets)
userpad = 3
datasetpad = 8
n_incorrect = 0
if entry_points:
userpad = len(max(entry_points, key=len))
if datasets:
datasetpad = len(max(datasets, key=len))
for year, day, plugin, dataset in it:
if year == aoc_now.year and day > aoc_now.day:
continue
token = datasets[dataset]
entry_point = entry_points[plugin]
os.environ["AOC_SESSION"] = token
puzzle = Puzzle(year=year, day=day)
title = puzzle.title
progress = "{}/{:<2d} - {:<40} {:>%d}/{:<%d}"
progress %= (userpad, datasetpad)
progress = progress.format(year, day, title, plugin, dataset)
a, b, walltime, error = run_one(
year=year,
day=day,
input_data=puzzle.input_data,
entry_point=entry_point,
timeout=timeout,
progress=progress,
capture=capture,
)
runtime = format_time(walltime, timeout)
line = " ".join([runtime, progress])
if error:
assert a == b == ""
icon = colored("", "red")
n_incorrect += 1
line += " {icon} {error}".format(icon=icon, error=error)
else:
result_template = " {icon} part {part}: {answer}"
for answer, part in zip((a, b), "ab"):
if day == 25 and part == "b":
# there's no part b on christmas day, skip
continue
expected = None
try:
expected = getattr(puzzle, "answer_" + part)
except AttributeError:
post = part == "a" or (part == "b" and puzzle.answered_a)
if autosubmit and post:
try:
puzzle._submit(answer, part, reopen=reopen, quiet=True)
except AocdError as err:
log.warning("error submitting - %s", err)
try:
expected = getattr(puzzle, "answer_" + part)
except AttributeError:
pass
correct = expected is not None and str(expected) == answer
icon = colored("", "green") if correct else colored("", "red")
correction = ""
if not correct:
n_incorrect += 1
if expected is None:
icon = colored("?", "magenta")
correction = "(correct answer unknown)"
else:
correction = "(expected: {})".format(expected)
answer = "{} {}".format(answer, correction)
if part == "a":
answer = answer.ljust(30)
line += result_template.format(icon=icon, part=part, answer=answer)
print(line)
return n_incorrect