Skip to content

Commit

Permalink
improve code
Browse files Browse the repository at this point in the history
  • Loading branch information
mzuenni committed Jan 28, 2025
1 parent 01ab72e commit 22770d9
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 78 deletions.
4 changes: 3 additions & 1 deletion bin/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@ def run(self, bar, cwd):
ans_path = cwd / 'testcase.ans'

# No {name}/{seed} substitution is done since all IO should be via stdin/stdout.
result = self.program.run(in_path, ans_path, args=self.args, cwd=cwd, default_timeout=True)
result = self.program.run(
in_path, ans_path, args=self.args, cwd=cwd, generator_timeout=True
)

if result.status == ExecStatus.TIMEOUT:
bar.debug(f'{Style.RESET_ALL}-> {shorten_path(self.problem, cwd)}')
Expand Down
49 changes: 21 additions & 28 deletions bin/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ def _read_settings(self):
'memory': 2048, # in MiB
'output': 8, # in MiB
'code': 128, # in KiB
# 'compilation_time': 60, # in seconds
# 'compilation_memory': 2048, # in MiB
'compilation_time': 60, # in seconds
'compilation_memory': 2048, # in MiB
'validation_time': 60, # in seconds
'validation_memory': 2048, # in MiB
# 'validation_output': 8, # in MiB
Expand Down Expand Up @@ -572,12 +572,24 @@ def validators(
singleton list(OutputValidator) if cls is OutputValidator
list(Validator) otherwise, maybe empty
"""
validators = problem._validators(cls, check_constraints)
if not strict and cls == validate.AnswerValidator:
return problem._validators(cls, check_constraints) + problem._validators(
validate.OutputValidator, check_constraints
)
else:
return problem._validators(cls, check_constraints)
validators += problem._validators(validate.OutputValidator, check_constraints)

# Check that the proper number of validators is present
match cls, len(validators):
case validate.InputValidator, 0:
warn(f'No input validators found.')
case validate.AnswerValidator, 0:
log(f"No answer validators found")
case validate.OutputValidator, l if l != 1:
error(f'Found {len(validators)} output validators, expected exactly one.')

build_ok = all(v.ok for v in validators)

# All validators must build.
# TODO Really? Why not at least return those that built?
return validators if build_ok else []

def _validators(
problem, cls: Type[validate.AnyValidator], check_constraints=False
Expand All @@ -586,7 +598,6 @@ def _validators(
key = (cls, check_constraints)
if key in problem._validators_cache:
return problem._validators_cache[key]
ok = True

assert hasattr(cls, 'source_dirs')
paths = [p for source_dir in cls.source_dirs for p in glob(problem.path / source_dir, '*')]
Expand All @@ -597,17 +608,6 @@ def _validators(
error("Validation is default but custom output validator exists (ignoring it)")
paths = [config.tools_root / 'support' / 'default_output_validator.cpp']

# Check that the proper number of validators is present
match cls, len(paths):
case validate.InputValidator, 0:
warn(f'No input validators found.')
case validate.AnswerValidator, 0:
log(f"No answer validators found")
case validate.OutputValidator, l if l != 1:
print(cls)
error(f'Found {len(paths)} output validators, expected exactly one.')
ok = False

# TODO: Instead of checking file contents, maybe specify this in generators.yaml?
def has_constraints_checking(f):
if not f.is_file():
Expand Down Expand Up @@ -640,23 +640,16 @@ def has_constraints_checking(f):
for path in paths
]
bar = ProgressBar(f'Building {cls.validator_type} validator', items=validators)
build_ok = True

def build_program(p):
nonlocal build_ok
localbar = bar.start(p)
build_ok &= p.build(localbar)
p.build(localbar)
localbar.done()

parallel.run_tasks(build_program, validators)

bar.finalize(print_done=False)

# All validators must build.
# TODO Really? Why not at least return those that built?
result = validators if ok and build_ok else []

problem._validators_cache[key] = result
problem._validators_cache[key] = validators
return validators

# get all testcses and submissions and prepare the output validator
Expand Down
43 changes: 36 additions & 7 deletions bin/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ def sanitizer():
# - language: the detected language
# - env: the environment variables used for compile/run command substitution
# - hash: a hash of all of the program including all source files
# - limits a dict of the optional limts, keys are:
# - code
# - compilation_time
# - compilation_memory
# - timeout
# - memory
#
# After build() has been called, the following are available:
# - run_command: command to be executed. E.g. ['/path/to/run'] or ['python3', '/path/to/main.py']. `None` if something failed.
Expand All @@ -111,6 +117,7 @@ def __init__(
deps: Optional[list[Path]] = None,
*,
skip_double_build_warning=False,
limits: dict[str, int] = {},
):
if deps is not None:
assert isinstance(self, Generator)
Expand Down Expand Up @@ -149,7 +156,7 @@ def __init__(
self.run_command: Optional[list[str]] = None
self.hash: Optional[str] = None
self.env: dict[str, int | str | Path] = {}
self.memory: Optional[int] = None
self.limits: dict[str, int] = limits

self.ok = True
self.built = False
Expand Down Expand Up @@ -276,7 +283,7 @@ def _get_language(self, bar: ProgressBar):
'mainclass': mainclass,
'Mainclass': mainclass[0].upper() + mainclass[1:],
# Memory limit in MB.
'memlim': self.memory or 2048,
'memlim': self.limits.get('memory', 2048),
# Out-of-spec variables used by 'manual' and 'Viva' languages.
'build': (
self.tmpdir / 'build' if (self.tmpdir / 'build') in self.input_files else ''
Expand Down Expand Up @@ -376,6 +383,8 @@ def _compile(self, bar: ProgressBar):
cwd=self.tmpdir,
# Compile errors are never cropped.
crop=False,
timeout=self.limits.get('compilation_time', None),
memory=self.limits.get('compilation_memory', None),
)
except FileNotFoundError as err:
self.ok = False
Expand Down Expand Up @@ -478,6 +487,14 @@ def build(self, bar: ProgressBar):
if self.path in self.problem._program_callbacks:
for c in self.problem._program_callbacks[self.path]:
c(self)

if 'code' in self.limits:
size = sum(f.stat().st_size for f in self.source_files)
if size > self.limits['code'] * 1024:
bar.warn(
f'Code limit exceeded (set limits->code to at least {(size + 1023) // 1024}KiB in problem.yaml)'
)

return True

@staticmethod
Expand All @@ -489,7 +506,9 @@ def add_callback(problem, path, c):

class Generator(Program):
def __init__(self, problem: "Problem", path: Path, **kwargs):
super().__init__(problem, path, 'generators', **kwargs)
super().__init__(
problem, path, 'generators', limits={'timeout': problem.limits.generator_time}, **kwargs
)

# Run the generator in the given working directory.
# May write files in |cwd| and stdout is piped to {name}.in if it's not written already.
Expand All @@ -509,11 +528,15 @@ def run(self, bar, cwd, name, args=[]):
else:
f.unlink()

timeout = self.problem.limits.generator_time
timeout = self.limits['timeout']

with stdout_path.open('w') as stdout_file:
result = exec_command(
self.run_command + args, stdout=stdout_file, timeout=timeout, cwd=cwd, memory=None
self.run_command + args,
stdout=stdout_file,
cwd=cwd,
timeout=timeout,
memory=None,
)

result.retry = False
Expand Down Expand Up @@ -544,15 +567,21 @@ def run(self, bar, cwd, name, args=[]):

class Visualizer(Program):
def __init__(self, problem: "Problem", path: Path, **kwargs):
super().__init__(problem, path, 'visualizers', **kwargs)
super().__init__(
problem,
path,
'visualizers',
limits={'timeout': problem.limits.visualizer_time},
**kwargs,
)

# Run the visualizer.
# Stdin and stdout are not used.
def run(self, cwd, args=[]):
assert self.run_command is not None
return exec_command(
self.run_command + args,
timeout=self.problem.limits.visualizer_time,
cwd=cwd,
timeout=self.limits['timeout'],
memory=None,
)
61 changes: 35 additions & 26 deletions bin/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,21 @@ def _validate_output(self, bar):
class Submission(program.Program):
def __init__(self, problem, path, skip_double_build_warning=False):
super().__init__(
problem, path, 'submissions', skip_double_build_warning=skip_double_build_warning
problem,
path,
'submissions',
limits={
'code': problem.limits.code,
'compilation_time': problem.limits.compilation_time,
'compilation_memory': problem.limits.compilation_memory,
'timeout': problem.settings.timeout,
'memory': problem.limits.memory,
},
skip_double_build_warning=skip_double_build_warning,
)

self.verdict = None
self.duration = None
self.memory = problem.limits.memory

# The first element will match the directory the file is in, if possible.
self.expected_verdicts = self._get_expected_verdicts()
Expand All @@ -245,16 +254,6 @@ def __init__(self, problem, path, skip_double_build_warning=False):
self.expected_verdicts += wrong_verdicts
break

def build(self, bar: ProgressBar):
ok = super().build(bar)
if ok:
size = sum(f.stat().st_size for f in self.source_files)
if size > self.problem.limits.code * 1024:
bar.warn(
f'Code limit exceeded (set limits->code to at least {(size + 1023) // 1024}KiB in problem.yaml)'
)
return ok

def _get_expected_verdicts(self) -> list[Verdict]:
expected_verdicts = []

Expand Down Expand Up @@ -312,9 +311,9 @@ def _get_expected_verdicts(self) -> list[Verdict]:
# Run submission on in_path, writing stdout to out_path or stdout if out_path is None.
# args is used by SubmissionInvocation to pass on additional arguments.
# Returns ExecResult
# The `default_timeout` argument is used when a submission is run as a solution when
# The `generator_timeout` argument is used when a submission is run as a solution when
# generating testcases.
def run(self, in_path, out_path, crop=True, args=[], cwd=None, default_timeout=False):
def run(self, in_path, out_path, crop=True, args=[], cwd=None, generator_timeout=False):
assert self.run_command is not None
# Just for safety reasons, change the cwd.
if cwd is None:
Expand All @@ -329,8 +328,12 @@ def run(self, in_path, out_path, crop=True, args=[], cwd=None, default_timeout=F
stdin=inf,
stdout=out_file,
stderr=None if out_file is None else True,
timeout=True if default_timeout else self.problem.settings.timeout,
memory=self.memory,
timeout=(
self.problem.limits.generator_time
if generator_timeout
else self.limits['timeout']
),
memory=self.limits['memory'],
cwd=cwd,
)
if out_file:
Expand Down Expand Up @@ -361,7 +364,7 @@ def run_testcases(

verdicts = Verdicts(
testcases,
self.problem.settings.timeout,
self.limits['timeout'],
run_until,
)

Expand Down Expand Up @@ -430,7 +433,7 @@ def process_run(run: Run):
color = f'{Style.DIM}'
else:
color = Fore.GREEN if got_expected else Fore.RED
timeout = result.duration >= self.problem.settings.timeout
timeout = result.duration >= self.limits['timeout']
duration_style = Style.BRIGHT if timeout else ''
passmsg = (
f':{Fore.CYAN}{result.pass_id}{Style.RESET_ALL}' if self.problem.multipass else ''
Expand Down Expand Up @@ -463,9 +466,7 @@ def process_run(run: Run):

(salient_testcase, salient_duration) = verdicts.salient_testcase()
salient_print_verdict = self.verdict
salient_duration_style = (
Style.BRIGHT if salient_duration >= self.problem.settings.timeout else ''
)
salient_duration_style = Style.BRIGHT if salient_duration >= self.limits['timeout'] else ''

# Summary line is the only thing shown.
message = f'{color}{salient_print_verdict.short():>3}{salient_duration_style}{salient_duration:6.3f}s{Style.RESET_ALL} {Style.DIM}@ {salient_testcase:{max_testcase_len}}{Style.RESET_ALL}'
Expand All @@ -486,7 +487,7 @@ def process_run(run: Run):
)

slowest_duration_style = (
Style.BRIGHT if slowest_duration >= self.problem.settings.timeout else ''
Style.BRIGHT if slowest_duration >= self.limits['timeout'] else ''
)

message += f' {Style.DIM}{Fore.CYAN}slowest{Fore.RESET}:{Style.RESET_ALL} {slowest_color}{slowest_verdict.short():>3}{slowest_duration_style}{slowest_duration:6.3f}s{Style.RESET_ALL} {Style.DIM}@ {slowest_testcase}{Style.RESET_ALL}'
Expand Down Expand Up @@ -524,11 +525,12 @@ def test(self):
stdin=inf,
stdout=None,
stderr=None,
timeout=self.problem.settings.timeout,
timeout=self.limits['timeout'],
memory=self.limits['memory'],
)

assert result.err is None and result.out is None
if result.duration >= self.problem.settings.timeout:
if result.duration >= self.limits['timeout']:
status = f'{Fore.RED}Aborted!'
config.n_error += 1
elif not result.status and result.status != ExecStatus.TIMEOUT:
Expand Down Expand Up @@ -593,7 +595,13 @@ def test_interactive(self):
bar.start(name)
# Reinitialize the underlying program, so that changed to the source
# code can be picked up in build.
super().__init__(self.problem, self.path, self.subdir, skip_double_build_warning=True)
super().__init__(
self.problem,
self.path,
self.subdir,
limits=self.limits,
skip_double_build_warning=True,
)
bar.log('from stdin' if is_tty else 'from file')

# Launch a separate thread to pass stdin to a pipe.
Expand Down Expand Up @@ -634,7 +642,8 @@ def test_interactive(self):
stdin=r,
stdout=None,
stderr=None,
timeout=None,
timeout=None, # no timeout since we wait for user input
memory=self.limits['memory'],
)

assert result.err is None and result.out is None
Expand Down
3 changes: 0 additions & 3 deletions bin/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -1031,9 +1031,6 @@ def exec_command(
if 'timeout' in kwargs:
if kwargs['timeout'] is None:
timeout = None
elif kwargs['timeout'] is True:
# Use the default timeout.
pass
elif kwargs['timeout']:
timeout = kwargs['timeout']
kwargs.pop('timeout')
Expand Down
Loading

0 comments on commit 22770d9

Please sign in to comment.