Skip to content

Commit

Permalink
Refactor: Move class Testcase to testcase.py
Browse files Browse the repository at this point in the history
  • Loading branch information
thorehusfeldt committed Jan 28, 2024
1 parent 3b56f06 commit 6a653a1
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 202 deletions.
15 changes: 8 additions & 7 deletions bin/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
from pathlib import Path, PurePosixPath, PurePath

import config
import inspect
import parallel
import program
import run
import parallel
import inspect
from testcase import Testcase
import validate

from util import *
Expand Down Expand Up @@ -235,7 +236,7 @@ def run_interactive(self, problem, bar, cwd, t):
if interaction_path.is_file():
return True

testcase = run.Testcase(problem, in_path, short_path=(t.path.parent / (t.name + '.in')))
testcase = Testcase(problem, in_path, short_path=(t.path.parent / (t.name + '.in')))
r = run.Run(problem, self.program, testcase)

# No {name}/{seed} substitution is done since all IO should be via stdin/stdout.
Expand Down Expand Up @@ -621,7 +622,7 @@ def up_to_date():
return (False, False)

# Check whether all input validators have been run.
testcase = run.Testcase(problem, infile, short_path=t.path / t.name)
testcase = Testcase(problem, infile, short_path=t.path / t.name)
for h in testcase.validator_hashes(validate.InputValidator):
if h not in meta_yaml.get('validator_hashes', []):
return (True, False)
Expand Down Expand Up @@ -816,7 +817,7 @@ def add_testdata_to_cache():
return

assert infile.is_file(), f'Expected .in file not found in cache: {infile}'
testcase = run.Testcase(problem, infile, short_path=t.path / t.name)
testcase = Testcase(problem, infile, short_path=t.path / t.name)

# Validate the in.
no_validators = config.args.no_validators
Expand Down Expand Up @@ -1093,7 +1094,7 @@ def generate(d, problem, generator_config, bar):
meta_path.is_file()
), f"Metadata file not found for included case {d.path / key}\nwith hash {t.input_hash}\nfile {meta_path}"
meta_yaml = read_yaml(meta_path)
testcase = run.Testcase(problem, infile, short_path=t.path / t.name)
testcase = Testcase(problem, infile, short_path=t.path / t.name)
hashes = testcase.validator_hashes(validate.InputValidator)

# All hashes validated before?
Expand All @@ -1105,7 +1106,7 @@ def up_to_date():

if not up_to_date():
# Validate the testcase input.
testcase = run.Testcase(problem, infile, short_path=new_case)
testcase = Testcase(problem, infile, short_path=new_case)
if not testcase.validate_format(
validate.InputValidator,
bar=bar,
Expand Down
5 changes: 3 additions & 2 deletions bin/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import parallel
import program
import run
import testcase
import validate
from util import *
from colorama import Fore, Style
Expand Down Expand Up @@ -234,7 +235,7 @@ def maybe_copy(x):

testcases = []
for f in in_paths:
t = run.Testcase(p, f)
t = testcase.Testcase(p, f)
# Require both in and ans files
if needinteraction and not t.in_path.with_suffix('.interaction').is_file():
assert only_sample
Expand Down Expand Up @@ -396,7 +397,7 @@ def has_constraints_checking(f):
)
]

