diff --git a/.gitignore b/.gitignore index dd38acf0..f90ce721 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ build .idea __pycache__ /tests/packages/**/cache +/tests/packages/**/.cache src/sinol_make/data # pytest-cov diff --git a/examples/config.yml b/examples/config.yml index d0f73678..21baee08 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -59,6 +59,12 @@ extra_compilation_files: ['abclib.cpp', 'abclib.py'] ### Keys used by sinol-make: +# This key represents the short name (consisting of 3 letters) of the task. +# The names of files in `prog/`, `doc/`, `in/` and `out/` directories have to start with this task id. +# This key is only used by `sinol-make`: running `sinol-make export` creates +# an archive with the proper name, which sio2 uses as the task id. +sinol_task_id: abc + # sinol-make can behave differently depending on the value of `sinol_contest_type` key. # Mainly, it affects how points are calculated. # If the key is not specified, then `default` is used. diff --git a/setup.cfg b/setup.cfg index 378453f1..53a58d0f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ install_requires = PyYAML dictdiffer importlib-resources + psutil [options.packages.find] where = src diff --git a/src/sinol_make/__init__.py b/src/sinol_make/__init__.py index fe4b9a3d..933c60f0 100644 --- a/src/sinol_make/__init__.py +++ b/src/sinol_make/__init__.py @@ -8,7 +8,7 @@ from sinol_make import util, oiejq -__version__ = "1.5.3" +__version__ = "1.5.6" def configure_parsers(): parser = argparse.ArgumentParser( diff --git a/src/sinol_make/commands/export/__init__.py b/src/sinol_make/commands/export/__init__.py index f2973bc5..7976d20d 100644 --- a/src/sinol_make/commands/export/__init__.py +++ b/src/sinol_make/commands/export/__init__.py @@ -6,7 +6,7 @@ import yaml from sinol_make import util -from sinol_make.helpers import package_util, parsers +from sinol_make.helpers import package_util, parsers, paths from sinol_make.commands.gen import gen_util from sinol_make.interfaces.BaseCommand import BaseCommand @@ -34,7 +34,7 @@ def get_generated_tests(self): if not gen_util.ingen_exists(self.task_id): return [] - working_dir = os.path.join(os.getcwd(), 'cache', 'export', 'tests') + working_dir = paths.get_cache_path('export', 'tests') if os.path.exists(working_dir): shutil.rmtree(working_dir) os.makedirs(working_dir) @@ -100,8 +100,8 @@ def create_makefile_in(self, target_dir: str, config: dict): :param config: Config dictionary. """ with open(os.path.join(target_dir, 'makefile.in'), 'w') as f: - cxx_flags = '-std=c++17' - c_flags = '-std=c17' + cxx_flags = '-std=c++20' + c_flags = '-std=gnu99' def format_multiple_arguments(obj): if isinstance(obj, str): return obj @@ -146,11 +146,12 @@ def run(self, args: argparse.Namespace): with open(os.path.join(os.getcwd(), 'config.yml'), 'r') as config_file: config = yaml.load(config_file, Loader=yaml.FullLoader) - export_package_path = os.path.join(os.getcwd(), 'cache', 'export', self.task_id) + export_package_path = paths.get_cache_path('export', self.task_id) if os.path.exists(export_package_path): shutil.rmtree(export_package_path) os.makedirs(export_package_path) + util.change_stack_size_to_unlimited() self.copy_package_required_files(export_package_path) self.clear_files(export_package_path) self.create_makefile_in(export_package_path, config) diff --git a/src/sinol_make/commands/gen/__init__.py b/src/sinol_make/commands/gen/__init__.py index 7b8feae2..4e625d7e 100644 --- a/src/sinol_make/commands/gen/__init__.py +++ b/src/sinol_make/commands/gen/__init__.py @@ -99,6 +99,7 @@ def run(self, args: argparse.Namespace): self.correct_solution = gen_util.get_correct_solution(self.task_id) self.ingen_exe = gen_util.compile_ingen(self.ingen, self.args, self.args.weak_compilation_flags) + util.change_stack_size_to_unlimited() if gen_util.run_ingen(self.ingen_exe): print(util.info('Successfully generated input files.')) else: diff --git a/src/sinol_make/commands/gen/gen_util.py b/src/sinol_make/commands/gen/gen_util.py index d5062603..42f9d94f 100644 --- a/src/sinol_make/commands/gen/gen_util.py +++ b/src/sinol_make/commands/gen/gen_util.py @@ -42,10 +42,14 @@ def get_ingen(task_id=None, ingen_path=None): util.exit_with_error(f'Ingen source file for task {task_id} does not exist.') # Sio2 first chooses shell scripts, then non-shell source codes. - if os.path.splitext(ingen[0])[1] == '.sh' and len(ingen) > 1: - return ingen[1] - else: - return ingen[0] + correct_ingen = None + for i in ingen: + if os.path.splitext(i)[1] == '.sh': + correct_ingen = i + break + if correct_ingen is None: + correct_ingen = ingen[0] + return correct_ingen def compile_ingen(ingen_path: str, args: argparse.Namespace, weak_compilation_flags=False): diff --git a/src/sinol_make/commands/inwer/__init__.py b/src/sinol_make/commands/inwer/__init__.py index 1c8051e6..cf45bed6 100644 --- a/src/sinol_make/commands/inwer/__init__.py +++ b/src/sinol_make/commands/inwer/__init__.py @@ -9,7 +9,7 @@ from sinol_make import util from sinol_make.commands.inwer.structs import TestResult, InwerExecution, VerificationResult, TableData -from sinol_make.helpers import package_util, compile, printer +from sinol_make.helpers import package_util, compile, printer, paths from sinol_make.helpers.parsers import add_compilation_arguments from sinol_make.interfaces.BaseCommand import BaseCommand from sinol_make.commands.inwer import inwer_util @@ -52,7 +52,7 @@ def verify_test(execution: InwerExecution) -> VerificationResult: """ Verifies a test and returns the result of inwer on this test. """ - output_dir = os.path.join(os.getcwd(), 'cache', 'executions', execution.test_name) + output_dir = paths.get_executables_path(execution.test_name) os.makedirs(output_dir, exist_ok=True) command = [execution.inwer_exe_path] @@ -140,6 +140,7 @@ def run(self, args: argparse.Namespace): else: print('Verifying tests: ' + util.bold(', '.join(self.tests))) + util.change_stack_size_to_unlimited() self.compile_inwer(args) results: Dict[str, TestResult] = self.verify_and_print_table() print('') diff --git a/src/sinol_make/commands/inwer/inwer_util.py b/src/sinol_make/commands/inwer/inwer_util.py index 2e450099..b9ec0653 100644 --- a/src/sinol_make/commands/inwer/inwer_util.py +++ b/src/sinol_make/commands/inwer/inwer_util.py @@ -91,12 +91,15 @@ def print_line_separator(): else: output.append("") - print(output[0].ljust(column_lengths[3])) - output.pop(0) + if output == []: + print(util.color_gray("No output")) + else: + print(output[0].ljust(column_lengths[3])) + output.pop(0) - for line in output: - print(" " * (column_lengths[0] + 2) + " | " + " " * (column_lengths[1] - 1) + " | " + - " " * (column_lengths[2] - 1) + " | " + line.ljust(column_lengths[3])) + for line in output: + print(" " * (column_lengths[0] + 2) + " | " + " " * (column_lengths[1] - 1) + " | " + + " " * (column_lengths[2] - 1) + " | " + line.ljust(column_lengths[3])) print_line_separator() print() diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index 35305d45..8f223e2f 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -4,8 +4,10 @@ import subprocess import signal import threading -from io import StringIO +import time +import psutil import glob +from io import StringIO from typing import Dict from sinol_make import contest_types, oiejq @@ -14,7 +16,7 @@ from sinol_make.helpers.parsers import add_compilation_arguments from sinol_make.interfaces.BaseCommand import BaseCommand from sinol_make.interfaces.Errors import CompilationError, CheckerOutputException, UnknownContestType -from sinol_make.helpers import compile, compiler, package_util, printer +from sinol_make.helpers import compile, compiler, package_util, printer, paths from sinol_make.structs.status_structs import Status import sinol_make.util as util import yaml, os, collections, sys, re, math, dictdiffer @@ -339,8 +341,8 @@ def get_groups(self, tests): def compile_solutions(self, solutions): - os.makedirs(self.COMPILATION_DIR, exist_ok=True) - os.makedirs(self.EXECUTABLES_DIR, exist_ok=True) + os.makedirs(paths.get_compilation_log_path(), exist_ok=True) + os.makedirs(paths.get_executables_path(), exist_ok=True) print("Compiling %d solutions..." % len(solutions)) args = [(solution, True) for solution in solutions] with mp.Pool(self.cpus) as pool: @@ -349,10 +351,9 @@ def compile_solutions(self, solutions): def compile(self, solution, use_extras = False): - compile_log_file = os.path.join( - self.COMPILATION_DIR, "%s.compile_log" % package_util.get_file_name(solution)) + compile_log_file = paths.get_compilation_log_path("%s.compile_log" % package_util.get_file_name(solution)) source_file = os.path.join(os.getcwd(), "prog", self.get_solution_from_exe(solution)) - output = os.path.join(self.EXECUTABLES_DIR, package_util.get_executable(solution)) + output = paths.get_executables_path(package_util.get_executable(solution)) extra_compilation_args = [] extra_compilation_files = [] @@ -504,7 +505,10 @@ def sigint_handler(signum, frame): def execute_time(self, command, name, result_file_path, input_file_path, output_file_path, answer_file_path, time_limit, memory_limit, hard_time_limit): + + executable = package_util.get_executable(name) timeout = False + mem_limit_exceeded = False with open(input_file_path, "r") as input_file: process = subprocess.Popen(command, stdin=input_file, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, preexec_fn=os.setsid) @@ -517,11 +521,27 @@ def sigint_handler(signum, frame): sys.exit(1) signal.signal(signal.SIGINT, sigint_handler) - try: - output, _ = process.communicate(timeout=hard_time_limit) - except subprocess.TimeoutExpired: - timeout = True - os.killpg(os.getpgid(process.pid), signal.SIGTERM) + start_time = time.time() + while process.poll() is None: + try: + time_process = psutil.Process(process.pid) + executable_process = None + for child in time_process.children(): + if child.name() == executable: + executable_process = child + break + if executable_process is not None and executable_process.memory_info().rss > memory_limit * 1024: + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + mem_limit_exceeded = True + break + except psutil.NoSuchProcess: + pass + + if time.time() - start_time > hard_time_limit: + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + timeout = True + break + output, _ = process.communicate() result = ExecutionResult() program_exit_code = None @@ -547,7 +567,7 @@ def sigint_handler(signum, frame): Command terminated by signal 11 """ program_exit_code = int(lines[0].strip().split(" ")[-1]) - else: + elif not mem_limit_exceeded: result.Status = Status.RE result.Error = "Unexpected output from time command: " + "\n".join(lines) return result @@ -556,6 +576,9 @@ def sigint_handler(signum, frame): result.Status = Status.RE elif timeout: result.Status = Status.TL + elif mem_limit_exceeded: + result.Memory = memory_limit + 1 # Add one so that the memory is red in the table + result.Status = Status.ML elif result.Time > time_limit: result.Status = Status.TL elif result.Memory > memory_limit: @@ -581,7 +604,7 @@ def run_solution(self, data_for_execution: ExecutionData): """ (name, executable, test, time_limit, memory_limit, timetool_path) = data_for_execution - file_no_ext = os.path.join(self.EXECUTIONS_DIR, name, package_util.extract_test_id(test)) + file_no_ext = paths.get_executions_path(name, package_util.extract_test_id(test)) output_file = file_no_ext + ".out" result_file = file_no_ext + ".res" hard_time_limit_in_s = math.ceil(2 * time_limit / 1000.0) @@ -620,7 +643,7 @@ def run_solutions(self, compiled_commands, names, solutions): executions.append((name, executable, test, package_util.get_time_limit(test, self.config, lang, self.args), package_util.get_memory_limit(test, self.config, lang, self.args), self.timetool_path)) all_results[name][self.get_group(test)][test] = ExecutionResult(Status.PENDING) - os.makedirs(os.path.join(self.EXECUTIONS_DIR, name), exist_ok=True) + os.makedirs(paths.get_executions_path(name), exist_ok=True) else: for test in self.tests: all_results[name][self.get_group(test)][test] = ExecutionResult(Status.CE) @@ -685,9 +708,8 @@ def compile_and_run(self, solutions): for i in range(len(solutions)): if not compilation_results[i]: self.failed_compilations.append(solutions[i]) - os.makedirs(self.EXECUTIONS_DIR, exist_ok=True) - executables = [os.path.join(self.EXECUTABLES_DIR, package_util.get_executable(solution)) - for solution in solutions] + os.makedirs(paths.get_executions_path(), exist_ok=True) + executables = [paths.get_executables_path(package_util.get_executable(solution)) for solution in solutions] compiled_commands = zip(solutions, executables, compilation_results) names = solutions return self.run_solutions(compiled_commands, names, solutions) @@ -966,12 +988,7 @@ def set_group_result(solution, group, result): def set_constants(self): self.ID = package_util.get_task_id() - self.TMP_DIR = os.path.join(os.getcwd(), "cache") - self.COMPILATION_DIR = os.path.join(self.TMP_DIR, "compilation") - self.EXECUTIONS_DIR = os.path.join(self.TMP_DIR, "executions") - self.EXECUTABLES_DIR = os.path.join(self.TMP_DIR, "executables") self.SOURCE_EXTENSIONS = ['.c', '.cpp', '.py', '.java'] - self.PROGRAMS_IN_ROW = 8 self.SOLUTIONS_RE = re.compile(r"^%s[bs]?[0-9]*\.(cpp|cc|java|py|pas)$" % self.ID) @@ -1138,7 +1155,7 @@ def run(self, args): print(util.info("Checker found: %s" % os.path.basename(checker[0]))) self.checker = checker[0] checker_basename = os.path.basename(self.checker) - self.checker_executable = os.path.join(self.EXECUTABLES_DIR, checker_basename + ".e") + self.checker_executable = paths.get_executables_path(checker_basename + ".e") checker_compilation = self.compile_solutions([self.checker]) if not checker_compilation[0]: @@ -1154,6 +1171,7 @@ def run(self, args): self.failed_compilations = [] solutions = self.get_solutions(self.args.solutions) + util.change_stack_size_to_unlimited() for solution in solutions: lang = package_util.get_file_lang(solution) for test in self.tests: diff --git a/src/sinol_make/helpers/compile.py b/src/sinol_make/helpers/compile.py index 0c0bd3cc..b8defdeb 100644 --- a/src/sinol_make/helpers/compile.py +++ b/src/sinol_make/helpers/compile.py @@ -8,19 +8,13 @@ import sinol_make.helpers.compiler as compiler from sinol_make import util +from sinol_make.helpers import paths from sinol_make.interfaces.Errors import CompilationError from sinol_make.structs.compiler_structs import Compilers -def get_executable_info_file(file_path): - """ - Calculate the md5 sum of file's content and return the path to file `cache/md5sums/`. - If this file exists it contains the path to the compiled executable. - Thanks to that, we cache the compiled solutions and recompile them when they change. - """ - os.makedirs(os.path.join(os.getcwd(), 'cache', 'md5sums'), exist_ok=True) - md5sum = util.get_file_md5(file_path) - return os.path.join(os.getcwd(), 'cache', 'md5sums', md5sum) +def create_compilation_cache(): + os.makedirs(paths.get_cache_path("md5sums"), exist_ok=True) def check_compiled(file_path: str): @@ -29,31 +23,36 @@ def check_compiled(file_path: str): :param file_path: Path to the file :return: executable path if compiled, None otherwise """ - info_file_path = get_executable_info_file(file_path) - + create_compilation_cache() + md5sum = util.get_file_md5(file_path) try: - with open(info_file_path, 'r') as md5sums_file: - exe_file = md5sums_file.read().strip() - if os.path.exists(exe_file): - return exe_file - else: - os.unlink(info_file_path) - return None + info_file_path = paths.get_cache_path("md5sums", os.path.basename(file_path)) + with open(info_file_path, 'r') as info_file: + info = yaml.load(info_file, Loader=yaml.FullLoader) + if info.get("md5sum", "") == md5sum: + exe_path = info.get("executable_path", "") + if os.path.exists(exe_path): + return exe_path + return None except FileNotFoundError: return None def save_compiled(file_path: str, exe_path: str): """ - Save the compiled executable path to cache in `cache/md5sums/`, - where is the md5 sum of the file's content. + Save the compiled executable path to cache in `.cache/md5sums/`, + which contains the md5sum of the file and the path to the executable. :param file_path: Path to the file :param exe_path: Path to the compiled executable """ - info_file_path = get_executable_info_file(file_path) - - with open(info_file_path, 'w') as md5sums_file: - md5sums_file.write(exe_path) + create_compilation_cache() + info_file_path = paths.get_cache_path("md5sums", os.path.basename(file_path)) + info = { + "md5sum": util.get_file_md5(file_path), + "executable_path": exe_path + } + with open(info_file_path, 'w') as info_file: + yaml.dump(info, info_file) def compile(program, output, compilers: Compilers = None, compile_log = None, weak_compilation_flags = False, @@ -101,11 +100,11 @@ def compile(program, output, compilers: Compilers = None, compile_log = None, we if ext == '.cpp': arguments = [compilers.cpp_compiler_path or compiler.get_cpp_compiler_path(), program] + \ extra_compilation_args + ['-o', output] + \ - f'--std=c++17 -O3 -lm {gcc_compilation_flags} -fdiagnostics-color'.split(' ') + f'--std=c++20 -O3 -lm {gcc_compilation_flags} -fdiagnostics-color'.split(' ') elif ext == '.c': arguments = [compilers.c_compiler_path or compiler.get_c_compiler_path(), program] + \ extra_compilation_args + ['-o', output] + \ - f'--std=c17 -O3 -lm {gcc_compilation_flags} -fdiagnostics-color'.split(' ') + f'--std=gnu99 -O3 -lm {gcc_compilation_flags} -fdiagnostics-color'.split(' ') elif ext == '.py': if sys.platform == 'win32' or sys.platform == 'cygwin': # TODO: Make this work on Windows @@ -149,11 +148,8 @@ def compile_file(file_path: str, name: str, compilers: Compilers, weak_compilati :param weak_compilation_flags: Use weaker compilation flags :return: Tuple of (executable path or None if compilation failed, log path) """ - - executable_dir = os.path.join(os.getcwd(), 'cache', 'executables') - compile_log_dir = os.path.join(os.getcwd(), 'cache', 'compilation') - os.makedirs(executable_dir, exist_ok=True) - os.makedirs(compile_log_dir, exist_ok=True) + os.makedirs(paths.get_executables_path(), exist_ok=True) + os.makedirs(paths.get_compilation_log_path(), exist_ok=True) with open(os.path.join(os.getcwd(), "config.yml"), "r") as config_file: config = yaml.load(config_file, Loader=yaml.FullLoader) @@ -166,8 +162,8 @@ def compile_file(file_path: str, name: str, compilers: Compilers, weak_compilati args = [args] extra_compilation_args = [os.path.join(os.getcwd(), "prog", file) for file in args] - output = os.path.join(executable_dir, name) - compile_log_path = os.path.join(compile_log_dir, os.path.splitext(name)[0] + '.compile_log') + output = paths.get_executables_path(name) + compile_log_path = paths.get_compilation_log_path(os.path.splitext(name)[0] + '.compile_log') with open(compile_log_path, 'w') as compile_log: try: if compile(file_path, output, compilers, compile_log, weak_compilation_flags, extra_compilation_args, diff --git a/src/sinol_make/helpers/compiler.py b/src/sinol_make/helpers/compiler.py index 4162e0c3..6b11bc36 100644 --- a/src/sinol_make/helpers/compiler.py +++ b/src/sinol_make/helpers/compiler.py @@ -52,6 +52,8 @@ def get_cpp_compiler_path(): else: return 'g++' elif sys.platform == 'darwin': + if check_if_installed('g++-12'): # g++12 is currently the default compiler on sio. + return 'g++-12' for i in [9, 10, 11]: compiler = 'g++-' + str(i) if check_if_installed(compiler): @@ -65,10 +67,12 @@ def get_python_interpreter_path(): Get the Python interpreter """ - if not check_if_installed('python3'): - return None - else: - return 'python3' + if check_if_installed('python3.11'): # python3.11 is currently the default interpreter on sio. + return 'python3.11' + for ver in ['3.9', '3.8', '3.7', '3']: + if check_if_installed('python' + ver): + return 'python' + ver + return None def get_java_compiler_path(): diff --git a/src/sinol_make/helpers/package_util.py b/src/sinol_make/helpers/package_util.py index 111c642e..150bc868 100644 --- a/src/sinol_make/helpers/package_util.py +++ b/src/sinol_make/helpers/package_util.py @@ -1,12 +1,25 @@ import os +import yaml +import glob from enum import Enum from typing import List, Union, Dict, Any from sinol_make import util +from sinol_make.helpers import paths def get_task_id() -> str: - return os.path.split(os.getcwd())[-1] + with open(os.path.join(os.getcwd(), "config.yml")) as config_file: + config = yaml.load(config_file, Loader=yaml.FullLoader) + if "sinol_task_id" in config: + return config["sinol_task_id"] + else: + print(util.warning("sinol_task_id not specified in config.yml. Using task id from directory name.")) + task_id = os.path.split(os.getcwd())[-1] + if len(task_id) == 3: + return task_id + else: + util.exit_with_error("Invalid task id. Task id should be 3 characters long.") def extract_test_id(test_path): @@ -59,7 +72,7 @@ def get_executable_path(solution: str) -> str: """ Returns path to compiled executable for given solution. """ - return os.path.join(os.getcwd(), 'cache', 'executables', get_executable(solution)) + return paths.get_executables_path(get_executable(solution)) def get_file_lang(file_path): diff --git a/src/sinol_make/helpers/paths.py b/src/sinol_make/helpers/paths.py new file mode 100644 index 00000000..771ca9e3 --- /dev/null +++ b/src/sinol_make/helpers/paths.py @@ -0,0 +1,33 @@ +import os + + +def get_cache_path(*paths): + """ + Function to get a path in the cache directory. Works the same as os.path.join. + With no arguments, it returns the path to the cache directory. + """ + return os.path.join(os.getcwd(), ".cache", *paths) + + +def get_executables_path(*paths): + """ + Function to get a path in executables directory. Works the same as os.path.join. + With no arguments, it returns the path to the executables directory. + """ + return os.path.join(get_cache_path("executables"), *paths) + + +def get_compilation_log_path(*paths): + """ + Function to get a path in compilation log directory. Works the same as os.path.join. + With no arguments, it returns the path to the compilation log directory. + """ + return os.path.join(get_cache_path("compilation"), *paths) + + +def get_executions_path(*paths): + """ + Function to get a path in executions directory. Works the same as os.path.join. + With no arguments, it returns the path to the executions directory. + """ + return os.path.join(get_cache_path("executions"), *paths) diff --git a/src/sinol_make/helpers/printer.py b/src/sinol_make/helpers/printer.py index 999f10a9..0fb20645 100644 --- a/src/sinol_make/helpers/printer.py +++ b/src/sinol_make/helpers/printer.py @@ -168,6 +168,8 @@ def _print_to_scr(scr, output, has_title): color = curses.color_pair(2) elif output[i + 1:i + 5] == '[93m': # Escape sequence for yellow. color = curses.color_pair(3) + elif output[i + 1:i + 5] == '[90m': # Escape sequence for gray. + color = curses.A_DIM else: color = curses.A_NORMAL i += 4 # Skip the escape sequence. diff --git a/src/sinol_make/oiejq/__init__.py b/src/sinol_make/oiejq/__init__.py index d71618fd..fccaf552 100644 --- a/src/sinol_make/oiejq/__init__.py +++ b/src/sinol_make/oiejq/__init__.py @@ -63,9 +63,9 @@ def install_oiejq(): if request.status_code != 200: raise Exception('Couldn\'t download oiejq (https://oij.edu.pl/zawodnik/srodowisko/oiejq.tar.gz returned status code: ' + str(request.status_code) + ')') - # oiejq is downloaded to a temporary directory and not to the `cache` dir, + # oiejq is downloaded to a temporary directory and not to the `.cache` dir, # as there is no guarantee that the current directory is the package directory. - # The `cache` dir is only used for files that are part of the package and those + # The `.cache` dir is only used for files that are part of the package and those # that the package creator might want to look into. with tempfile.TemporaryDirectory() as tmpdir: oiejq_path = os.path.join(tmpdir, 'oiejq.tar.gz') diff --git a/src/sinol_make/util.py b/src/sinol_make/util.py index 4cc24f38..8bf300b6 100644 --- a/src/sinol_make/util.py +++ b/src/sinol_make/util.py @@ -1,10 +1,15 @@ import glob, importlib, os, sys, requests, yaml import platform import tempfile +import shutil import hashlib +import subprocess import threading +import resource from typing import Union +import sinol_make + def get_commands(): """ @@ -246,6 +251,18 @@ def stringify_keys(d): return d +def change_stack_size_to_unlimited(): + """ + Function to change the stack size to unlimited. + """ + try: + resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY)) + except (resource.error, ValueError): + # We can't run `ulimit -s unlimited` in the code, because since it failed, it probably requires root. + print(error(f'Failed to change stack size to unlimited. Please run `ulimit -s unlimited` ' + f'to make sure that solutions with large stack size will work.')) + + def is_wsl(): """ Function to check if the program is running on Windows Subsystem for Linux. @@ -268,6 +285,7 @@ def get_file_md5(path): def color_red(text): return "\033[91m{}\033[00m".format(text) def color_green(text): return "\033[92m{}\033[00m".format(text) def color_yellow(text): return "\033[93m{}\033[00m".format(text) +def color_gray(text): return "\033[90m{}\033[00m".format(text) def bold(text): return "\033[01m{}\033[00m".format(text) def info(text): diff --git a/tests/commands/export/util.py b/tests/commands/export/util.py index ac103636..9bf2f8bd 100644 --- a/tests/commands/export/util.py +++ b/tests/commands/export/util.py @@ -53,8 +53,8 @@ def _get_value_from_key(key, seperator): assert _get_value_from_key("SLOW_TIMELIMIT", "=") == str(4 * config["time_limit"]) assert _get_value_from_key("MEMLIMIT", "=") == str(config["memory_limit"]) - cxx_flags = '-std=c++17' - c_flags = '-std=c17' + cxx_flags = '-std=c++20' + c_flags = '-std=gnu99' def format_multiple_arguments(obj): if isinstance(obj, str): return obj diff --git a/tests/commands/gen/test_unit.py b/tests/commands/gen/test_unit.py index c3f991f5..e82eea59 100644 --- a/tests/commands/gen/test_unit.py +++ b/tests/commands/gen/test_unit.py @@ -50,7 +50,7 @@ def test_get_ingen(): os.rename("prog/gen_helper.cpp", "prog/geningen.cpp") ingen_path = gen_util.get_ingen("gen") - assert os.path.basename(ingen_path) == "geningen.cpp" + assert os.path.basename(ingen_path) == "geningen.sh" @pytest.mark.parametrize("create_package", [util.get_simple_package_path()], indirect=True) diff --git a/tests/commands/inwer/test_integration.py b/tests/commands/inwer/test_integration.py index c63ff22e..767e2d4f 100644 --- a/tests/commands/inwer/test_integration.py +++ b/tests/commands/inwer/test_integration.py @@ -117,3 +117,21 @@ def test_flag_tests(capsys, create_package): assert "wer1a.in" in out assert "wer2a.in" in out assert "wer3a.in" not in out + + +@pytest.mark.parametrize("create_package", [util.get_inwer_package_path()], indirect=True) +def test_no_output(capsys, create_package): + """ + Test `inwer` command when inwer doesn't print anything. + """ + package_path = create_package + util.create_ins(package_path) + parser = configure_parsers() + args = parser.parse_args(["inwer", "prog/werinwer4.cpp"]) + command = Command() + + with pytest.raises(SystemExit) as e: + command.run(args) + assert e.value.code == 0 + out = capsys.readouterr().out + assert "No output" in out diff --git a/tests/commands/run/test_integration.py b/tests/commands/run/test_integration.py index d648f51a..fde696a2 100644 --- a/tests/commands/run/test_integration.py +++ b/tests/commands/run/test_integration.py @@ -1,5 +1,6 @@ import copy import sys +import time import pytest import copy @@ -11,7 +12,7 @@ @pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(), get_checker_package_path(), get_library_package_path(), get_library_string_args_package_path(), get_limits_package_path(), - get_limits_package_path(), get_override_limits_package_path()], + get_override_limits_package_path()], indirect=True) def test_simple(create_package, time_tool): """ @@ -30,7 +31,7 @@ def test_simple(create_package, time_tool): @pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(), get_checker_package_path(), get_library_package_path(), get_library_string_args_package_path(), get_limits_package_path(), - get_limits_package_path(), get_override_limits_package_path()], + get_override_limits_package_path()], indirect=True) def test_no_expected_scores(capsys, create_package, time_tool): """ @@ -66,7 +67,7 @@ def test_no_expected_scores(capsys, create_package, time_tool): @pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(), get_checker_package_path(), get_library_package_path(), get_library_string_args_package_path(), get_limits_package_path(), - get_limits_package_path(), get_override_limits_package_path()], + get_override_limits_package_path()], indirect=True) def test_apply_suggestions(create_package, time_tool): """ @@ -423,3 +424,25 @@ def test_override_limits(create_package, time_tool): "points": 0 } } + + +@pytest.mark.parametrize("create_package", [get_stack_size_package_path()], indirect=True) +def test_mem_limit_kill(create_package, time_tool): + """ + Test if `sinol-make` kills solution if it runs with memory limit exceeded. + """ + package_path = create_package + command = get_command() + create_ins_outs(package_path) + + parser = configure_parsers() + args = parser.parse_args(["run", "--time-tool", time_tool]) + command = Command() + start_time = time.time() + with pytest.raises(SystemExit) as e: + command.run(args) + end_time = time.time() + + assert e.value.code == 1 + assert end_time - start_time < 5 # The solution runs for 20 seconds, but it immediately exceeds memory limit, + # so it should be killed. diff --git a/tests/commands/run/test_unit.py b/tests/commands/run/test_unit.py index db367458..d0f80407 100644 --- a/tests/commands/run/test_unit.py +++ b/tests/commands/run/test_unit.py @@ -55,8 +55,8 @@ def test_execution(create_package, time_tool): with open(os.path.join(package_path, "config.yml"), "r") as config_file: config = yaml.load(config_file, Loader=yaml.FullLoader) - os.makedirs(os.path.join(command.EXECUTIONS_DIR, solution), exist_ok=True) - result = command.run_solution((solution, os.path.join(command.EXECUTABLES_DIR, executable), test, config['time_limit'], config['memory_limit'], oiejq.get_oiejq_path())) + os.makedirs(paths.get_executions_path(solution), exist_ok=True) + result = command.run_solution((solution, paths.get_executables_path(executable), test, config['time_limit'], config['memory_limit'], oiejq.get_oiejq_path())) assert result.Status == Status.OK @@ -125,12 +125,12 @@ def test_print_expected_scores(capsys): expected_scores_dict = yaml.load(expected_scores, Loader=yaml.FullLoader) command.print_expected_scores(expected_scores_dict["sinol_expected_scores"]) out = capsys.readouterr().out - assert out == expected_scores.replace('"', '') + assert expected_scores.replace('"', '') in out def test_validate_expected_scores_success(): - command = get_command() os.chdir(get_simple_package_path()) + command = get_command() command.scores = command.config["scores"] command.tests = package_util.get_tests(None) @@ -195,8 +195,8 @@ def test_validate_expected_scores_success(): def test_validate_expected_scores_fail(capsys): - command = get_command() os.chdir(get_simple_package_path()) + command = get_command() command.scores = {1: 20, 2: 20, 3: 20, 4: 20} # Test with missing points for group in config. diff --git a/tests/conftest.py b/tests/conftest.py index 71575b4b..f5d6e0a6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,15 +5,14 @@ import pytest import multiprocessing as mp -from sinol_make.helpers import compile +from sinol_make.helpers import compile, paths def _compile(args): package, file_path = args os.chdir(package) - output = os.path.join(package, "cache", "executables", os.path.splitext(os.path.basename(file_path))[0] + ".e") - with open(os.path.join(package, "cache", "compilation", - os.path.basename(file_path) + ".compile_log"), "w") as compile_log: + output = paths.get_executables_path(os.path.splitext(os.path.basename(file_path))[0] + ".e") + with open(paths.get_compilation_log_path(os.path.basename(file_path) + ".compile_log"), "w") as compile_log: compile.compile(file_path, output, compile_log=compile_log) @@ -43,7 +42,7 @@ def pytest_configure(config): continue for d in ["compilation", "executables"]: - os.makedirs(os.path.join(package, "cache", d), exist_ok=True) + os.makedirs(os.path.join(package, ".cache", d), exist_ok=True) for program in glob.glob(os.path.join(package, "prog", "*")): if os.path.isfile(program) and os.path.splitext(program)[1] in [".c", ".cpp", ".py", ".java"]: diff --git a/tests/helpers/test_cache.py b/tests/helpers/test_cache.py new file mode 100644 index 00000000..4d7681ee --- /dev/null +++ b/tests/helpers/test_cache.py @@ -0,0 +1,31 @@ +import os +import tempfile + +from sinol_make.helpers import compile + + +def test_compilation_caching(): + with tempfile.TemporaryDirectory() as tmpdir: + os.chdir(tmpdir) + program = os.path.join(tmpdir, 'program.cpp') + open(program, 'w').write('int main() { return 0; }') + + assert compile.check_compiled(program) is None + + assert compile.compile(program, os.path.join(tmpdir, 'program'), compile_log=None) + exe_path = compile.check_compiled(program) + assert exe_path is not None + + assert compile.compile(program, os.path.join(tmpdir, 'program'), compile_log=None) + exe_path2 = compile.check_compiled(program) + assert exe_path2 == exe_path + + open(program, 'w').write('int main() { return 1; }') + assert compile.check_compiled(program) is None + assert compile.compile(program, os.path.join(tmpdir, 'program'), compile_log=None) + assert compile.check_compiled(program) is not None + + open(program, 'w').write('int main() { return 0; }') + assert compile.check_compiled(program) is None + assert compile.compile(program, os.path.join(tmpdir, 'program'), compile_log=None) + assert compile.check_compiled(program) is not None diff --git a/tests/helpers/test_package_util.py b/tests/helpers/test_package_util.py index 811ed722..d71efb6a 100644 --- a/tests/helpers/test_package_util.py +++ b/tests/helpers/test_package_util.py @@ -1,10 +1,19 @@ +import pytest + from ..commands.run.util import create_ins from ..fixtures import * +from tests import util from sinol_make.helpers import package_util +@pytest.mark.parametrize("create_package", [util.get_long_name_package_path()], indirect=True) def test_get_task_id(create_package): - assert package_util.get_task_id() == "abc" + package_path = create_package + assert package_util.get_task_id() == "lpn" + with open(os.path.join(package_path, "config.yml"), "w") as config_file: + config_file.write("title: Long package name\n") + with pytest.raises(SystemExit): + package_util.get_task_id() def test_extract_test_id(): @@ -103,7 +112,6 @@ def test_get_time_limit(): assert package_util.get_time_limit("in/abc2a.in", config, "py") == 500 - def test_get_memory_limit(): config = { "memory_limit": 256, diff --git a/tests/packages/gen/prog/geningen.sh b/tests/packages/gen/prog/geningen.sh index 5d72e987..14329079 100644 --- a/tests/packages/gen/prog/geningen.sh +++ b/tests/packages/gen/prog/geningen.sh @@ -12,7 +12,7 @@ function generate() { done } -cache=$(dirname "$0")/../cache +cache=$(dirname "$0")/../.cache mkdir -p "$cache" g++ gen_helper.cpp -o "$cache"/gen diff --git a/tests/packages/long_package_name/config.yml b/tests/packages/long_package_name/config.yml new file mode 100644 index 00000000..7cd1d355 --- /dev/null +++ b/tests/packages/long_package_name/config.yml @@ -0,0 +1,2 @@ +title: Package with long name for testing `package_util.get_task_id()` function +sinol_task_id: lpn diff --git a/tests/packages/stc/config.yml b/tests/packages/stc/config.yml new file mode 100644 index 00000000..f387bf41 --- /dev/null +++ b/tests/packages/stc/config.yml @@ -0,0 +1,9 @@ +title: Package for testing if changing stack size works + +memory_limit: 1000 +time_limit: 10000 + +sinol_expected_scores: + stc.cpp: + expected: {1: OK} + points: 100 diff --git a/tests/packages/stc/in/.gitkeep b/tests/packages/stc/in/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/stc/out/.gitkeep b/tests/packages/stc/out/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/stc/prog/stc.cpp b/tests/packages/stc/prog/stc.cpp new file mode 100644 index 00000000..32e97559 --- /dev/null +++ b/tests/packages/stc/prog/stc.cpp @@ -0,0 +1,19 @@ +#include +#include + +using namespace std; +using namespace std::chrono_literals; + + +int main() { + char array[30000000]; // 30 MB + for (int i = 0; i < 30000000; i++) { + array[i] = 'a'; + } + this_thread::sleep_for(5s); + int a, b; + cin >> a >> b; + array[a] = (char)b; + cout << a + array[a]; + return 0; +} diff --git a/tests/packages/stc/prog/stcingen.cpp b/tests/packages/stc/prog/stcingen.cpp new file mode 100644 index 00000000..5f364c79 --- /dev/null +++ b/tests/packages/stc/prog/stcingen.cpp @@ -0,0 +1,9 @@ +#include + +using namespace std; + +int main() { + ofstream f("stc1a.in"); + f << "1 2\n"; + f.close(); +} diff --git a/tests/packages/wer/prog/werinwer4.cpp b/tests/packages/wer/prog/werinwer4.cpp new file mode 100644 index 00000000..f30c8dae --- /dev/null +++ b/tests/packages/wer/prog/werinwer4.cpp @@ -0,0 +1,24 @@ +#include + +using namespace std; + +int main(int argc, char const *argv[]) { + if (argc != 1) { + cout << "ERROR: Invalid number of arguments" << endl; + cout << "inwer: " << argv[0] << endl; + return 1; + } + + int n, a; + cin >> n; + for (int i = 0; i < n; i++) { + cin >> a; + + if (a >= n) { + cout << "ERROR: " << a << " >= " << n << "\n"; + return 1; + } + } + + return 0; +} diff --git a/tests/test_util.py b/tests/test_util.py index fa9e508c..8e06ea06 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -5,10 +5,14 @@ import json import tempfile import requests +import resource import requests_mock import pytest -from sinol_make import util +from sinol_make import util, configure_parsers +from tests import util as test_util +from tests.fixtures import create_package +from tests.commands.run import util as run_util def test_file_diff(): diff --git a/tests/util.py b/tests/util.py index 975440c5..c65fa34e 100644 --- a/tests/util.py +++ b/tests/util.py @@ -2,8 +2,7 @@ import glob import subprocess -from sinol_make.helpers import compile - +from sinol_make.helpers import compile, paths def get_simple_package_path(): @@ -74,6 +73,14 @@ def get_handwritten_package_path(): return os.path.join(os.path.dirname(__file__), "packages", "hwr") +def get_stack_size_package_path(): + """ + Get path to package for testing of changing stack size (/test/packages/stc) + """ + return os.path.join(os.path.dirname(__file__), "packages", "stc") + + + def get_override_limits_package_path(): """ Get path to package with `override_limits` present in config (/test/packages/ovl) @@ -87,17 +94,24 @@ def get_doc_package_path(): """ return os.path.join(os.path.dirname(__file__), "packages", "doc") + +def get_long_name_package_path(): + """ + Get path to package with long name (/test/packages/long_package_name) + """ + return os.path.join(os.path.dirname(__file__), "packages", "long_package_name") + def create_ins(package_path): """ Create .in files for package. """ ingen = glob.glob(os.path.join(package_path, "prog", "*ingen.*"))[0] - ingen_executable = os.path.join(package_path, "cache", "executables", "ingen.e") - os.makedirs(os.path.join(package_path, "cache", "executables"), exist_ok=True) + ingen_executable = paths.get_executables_path("ingen.e") + os.makedirs(paths.get_executables_path(), exist_ok=True) assert compile.compile(ingen, ingen_executable) os.chdir(os.path.join(package_path, "in")) - os.system("../cache/executables/ingen.e") + os.system("../.cache/executables/ingen.e") os.chdir(package_path) @@ -106,13 +120,13 @@ def create_outs(package_path): Create .out files for package. """ solution = glob.glob(os.path.join(package_path, "prog", "???.*"))[0] - solution_executable = os.path.join(package_path, "cache", "executables", "solution.e") - os.makedirs(os.path.join(package_path, "cache", "executables"), exist_ok=True) + solution_executable = paths.get_executables_path("solution.e") + os.makedirs(paths.get_executables_path(), exist_ok=True) assert compile.compile(solution, solution_executable) os.chdir(os.path.join(package_path, "in")) for file in glob.glob("*.in"): with open(file, "r") as in_file, open(os.path.join("../out", file.replace(".in", ".out")), "w") as out_file: - subprocess.Popen([os.path.join(package_path, "cache", "executables", "solution.e")], + subprocess.Popen([os.path.join(package_path, ".cache", "executables", "solution.e")], stdin=in_file, stdout=out_file).wait() os.chdir(package_path)