2019-08-15 12:12:11 -04:00
|
|
|
#!/usr/bin/env python3.7
|
|
|
|
|
import argparse
|
2021-02-03 10:52:31 -05:00
|
|
|
from argparse import Namespace
|
|
|
|
|
from enum import IntEnum, auto
|
2019-08-15 12:12:11 -04:00
|
|
|
from fnmatch import fnmatch
|
|
|
|
|
import logging
|
2021-01-31 16:31:55 -05:00
|
|
|
from pathlib import Path
|
2019-08-15 12:12:11 -04:00
|
|
|
import shlex
|
|
|
|
|
from subprocess import check_call, DEVNULL, CalledProcessError
|
|
|
|
|
import sys
|
2021-02-05 22:01:53 -05:00
|
|
|
from typing import List, Iterator
|
2019-08-15 12:12:11 -04:00
|
|
|
|
2021-02-05 22:01:53 -05:00
|
|
|
from data import Config, ExerciseInfo, ExerciseStatus
|
2021-02-03 10:52:31 -05:00
|
|
|
from generate_tests import clone_if_missing
|
|
|
|
|
from githelp import Repo
|
2021-01-31 16:31:55 -05:00
|
|
|
from test_exercises import check_assignment
|
|
|
|
|
|
2021-02-03 10:52:31 -05:00
|
|
|
DEFAULT_SPEC_LOCATION = Path('.problem-specifications')
|
2019-08-15 12:12:11 -04:00
|
|
|
|
|
|
|
|
logging.basicConfig(format="%(levelname)s:%(message)s")
|
|
|
|
|
logger = logging.getLogger("generator")
|
|
|
|
|
logger.setLevel(logging.WARN)
|
|
|
|
|
|
|
|
|
|
|
2021-02-03 10:52:31 -05:00
|
|
|
class TemplateStatus(IntEnum):
|
2019-08-15 12:12:11 -04:00
|
|
|
OK = auto()
|
|
|
|
|
MISSING = auto()
|
|
|
|
|
INVALID = auto()
|
|
|
|
|
TEST_FAILURE = auto()
|
|
|
|
|
|
|
|
|
|
|
2021-02-03 10:52:31 -05:00
|
|
|
def exec_cmd(cmd: str) -> bool:
|
2019-08-15 12:12:11 -04:00
|
|
|
try:
|
|
|
|
|
args = shlex.split(cmd)
|
|
|
|
|
if logger.isEnabledFor(logging.DEBUG):
|
|
|
|
|
check_call(args)
|
|
|
|
|
else:
|
|
|
|
|
check_call(args, stderr=DEVNULL, stdout=DEVNULL)
|
|
|
|
|
return True
|
|
|
|
|
except CalledProcessError as e:
|
|
|
|
|
logger.debug(str(e))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
2021-01-31 16:31:55 -05:00
|
|
|
def generate_template(exercise: ExerciseInfo, spec_path: Path) -> bool:
|
|
|
|
|
script = Path('bin/generate_tests.py')
|
|
|
|
|
return exec_cmd(f'{script} --verbose --spec-path "{spec_path}" {exercise.slug}')
|
2019-08-15 12:12:11 -04:00
|
|
|
|
|
|
|
|
|
2021-01-31 16:31:55 -05:00
|
|
|
def run_tests(exercise: ExerciseInfo) -> bool:
|
|
|
|
|
return check_assignment(exercise, quiet=True) == 0
|
2019-08-15 12:12:11 -04:00
|
|
|
|
|
|
|
|
|
2021-02-03 10:52:31 -05:00
|
|
|
def get_status(exercise: ExerciseInfo, spec_path: Path) -> TemplateStatus:
|
2021-01-31 16:31:55 -05:00
|
|
|
if exercise.template_path.is_file():
|
2019-08-15 12:12:11 -04:00
|
|
|
if generate_template(exercise, spec_path):
|
|
|
|
|
if run_tests(exercise):
|
2021-01-31 16:31:55 -05:00
|
|
|
logging.info(f"{exercise.slug}: OK")
|
2019-08-15 12:12:11 -04:00
|
|
|
return TemplateStatus.OK
|
|
|
|
|
else:
|
|
|
|
|
return TemplateStatus.TEST_FAILURE
|
|
|
|
|
else:
|
|
|
|
|
return TemplateStatus.INVALID
|
|
|
|
|
else:
|
|
|
|
|
return TemplateStatus.MISSING
|
|
|
|
|
|
|
|
|
|
|
2021-02-03 10:52:31 -05:00
|
|
|
def set_loglevel(opts: Namespace):
|
|
|
|
|
if opts.quiet:
|
|
|
|
|
logger.setLevel(logging.FATAL)
|
|
|
|
|
elif opts.verbose >= 2:
|
|
|
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
|
elif opts.verbose >= 1:
|
|
|
|
|
logger.setLevel(logging.INFO)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def filter_exercises(exercises: List[ExerciseInfo], pattern: str) -> Iterator[ExerciseInfo]:
|
|
|
|
|
for exercise in exercises:
|
2021-02-05 22:01:53 -05:00
|
|
|
if exercise.status != ExerciseStatus.Deprecated:
|
2021-02-03 10:52:31 -05:00
|
|
|
if exercise.type == 'concept':
|
|
|
|
|
# Concept exercises are not generated
|
|
|
|
|
continue
|
|
|
|
|
if fnmatch(exercise["slug"], pattern):
|
|
|
|
|
yield exercise
|
|
|
|
|
|
|
|
|
|
|
2019-08-15 12:12:11 -04:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
|
parser.add_argument("exercise_pattern", nargs="?", default="*", metavar="EXERCISE")
|
|
|
|
|
parser.add_argument("-v", "--verbose", action="count", default=0)
|
|
|
|
|
parser.add_argument("-q", "--quiet", action="store_true")
|
|
|
|
|
parser.add_argument("--stop-on-failure", action="store_true")
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"-p",
|
|
|
|
|
"--spec-path",
|
|
|
|
|
default=DEFAULT_SPEC_LOCATION,
|
2021-01-31 16:31:55 -05:00
|
|
|
type=Path,
|
2019-08-15 12:12:11 -04:00
|
|
|
help=(
|
|
|
|
|
"path to clone of exercism/problem-specifications " "(default: %(default)s)"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
opts = parser.parse_args()
|
2021-02-03 10:52:31 -05:00
|
|
|
set_loglevel(opts)
|
2019-08-15 12:12:11 -04:00
|
|
|
|
2021-01-31 16:31:55 -05:00
|
|
|
if not opts.spec_path.is_dir():
|
2019-08-15 12:12:11 -04:00
|
|
|
logger.error(f"{opts.spec_path} is not a directory")
|
|
|
|
|
sys.exit(1)
|
2021-02-03 10:52:31 -05:00
|
|
|
with clone_if_missing(repo=Repo.ProblemSpecifications, directory=opts.spec_path):
|
|
|
|
|
|
|
|
|
|
result = True
|
|
|
|
|
buckets = {
|
|
|
|
|
TemplateStatus.MISSING: [],
|
|
|
|
|
TemplateStatus.INVALID: [],
|
|
|
|
|
TemplateStatus.TEST_FAILURE: [],
|
|
|
|
|
}
|
|
|
|
|
config = Config.load()
|
|
|
|
|
for exercise in filter_exercises(config.exercises.all()):
|
|
|
|
|
status = get_status(exercise, opts.spec_path)
|
|
|
|
|
if status == TemplateStatus.OK:
|
|
|
|
|
logger.info(f"{exercise.slug}: {status.name}")
|
|
|
|
|
else:
|
|
|
|
|
buckets[status].append(exercise.slug)
|
|
|
|
|
result = False
|
|
|
|
|
if opts.stop_on_failure:
|
|
|
|
|
logger.error(f"{exercise.slug}: {status.name}")
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if not opts.quiet and not opts.stop_on_failure:
|
|
|
|
|
for status, bucket in sorted(buckets.items()):
|
|
|
|
|
if bucket:
|
|
|
|
|
print(f"The following exercises have status '{status.name}'")
|
|
|
|
|
for exercise in sorted(bucket):
|
|
|
|
|
print(f' {exercise}')
|
|
|
|
|
|
|
|
|
|
if not result:
|
|
|
|
|
sys.exit(1)
|