skip_double_build_warning = check_constraints #or not paths_for_class[Class.ANSWER]
skip_double_build_warning = check_constraints #or not paths_for_class[Class.ANSWER] TODO not sure about this
validators = [
cls(
problem,
Expand Down
192 changes: 0 additions & 192 deletions bin/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,198 +12,6 @@
from colorama import Fore, Style


class Testcase:
# Testcases outside problem/data must pass in the short_path explicitly.
# In that case, `path` is the (absolute) path to the `.in` file being
# tested, and `short_path` is the name of the testcase relative to
# `problem.path / 'data'`.
def __init__(self, problem, path, *, short_path=None):
assert path.suffix == '.in' or path.suffixes == [".in", ".statement"]

self.problem = problem

self.in_path = path
self.ans_path = (
self.in_path.with_suffix('.ans')
if path.suffix == '.in'
else self.in_path.with_name(self.in_path.with_suffix('').stem + '.ans.statement')
)
if short_path is None:
try:
self.short_path = path.relative_to(problem.path / 'data')
except ValueError:
fatal(f"Testcase {path} is not inside {problem.path / 'data'}.")
else:
assert short_path is not None
self.short_path = short_path

# Display name: everything after data/.
self.name = str(self.short_path.with_suffix(''))

self.bad_input = self.short_path.parts[0] == 'invalid_inputs'
self.bad_output = self.short_path.parts[0] == 'invalid_outputs'

# Backwards compatibility support for `data/bad`.
if self.short_path.parts[0] == 'bad':
self.bad_input = not self.ans_path.is_file()
self.bad_output = self.ans_path.is_file()

self.sample = self.short_path.parts[0] == 'sample'

self.included = False
if path.is_symlink():
include_target = Path(os.path.normpath(path.parent / os.readlink(path)))
if is_relative_to(problem.path / 'data', include_target):
self.included = True
else:
# The case is an unlisted cases included from generators/.
pass

# Get the testdata.yaml content for this testcase.
# Read using the short_path instead of the in_path, because during
# generate the testcase will live in a temporary directory, where
# testdata.yaml doesn't exist.
self.testdata_yaml = problem.get_testdata_yaml(self.problem.path / 'data' / self.short_path)

def with_suffix(self, ext):
return self.in_path.with_suffix(ext)

# Return the flags specified in testdata.yaml for the given validator,
# None if no flags were found, or False if this validator should be skipped.
# If `split`, split the string by spaces.
def testdata_yaml_validator_flags(self, validator_type, validator, split=True):
# Do not use flags when using the default output validator.
if self.problem.settings.validation == 'default' and validator_type == 'output':
return None

if self.testdata_yaml is None:
return None
key = (
'input_validator_flags'
if validator_type == 'input'
else 'output_validator_flags'
)
if key not in self.testdata_yaml:
return None
flags = self.testdata_yaml[key]
# Note: support for lists/dicts for was removed in #259.
if not isinstance(flags, str):
fatal(f'{key} must be a string in testdata.yaml')
return flags.split() if split else flags

# Returns a dict of objects
# hash =>
# - name
# - flags
# - hash
# indicating which validators will be run for the current testcase.
def validator_hashes(self, cls: Type[validate.Validator]):
assert cls in [validate.InputValidator, validate.AnswerValidator]
validators = self.problem.validators(cls) or []

d = dict()

for validator in validators:
flags = self.testdata_yaml_validator_flags(cls, validator, split=False)
if flags is False:
continue
o = {
'name': validator.name,
'flags': flags,
'hash': validator.hash,
}
h = combine_hashes_dict(o)
# Don't actually store the somewhat useless validator hash.
del o['hash']
d[h] = o

return d

def validate_format(
self, cls: Type[validate.Validator], *, bar, constraints=None, warn_instead_of_error=False, args=None
):

bad_testcase = self.bad_input if cls == validate.InputValidator else self.bad_output

success = True

validators = self.problem.validators(cls, check_constraints=constraints != None)
if validators == False:
return True

for validator in validators:
flags = self.testdata_yaml_validator_flags(cls, validator)
if flags is False:
continue
flags = args if flags is None else flags + args

ret = validator.run(self, constraints=None if bad_testcase else constraints, args=flags)

success &= ret.ok is True
message = validator.name + (' accepted' if ret.ok != bad_testcase else ' rejected')

# Print stdout and stderr whenever something is printed
data = ''
if ret.ok is not True or config.args.error:
if ret.err and ret.out:
ret.out = (
ret.err
+ f'\n{Fore.RED}VALIDATOR STDOUT{Style.RESET_ALL}\n'
+ Fore.YELLOW
+ ret.out
)
elif ret.err:
data = ret.err
elif ret.out:
data = ret.out

file = self.in_path if cls == validate.InputValidator else self.ans_path
data += (
f'{Style.RESET_ALL}-> {shorten_path(self.problem, file.parent) / file.name}\n'
)
else:
data = ret.err

bar.part_done(
ret.ok is True, message, data=data, warn_instead_of_error=warn_instead_of_error
)

if ret.ok is True:
continue

# Move testcase to destination directory if specified.
if config.args.move_to:
infile = self.in_path
targetdir = self.problem.path / config.args.move_to
targetdir.mkdir(parents=True, exist_ok=True)
intarget = targetdir / infile.name
infile.rename(intarget)
bar.log('Moved to ' + print_name(intarget))
ansfile = self.ans_path
if ansfile.is_file():
anstarget = intarget.with_suffix('.ans')
ansfile.rename(anstarget)
bar.log('Moved to ' + print_name(anstarget))

# Remove testcase if specified.
elif cls == validate.InputValidator and config.args.remove:
bar.log(Fore.RED + 'REMOVING TESTCASE!' + Style.RESET_ALL)
if self.in_path.exists():
self.in_path.unlink()
if self.ans_path.exists():
self.ans_path.unlink()

break

if success and not bad_testcase:
if cls == validate.InputValidator:
validate.generic_validation(cls, self.in_path, bar=bar)

if cls == validate.AnswerValidator:
validate.generic_validation(cls, self.ans_path, bar=bar)

return success


class Run:
def __init__(self, problem, submission, testcase):
Expand Down
Loading

0 comments on commit 6a653a1

Please sign in to comment.