253 lines
9.1 KiB
Python
253 lines
9.1 KiB
Python
# 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
|