Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add -j flag for multi-threaded validation #263

Merged
merged 3 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion problemtools/problem2html.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def convert(options: argparse.Namespace) -> None:
os.chdir(origcwd)


def get_parser() -> argparse.Namespace:
def get_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)

parser.add_argument('-b', '--body-only', dest='bodyonly', action='store_true', help='only generate HTML body, no HTML headers', default=False)
Expand Down
2 changes: 1 addition & 1 deletion problemtools/problem2pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def convert(options: argparse.Namespace) -> bool:

return status == 0

def get_parser() -> argparse.Namespace:
def get_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)

parser.add_argument('-o', '--output', dest='destfile', help="output file name", default='${problem}.pdf')
Expand Down
4 changes: 2 additions & 2 deletions problemtools/run/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@


def find_programs(path, pattern='.*', language_config=None, work_dir=None,
include_dir=None, allow_validation_script=False):
include_dir=None, allow_validation_script=False) -> list[Program]:
"""Find all programs in a directory.

Args:
Expand Down Expand Up @@ -62,7 +62,7 @@ def find_programs(path, pattern='.*', language_config=None, work_dir=None,


def get_program(path, language_config=None, work_dir=None, include_dir=None,
allow_validation_script=False):
allow_validation_script=False) -> Program|None:
pehrsoderman marked this conversation as resolved.
Show resolved Hide resolved
"""Get a Program object for a program

Args:
Expand Down
23 changes: 10 additions & 13 deletions problemtools/run/buildrun.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def __init__(self, path, work_dir=None, include_dir=None):
work_dir (str): name of temp directory in which to run the
scripts (if None, will make new temp directory).
"""
super().__init__()

if not os.path.isdir(path):
raise ProgramError('%s is not a directory' % path)

Expand All @@ -53,32 +55,27 @@ def __init__(self, path, work_dir=None, include_dir=None):
if not os.access(build, os.X_OK):
raise ProgramError('%s/build is not executable' % path)

def __str__(self):
def __str__(self) -> str:
"""String representation"""
return '%s/' % (self.path)


_compile_result = None
def compile(self):
def do_compile(self) -> tuple[bool, str|None]:
pehrsoderman marked this conversation as resolved.
Show resolved Hide resolved
"""Run the build script."""
if self._compile_result is not None:
return self._compile_result

with open(os.devnull, 'w') as devnull:
status = subprocess.call(['./build'], stdout=devnull, stderr=devnull, cwd=self.path)
run = os.path.join(self.path, 'run')

if status:
log.debug('Build script failed (status %d) when compiling %s', status, self.name)
self._compile_result = (False, f'build script failed with exit code {status:d}')
logging.debug('Build script failed (status %d) when compiling %s\n', status, self.name)
return (False, 'build script failed with exit code %d' % (status))
elif not os.path.isfile(run) or not os.access(run, os.X_OK):
self._compile_result = (False, 'build script did not produce an executable called "run"')
return (False, 'build script did not produce an executable called "run"')
else:
self._compile_result = (True, None)
return self._compile_result
return (True, None)


def get_runcmd(self, cwd=None, memlim=None):
def get_runcmd(self, cwd=None, memlim=None) -> list[str]:
"""Run command for the program.

Args:
Expand All @@ -89,6 +86,6 @@ def get_runcmd(self, cwd=None, memlim=None):
return [os.path.join(path, 'run')]


def should_skip_memory_rlimit(self):
def should_skip_memory_rlimit(self) -> bool:
"""Ugly hack (see program.py for details)."""
return True
14 changes: 5 additions & 9 deletions problemtools/run/checktestdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,23 @@ def __init__(self, path):
if Checktestdata._CTD_PATH is None:
raise ProgramError(
'Could not locate the Checktestdata program to run %s' % path)
super(Checktestdata, self).__init__(Checktestdata._CTD_PATH,
args=[path])
super().__init__(Checktestdata._CTD_PATH, args=[path])


def __str__(self):
def __str__(self) -> str:
"""String representation"""
return '%s' % (self.args[0])


_compile_result = None
def compile(self):
def do_compile(self) -> tuple[bool, str|None]:
pehrsoderman marked this conversation as resolved.
Show resolved Hide resolved
"""Syntax-check the Checktestdata script

Returns:
(False, None) if the Checktestdata script has syntax errors and
(True, None) otherwise
"""
if self._compile_result is None:
(status, _) = super(Checktestdata, self).run()
self._compile_result = ((os.WIFEXITED(status) and os.WEXITSTATUS(status) in [0, 1]), None)
return self._compile_result
(status, _) = super().run()
return ((os.WIFEXITED(status) and os.WEXITSTATUS(status) in [0, 1]), None)


