def run(self, in_path, out_path, args=None, timeout=None): # pylint: disable=too-many-locals """Run binary redirecting standard input and output. Args: in_path (Optional[FilePath]): Path to redirect stdin from. If None input is redirected from /dev/null. out_path (Optional[FilePath]): File to redirec stdout to. If None output is redirected to /dev/null. args (List[str]): Additional parameters Returns: (bool, str, float): Returns a tuple (status, time, errmsg). status is True if the execution terminates with exit code zero or False otherwise. time corresponds to execution time. if status is False errmsg contains an explanatory error message, otherwise it contains a success message. """ args = args or [] assert in_path is None or in_path.exists() with contextlib.ExitStack() as stack: if in_path is None: in_path = FilePath('/dev/null') in_file = stack.enter_context(in_path.open('r')) if not out_path: out_path = FilePath('/dev/null') out_file = stack.enter_context(out_path.open('w')) start = pytime.monotonic() self._cmd.extend(args) try: complete = subprocess.run( self._cmd, timeout=timeout, stdin=in_file, stdout=out_file, universal_newlines=True, stderr=subprocess.PIPE) except subprocess.TimeoutExpired: return (False, pytime.monotonic() - start, 'Execution timed out') time = pytime.monotonic() - start ret = complete.returncode status = ret == 0 msg = 'OK' if not status: stderr = complete.stderr.strip('\n') if stderr and len(stderr) < 100: msg = stderr else: if ret < 0: sig = -ret msg = 'Execution killed with signal %d' % sig if sig in SIGNALS: msg += ': %s' % SIGNALS[sig] else: msg = 'Execution ended with error (return code %d)' % ret return (status, time, msg)
class Statement: """Represents a statement. A statement is formed by a latex source and a pdf file. """ def __init__(self, directory, num=None, codename=None): """ Args: directory (Directory): Directory to search for statement source file. num (int): Number of the statement in the contest starting from 0 """ assert FilePath(directory, 'statement.tex').exists() self._source = FilePath(directory, 'statement.tex') self._pdf = self._source.chext('.pdf') self._compiler = LatexCompiler() self._directory = directory self._num = num self._codename = codename @property def pdf(self): """Returns path to pdf file and compiles it if necessary. Returns: Optional[FilePath]: The file path if the binary is present or None if the pdf file cannot be generated. """ if self._pdf.mtime() < self._source.mtime(): (st, _msg) = self.build() if not st: return None return self._pdf def __str__(self): return str(self._source) @ui.work('PDF') def build(self, blank_page=False): """Compile statement latex source Args: blank_page (Optional[bool]) if true adds a blank page at the end of the problem. Returns: (bool, msg) a tuple containing status code and result message. """ if self._num is not None: os.environ['OCIMATIC_PROBLEM_NUMBER'] = chr(ord('A') + self._num) if self._codename: os.environ['OCIMATIC_CODENAME'] = self._codename if blank_page: os.environ['OCIMATIC_BLANK_PAGE'] = 'True' st = self._compiler(self._source) return (st, 'OK' if st else 'FAILED') def io_samples(self): """Find sample input data in the satement Returns: List[FilePath]: list of paths """ latex_file = self._source.open('r') samples = set() for line in latex_file: m = re.match(r'[^%]*\\sampleIO(\[[^\]]*\]){0,2}{([^}]+)}', line) if m: samples.add(m.group(2)) latex_file.close() return [FilePath(self._directory, s) for s in samples]
class DatasetPlan: """Functionality to read and run a plan for generating dataset.""" def __init__(self, directory, task_directory, dataset_directory, filename='testplan.txt'): self._directory = directory self._testplan_path = FilePath(directory, filename) if not self._testplan_path.exists(): ui.fatal_error('No such file plan for creating dataset: "%s"' % self._testplan_path) self._task_directory = task_directory self._dataset_directory = dataset_directory self._cpp_compiler = CppCompiler() self._java_compiler = JavaCompiler() def test_filepath(self, stn, group, i): st_dir = FilePath(self._dataset_directory, 'st%d' % stn).get_or_create_dir() return FilePath(st_dir, '%s-%d.in' % (group, i)) def validate_input(self): (_, cmds) = self.parse_file() for (st, subtask) in sorted(cmds.items()): self.validate_subtask(st, subtask) @ui.workgroup('Subtask {1}') def validate_subtask(self, stn, subtask): validator = None if subtask['validator']: (validator, msg) = self.build_validator(subtask['validator']) if validator is None: ui.show_message('Warning', 'Failed to build validator: %s' % msg, ui.WARNING) else: ui.show_message('Info', 'No validator specified', ui.INFO) if validator: for (group, tests) in sorted(subtask['groups'].items()): for (i, _) in enumerate(tests, 1): test_file = self.test_filepath(stn, group, i) self.validate_test_input(test_file, validator) @ui.work('Validating', '{1}') def validate_test_input(self, test_file, validator): if not test_file.exists(): return False, 'Test file does not exist' (st, _time, msg) = validator.run(test_file, None) return st, msg def build_validator(self, source): fp = FilePath(self._directory, source) if not fp.exists(): return (None, 'File does not exists.') if fp.ext == '.cpp': binary = fp.chext('.bin') if binary.mtime() < fp.mtime() and not self._cpp_compiler(fp, binary): return (None, 'Failed to build validator.') return (Runnable(binary), 'OK') if fp.ext in ['.py', '.py3']: return (Runnable('python3', [str(source)]), 'OK') if fp.ext == '.py2': return (Runnable('python2', [str(source)]), 'OK') return (None, 'Not supported source file.') def run(self): (subtasks, cmds) = self.parse_file() for stn in range(1, subtasks + 1): dire = FilePath(self._dataset_directory, 'st%d' % stn).get_or_create_dir() dire.clear() if not cmds: ui.show_message("Warning", 'no commands were executed for the plan.', ui.WARNING) for (stn, subtask) in sorted(cmds.items()): self.run_subtask(stn, subtask) @ui.workgroup('Subtask {1}') def run_subtask(self, stn, subtask): groups = subtask['groups'] for (group, tests) in sorted(groups.items()): for (i, test) in enumerate(tests, 1): cmd = test['cmd'] test_file = self.test_filepath(stn, group, i) if cmd == 'copy': self.copy(test['file'], test_file) elif cmd == 'echo': self.echo(test['args'], test_file) else: test['args'].insert(0, '%s-%s-%s' % (stn, group, i)) source = FilePath(self._directory, test['source']) if cmd == 'cpp': self.run_cpp_generator(source, test['args'], test_file) elif cmd in ['py', 'py2', 'py3']: self.run_py_generator(source, test['args'], test_file, cmd) elif cmd == 'java': self.run_java_generator(source, test['args'], test_file) elif cmd == 'run': bin_path = FilePath(self._directory, test['bin']) self.run_bin_generator(bin_path, test['args'], test_file) else: ui.fatal_error('unexpected command when running plan: %s ' % cmd) @ui.work('Copy', '{1}') def copy(self, src, dst): fp = FilePath(self._task_directory, src) if not fp.exists(): return (False, 'No such file') try: fp.copy(dst) (st, msg) = (True, 'OK') return (st, msg) except Exception: # pylint: disable=broad-except return (False, 'Error when copying file') @ui.work('Echo', '{1}') def echo(self, args, dst): with dst.open('w') as test_file: test_file.write(' '.join(args) + '\n') (st, msg) = (True, 'Ok') return (st, msg) @ui.work('Gen', '{1}') def run_cpp_generator(self, source, args, dst): if not source.exists(): return (False, 'No such file') binary = source.chext('.bin') if binary.mtime() < source.mtime(): st = self._cpp_compiler(source, binary) if not st: return (st, 'Failed to build generator') (st, _time, msg) = Runnable(binary).run(None, dst, args) return (st, msg) @ui.work('Gen', '{1}') def run_py_generator(self, source, args, dst, cmd): if not source.exists(): return (False, 'No such file') python = 'python2' if cmd == 'py2' else 'python3' (st, _time, msg) = Runnable(python, [str(source)]).run(None, dst, args) return (st, msg) @ui.work('Gen', '{1}') def run_java_generator(self, source, args, dst): if not source.exists(): return (False, 'No such file') bytecode = source.chext('.class') if bytecode.mtime() < source.mtime(): st = self._java_compiler(source) if not st: return (st, 'Failed to build generator') classname = bytecode.rootname() classpath = str(bytecode.directory().path()) (st, _time, msg) = Runnable('java', ['-cp', classpath, classname]).run(None, dst, args) return (st, msg) @ui.work('Gen', '{1}') def run_bin_generator(self, bin_path, args, dst): if not bin_path.exists(): return (False, 'No such file') if not Runnable.is_callable(bin_path): return (False, 'Cannot run file, it may not have correct permissions') (st, _time, msg) = Runnable(bin_path).run(None, dst, args) return (st, msg) def parse_file(self): # pylint: disable=too-many-locals,too-many-branches """ Args: path (FilePath) """ cmds = {} st = 0 for (lineno, line) in enumerate(self._testplan_path.open('r').readlines(), 1): line = line.strip() subtask_header = re.compile(r'\s*\[\s*Subtask\s*(\d+)\s*(?:-\s*([^\]\s]+))?\s*\]\s*') cmd_line = re.compile(r'\s*([^;\s]+)\s*;\s*(\S+)(:?\s+(.*))?') comment = re.compile(r'\s*#.*') if not line: continue if not comment.fullmatch(line): header_match = subtask_header.fullmatch(line) cmd_match = cmd_line.fullmatch(line) if header_match: found_st = int(header_match.group(1)) validator = header_match.group(2) if st + 1 != found_st: ui.fatal_error('line %d: found subtask %d, but subtask %d was expected' % (lineno, found_st, st + 1)) st += 1 cmds[st] = {'validator': validator, 'groups': {}} elif cmd_match: if st == 0: ui.fatal_error( 'line %d: found command before declaring a subtask.' % lineno) group = cmd_match.group(1) cmd = cmd_match.group(2) args = (cmd_match.group(3) or '').split() if group not in cmds[st]['groups']: cmds[st]['groups'][group] = [] if cmd == 'copy': if len(args) > 2: ui.fatal_error( 'line %d: command copy expects exactly one argument.' % lineno) cmds[st]['groups'][group].append({ 'cmd': 'copy', 'file': args[0], }) elif cmd == 'echo': cmds[st]['groups'][group].append({'cmd': 'echo', 'args': args}) else: f = FilePath(self._directory, cmd) if f.ext in ['.cpp', '.java', '.py', '.py2', '.py3']: cmds[st]['groups'][group].append({ 'cmd': f.ext[1:], 'source': cmd, 'args': args }) else: cmds[st]['groups'][group].append({ 'cmd': 'run', 'bin': cmd, 'args': args }) else: ui.fatal_error('line %d: error while parsing line `%s`\n' % (lineno, line)) return (st, cmds)
class DatasetPlan: """Functionality to read and run a plan for generating dataset.""" def __init__(self, directory, task_directory, dataset_directory, filename='testplan.txt'): self._directory = directory self._testplan_path = FilePath(directory, filename) if not self._testplan_path.exists(): ui.fatal_error('No such file plan for creating dataset: "%s"' % self._testplan_path) self._task_directory = task_directory self._dataset_directory = dataset_directory self._cpp_compiler = CppCompiler() self._java_compiler = JavaCompiler() def test_filepath(self, stn, group, i): st_dir = FilePath(self._dataset_directory, 'st%d' % stn).get_or_create_dir() return FilePath(st_dir, '%s-%d.in' % (group, i)) def validate_input(self): (_, cmds) = self.parse_file() for (st, subtask) in sorted(cmds.items()): self.validate_subtask(st, subtask) @ui.workgroup('Subtask {1}') def validate_subtask(self, stn, subtask): validator = None if subtask['validator']: (validator, msg) = self.build_validator(subtask['validator']) if validator is None: ui.show_message('Warning', 'Failed to build validator: %s' % msg, ui.WARNING) else: ui.show_message('Info', 'No validator specified', ui.INFO) if validator: for (group, tests) in sorted(subtask['groups'].items()): for (i, _) in enumerate(tests, 1): test_file = self.test_filepath(stn, group, i) self.validate_test_input(test_file, validator) @ui.work('Validating', '{1}') def validate_test_input(self, test_file, validator): if not test_file.exists(): return False, 'Test file does not exist' (st, _time, msg) = validator.run(test_file, None) return st, msg def build_validator(self, source): fp = FilePath(self._directory, source) if not fp.exists(): return (None, 'File does not exists.') if fp.ext == '.cpp': binary = fp.chext('.bin') if binary.mtime() < fp.mtime() and not self._cpp_compiler( fp, binary): return (None, 'Failed to build validator.') return (Runnable(binary), 'OK') if fp.ext in ['.py', '.py3']: return (Runnable('python3', [str(source)]), 'OK') if fp.ext == '.py2': return (Runnable('python2', [str(source)]), 'OK') return (None, 'Not supported source file.') def run(self): (subtasks, cmds) = self.parse_file() for stn in range(1, subtasks + 1): dire = FilePath(self._dataset_directory, 'st%d' % stn).get_or_create_dir() dire.clear() if not cmds: ui.show_message("Warning", 'no commands were executed for the plan.', ui.WARNING) for (stn, subtask) in sorted(cmds.items()): self.run_subtask(stn, subtask) @ui.workgroup('Subtask {1}') def run_subtask(self, stn, subtask): groups = subtask['groups'] for (group, tests) in sorted(groups.items()): for (i, test) in enumerate(tests, 1): cmd = test['cmd'] test_file = self.test_filepath(stn, group, i) if cmd == 'copy': self.copy(test['file'], test_file) elif cmd == 'echo': self.echo(test['args'], test_file) else: test['args'].insert(0, '%s-%s-%s' % (stn, group, i)) source = FilePath(self._directory, test['source']) if cmd == 'cpp': self.run_cpp_generator(source, test['args'], test_file) elif cmd in ['py', 'py2', 'py3']: self.run_py_generator(source, test['args'], test_file, cmd) elif cmd == 'java': self.run_java_generator(source, test['args'], test_file) elif cmd == 'run': bin_path = FilePath(self._directory, test['bin']) self.run_bin_generator(bin_path, test['args'], test_file) else: ui.fatal_error( 'unexpected command when running plan: %s ' % cmd) @ui.work('Copy', '{1}') def copy(self, src, dst): fp = FilePath(self._task_directory, src) if not fp.exists(): return (False, 'No such file') try: fp.copy(dst) (st, msg) = (True, 'OK') return (st, msg) except Exception: # pylint: disable=broad-except return (False, 'Error when copying file') @ui.work('Echo', '{1}') def echo(self, args, dst): with dst.open('w') as test_file: test_file.write(' '.join(args) + '\n') (st, msg) = (True, 'Ok') return (st, msg) @ui.work('Gen', '{1}') def run_cpp_generator(self, source, args, dst): if not source.exists(): return (False, 'No such file') binary = source.chext('.bin') if binary.mtime() < source.mtime(): st = self._cpp_compiler(source, binary) if not st: return (st, 'Failed to build generator') (st, _time, msg) = Runnable(binary).run(None, dst, args) return (st, msg) @ui.work('Gen', '{1}') def run_py_generator(self, source, args, dst, cmd): if not source.exists(): return (False, 'No such file') python = 'python2' if cmd == 'py2' else 'python3' (st, _time, msg) = Runnable(python, [str(source)]).run(None, dst, args) return (st, msg) @ui.work('Gen', '{1}') def run_java_generator(self, source, args, dst): if not source.exists(): return (False, 'No such file') bytecode = source.chext('.class') if bytecode.mtime() < source.mtime(): st = self._java_compiler(source) if not st: return (st, 'Failed to build generator') classname = bytecode.rootname() classpath = str(bytecode.directory().path()) (st, _time, msg) = Runnable('java', ['-cp', classpath, classname]).run(None, dst, args) return (st, msg) @ui.work('Gen', '{1}') def run_bin_generator(self, bin_path, args, dst): if not bin_path.exists(): return (False, 'No such file') if not Runnable.is_callable(bin_path): return (False, 'Cannot run file, it may not have correct permissions') (st, _time, msg) = Runnable(bin_path).run(None, dst, args) return (st, msg) def parse_file(self): # pylint: disable=too-many-locals,too-many-branches """ Args: path (FilePath) """ cmds = {} st = 0 for (lineno, line) in enumerate(self._testplan_path.open('r').readlines(), 1): line = line.strip() subtask_header = re.compile( r'\s*\[\s*Subtask\s*(\d+)\s*(?:-\s*([^\]\s]+))?\s*\]\s*') cmd_line = re.compile(r'\s*([^;\s]+)\s*;\s*(\S+)(:?\s+(.*))?') comment = re.compile(r'\s*#.*') if not line: continue if not comment.fullmatch(line): header_match = subtask_header.fullmatch(line) cmd_match = cmd_line.fullmatch(line) if header_match: found_st = int(header_match.group(1)) validator = header_match.group(2) if st + 1 != found_st: ui.fatal_error( 'line %d: found subtask %d, but subtask %d was expected' % (lineno, found_st, st + 1)) st += 1 cmds[st] = {'validator': validator, 'groups': {}} elif cmd_match: if st == 0: ui.fatal_error( 'line %d: found command before declaring a subtask.' % lineno) group = cmd_match.group(1) cmd = cmd_match.group(2) args = _parse_args(cmd_match.group(3) or '') if group not in cmds[st]['groups']: cmds[st]['groups'][group] = [] if cmd == 'copy': if len(args) > 2: ui.fatal_error( 'line %d: command copy expects exactly one argument.' % lineno) cmds[st]['groups'][group].append({ 'cmd': 'copy', 'file': args[0], }) elif cmd == 'echo': cmds[st]['groups'][group].append({ 'cmd': 'echo', 'args': args }) else: f = FilePath(self._directory, cmd) if f.ext in ['.cpp', '.java', '.py', '.py2', '.py3']: cmds[st]['groups'][group].append({ 'cmd': f.ext[1:], 'source': cmd, 'args': args }) else: cmds[st]['groups'][group].append({ 'cmd': 'run', 'bin': cmd, 'args': args }) else: ui.fatal_error('line %d: error while parsing line `%s`\n' % (lineno, line)) return (st, cmds)