def run(self, infile='/dev/null', outfile='/dev/null',
Expand Down
7 changes: 2 additions & 5 deletions problemtools/run/executable.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ def __init__(self, path, args=None):
args: list of additional command line arguments that
should be passed to the program every time it is executed.
"""
super().__init__()

if not os.path.isfile(path) or not os.access(path, os.X_OK):
raise ProgramError('%s is not an executable program' % path)
self.path = path
Expand All @@ -26,11 +28,6 @@ def __str__(self):
"""String representation"""
return '%s' % (self.path)

def compile(self):
"""Dummy implementation of the compile method -- nothing to check!
"""
return (True, None)

def get_runcmd(self, cwd=None, memlim=None):
"""Command to run the program.
"""
Expand Down
22 changes: 19 additions & 3 deletions problemtools/run/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import resource
import signal
import logging
import threading

from .errors import ProgramError

Expand All @@ -14,7 +15,11 @@
class Program(object):
"""Abstract base class for programs.
"""
runtime = 0

def __init__(self) -> None:
self.runtime = 0
self._compile_lock = threading.Lock()
self._compile_result: tuple[bool, str|None]|None = None

def run(self, infile='/dev/null', outfile='/dev/null', errfile='/dev/null',
args=None, timelim=1000, memlim=1024, set_work_dir=False):
Expand Down Expand Up @@ -50,12 +55,23 @@ def run(self, infile='/dev/null', outfile='/dev/null', errfile='/dev/null',

return status, runtime

def code_size(self):
def compile(self) -> tuple[bool, str|None]:
pehrsoderman marked this conversation as resolved.
Show resolved Hide resolved
with self._compile_lock:
if self._compile_result is None:
self._compile_result = self.do_compile()
return self._compile_result

def do_compile(self) -> tuple[bool, str|None]:
pehrsoderman marked this conversation as resolved.
Show resolved Hide resolved
"""Actually compile the program, if needed. Subclasses should override this method.
Do not call this manually -- use compile() instead."""
return (True, None)

def code_size(self) -> int:
"""Subclasses should override this method with the total size of the
source code."""
return 0

def should_skip_memory_rlimit(self):
def should_skip_memory_rlimit(self) -> bool:
"""Ugly workaround to accommodate Java -- the JVM will crash and burn
if there is a memory rlimit applied and this will probably not
change anytime soon [time of writing this: 2017-02-05], see
Expand Down
23 changes: 8 additions & 15 deletions problemtools/run/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def __init__(self, path, language, work_dir=None, include_dir=None):
then the files in include_dir/<foo>/ will be copied
into the work_dir along with the source file(s).
"""
super().__init__()

if path[-1] == '/':
path = path[:-1]
Expand Down Expand Up @@ -80,24 +81,18 @@ def __init__(self, path, language, work_dir=None, include_dir=None):
self.binary = os.path.join(self.path, 'run')


def code_size(self):
def code_size(self) -> int:
return sum(os.path.getsize(x) for x in self.src)


_compile_result = None

def compile(self):
def do_compile(self) -> tuple[bool, str|None]:
pehrsoderman marked this conversation as resolved.
Show resolved Hide resolved
"""Compile the source code.

Returns tuple:
(True, None) if compilation succeeded
(False, errmsg) otherwise
"""
if self._compile_result is not None:
return self._compile_result

if self.language.compile is None:
self._compile_result = (True, None)
return (True, None)

command = self.get_compilecmd()
Expand All @@ -110,14 +105,12 @@ def compile(self):

try:
subprocess.check_output(command, stderr=subprocess.STDOUT)
self._compile_result = (True, None)
return (True, None)
except subprocess.CalledProcessError as err:
self._compile_result = (False, err.output.decode('utf8', 'replace'))

return self._compile_result
return (False, err.output.decode('utf8', 'replace'))


def get_compilecmd(self):
def get_compilecmd(self) -> list[str]:
return shlex.split(self.language.compile.format(**self.__get_substitution()))


Expand All @@ -140,12 +133,12 @@ def get_runcmd(self, cwd=None, memlim=1024):
return shlex.split(self.language.run.format(**subs))


def should_skip_memory_rlimit(self):
def should_skip_memory_rlimit(self) -> bool:
"""Ugly hack (see program.py for details)."""
return self.language.name in ['Java', 'Scala', 'Kotlin', 'Common Lisp']


def __str__(self):
def __str__(self) -> str:
"""String representation"""
return '%s (%s)' % (self.name, self.language.name)

Expand Down
12 changes: 4 additions & 8 deletions problemtools/run/viva.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,22 @@ def __init__(self, path):
if Viva._VIVA_PATH is None:
raise ProgramError(
'Could not locate the VIVA program to run %s' % path)
super(Viva, self).__init__(Viva._VIVA_PATH,
args=[path])
super().__init__(Viva._VIVA_PATH, args=[path])


def __str__(self):
"""String representation"""
return '%s' % (self.args[0])


_compile_result = None
def compile(self):
def do_compile(self) -> tuple[bool, str|None]:
pehrsoderman marked this conversation as resolved.
Show resolved Hide resolved
"""Syntax-check the VIVA script

Returns:
(False, None) if the VIVA script has syntax errors and (True, None) otherwise
"""
if self._compile_result is None:
(status, _) = super(Viva, self).run()
self._compile_result = ((os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0), None)
return self._compile_result
(status, _) = super().run()
return ((os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0), None)


def run(self, infile='/dev/null', outfile='/dev/null',
Expand Down
Loading