From 7a0a94dd50af3173de85c036677d6c26f26c2333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Wed, 10 Apr 2024 10:27:59 +0200 Subject: [PATCH 01/13] Modify missing outputs (#234) Stricter no outs verification Fix script crashing on lack of any outputs --- src/sinol_make/commands/run/__init__.py | 7 ++++++ tests/commands/run/test_integration.py | 29 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index 79612732..c7c6190e 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -300,6 +300,8 @@ def configure_subparser(self, subparser): help='path to oiejq executable (default: `~/.local/bin/oiejq`)') parser.add_argument('-a', '--apply-suggestions', dest='apply_suggestions', action='store_true', help='apply suggestions from expected scores report') + parser.add_argument('--no-outputs', dest='allow_no_outputs', action='store_true', + help='allow running the script without full outputs') add_compilation_arguments(parser) def parse_time(self, time_str): @@ -1121,9 +1123,14 @@ def validate_existence_of_outputs(self): print(util.warning('Missing output files for tests: ' + ', '.join( [self.extract_file_name(test) for test in missing_tests]))) + if self.args.allow_no_outputs != True: + util.exit_with_error('There are tests without outputs. \n' + 'Run outgen to fix this issue or add the --no-outputs flag to ignore the issue.') print(util.warning('Running only on tests with output files.')) self.tests = valid_input_files self.groups = self.get_groups(self.tests) + if len(self.groups) < 1: + util.exit_with_error('No tests with valid outputs.') def check_are_any_tests_to_run(self): """ diff --git a/tests/commands/run/test_integration.py b/tests/commands/run/test_integration.py index da17eb97..6d47dab4 100644 --- a/tests/commands/run/test_integration.py +++ b/tests/commands/run/test_integration.py @@ -336,6 +336,35 @@ def test_missing_output_files(capsys, create_package): out = capsys.readouterr().out assert f'Missing output files for tests: {out1}, {out2}' in out + assert 'There are tests without outputs.' in out + assert 'Run outgen to fix this issue or add the --no-outputs flag to ignore the issue.' in out + assert 'An error occurred while running the command.' not in out + + +@pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path()], indirect=True) +def test_missing_output_files_allow_missing(capsys, create_package): + """ + Test with missing output files. + """ + package_path = create_package + command = get_command() + create_ins_outs(package_path) + + outs = glob.glob(os.path.join(package_path, "out", "*.out")) + for i in outs: + os.unlink(i) + + parser = configure_parsers() + args = parser.parse_args(["run", "--time-tool", "time", "--no-outputs"]) + command = Command() + with pytest.raises(SystemExit): + command.run(args) + + out = capsys.readouterr().out + assert 'No tests with valid outputs.' in out + assert 'An error occurred while running the command.' not in out + assert 'There are tests without outputs.' not in out + assert 'Run outgen to fix this issue or add the --no-outputs flag to ignore the issue.' not in out @pytest.mark.parametrize("create_package", [get_limits_package_path()], indirect=True) From 9614701ea94ac0ff219df8664c707e368b4ef165 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Thu, 18 Apr 2024 16:03:40 +0200 Subject: [PATCH 02/13] Ingen deleting handwritten tests (#236) * Fix ingen deleting wrong inputs * Add tests * Update example config * Bump version --- example_package/config.yml | 5 ++++ src/sinol_make/__init__.py | 2 +- src/sinol_make/commands/ingen/__init__.py | 29 +++++++++++++++++++---- src/sinol_make/util.py | 1 + tests/commands/gen/test_integration.py | 25 ++++++++++++++++++- 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/example_package/config.yml b/example_package/config.yml index 700f66ee..ec3161ae 100644 --- a/example_package/config.yml +++ b/example_package/config.yml @@ -83,6 +83,11 @@ sinol_task_id: abc # A solution passes when all groups pass. sinol_contest_type: oi +# You can specify which tests are static (handwritten). This allows sinol-make to differentiate between +# old and handwritten tests. If this key is not present old tests won't be removed. +# This key is optional and should be a list of tests. +sinol_static_tests: ["abc0.in", "abc0a.in"] + # sinol-make can check if the solutions run as expected when using `run` command. # Key `sinol_expected_scores` defines expected scores for each solution on each tests. # There should be no reason to change this key manually. diff --git a/src/sinol_make/__init__.py b/src/sinol_make/__init__.py index f7aa5f70..29f54ae2 100644 --- a/src/sinol_make/__init__.py +++ b/src/sinol_make/__init__.py @@ -6,7 +6,7 @@ from sinol_make import util, oiejq -__version__ = "1.5.29" +__version__ = "1.5.30" def configure_parsers(): diff --git a/src/sinol_make/commands/ingen/__init__.py b/src/sinol_make/commands/ingen/__init__.py index 9be7a6ef..87794a71 100644 --- a/src/sinol_make/commands/ingen/__init__.py +++ b/src/sinol_make/commands/ingen/__init__.py @@ -32,6 +32,29 @@ def configure_subparser(self, subparser): help='do not validate test contents') parsers.add_compilation_arguments(parser) + def delete_dangling_files(self, dates): + to_delete = set() + for test in glob.glob(os.path.join(os.getcwd(), "in", f"{self.task_id}*.in")): + basename = os.path.basename(test) + if basename in dates and dates[basename] == os.path.getmtime(test): + to_delete.add(os.path.basename(test)) + if to_delete: + config = package_util.get_config() + if 'sinol_static_tests' not in config: + print(util.warning('Old input files won\'t be deleted, ' + 'because static tests are not defined. ' + 'You can define them in config.yml with `sinol_static_tests` key.')) + else: + static_files = config['sinol_static_tests'] + if isinstance(static_files, str): + static_files = [static_files] + static_files = set([os.path.basename(test) for test in static_files]) + to_delete = to_delete - static_files + if to_delete: + print(util.info('Cleaning up old input files.')) + for test in to_delete: + os.remove(os.path.join(os.getcwd(), "in", test)) + def run(self, args: argparse.Namespace): args = util.init_package_command(args) @@ -60,11 +83,7 @@ def run(self, args: argparse.Namespace): else: util.exit_with_error('Failed to generate input files.') - print(util.info('Cleaning up old input files.')) - for test in glob.glob(os.path.join(os.getcwd(), "in", f"{self.task_id}*.in")): - basename = os.path.basename(test) - if basename in dates and dates[basename] == os.path.getmtime(test): - os.unlink(test) + self.delete_dangling_files(dates) with open(paths.get_cache_path("input_tests"), "w") as f: f.write("\n".join(glob.glob(os.path.join(os.getcwd(), "in", f"{self.task_id}*.in")))) diff --git a/src/sinol_make/util.py b/src/sinol_make/util.py index a8714b52..656efe9b 100644 --- a/src/sinol_make/util.py +++ b/src/sinol_make/util.py @@ -95,6 +95,7 @@ def save_config(config): "title_en", "sinol_task_id", "sinol_contest_type", + "sinol_static_tests", "sinol_undocumented_time_tool", "sinol_undocumented_test_limits", "memory_limit", diff --git a/tests/commands/gen/test_integration.py b/tests/commands/gen/test_integration.py index da8c5fa7..b5844e4b 100644 --- a/tests/commands/gen/test_integration.py +++ b/tests/commands/gen/test_integration.py @@ -296,18 +296,41 @@ def test_bad_tests(create_package, capsys): @pytest.mark.parametrize("create_package", [util.get_shell_ingen_pack_path()], indirect=True) -def test_dangling_input_files(create_package): +def test_dangling_inputs(create_package, capsys): """ Test if dangling input files are removed. """ simple_run(["prog/geningen5.cpp"], command="ingen") for f in ["gen1.in", "gen2.in"]: assert os.path.exists(os.path.join(create_package, "in", f)) + _ = capsys.readouterr().out simple_run(["prog/geningen6.cpp"], command="ingen") + out = capsys.readouterr().out + assert ("Old input files won't be deleted, because static tests are not defined. " + "You can define them in config.yml with `sinol_static_tests` key.") in out + + config = package_util.get_config() + config["sinol_static_tests"] = [] + sm_util.save_config(config) + simple_run(["prog/geningen6.cpp"], command="ingen") + out = capsys.readouterr().out + assert "Cleaning up old input files." in out assert not os.path.exists(os.path.join(create_package, "in", "gen1.in")) assert os.path.exists(os.path.join(create_package, "in", "gen2.in")) + config = package_util.get_config() + config["sinol_static_tests"] = "gen1.in" + sm_util.save_config(config) + simple_run(["prog/geningen5.cpp"], command="ingen") + for f in ["gen1.in", "gen2.in"]: + assert os.path.exists(os.path.join(create_package, "in", f)) + _ = capsys.readouterr().out + simple_run(["prog/geningen6.cpp"], command="ingen") + out = capsys.readouterr().out + for f in ["gen1.in", "gen2.in"]: + assert os.path.exists(os.path.join(create_package, "in", f)) + @pytest.mark.parametrize("create_package", [util.get_simple_package_path()], indirect=True) def test_outgen_cache_cleaning(create_package, capsys): From 6cafe417bfadc075bd075d5cfc7330e587228a83 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sun, 21 Apr 2024 10:26:09 +0200 Subject: [PATCH 03/13] Autoupdate oiejq (#222) * Autoupdate oiejq * Add tests * Fix tests * Another fix * Update sio2jail version * Remove unnecessary try-catch * Check if correct oiejq is installed * Change oiejq install message * Fix message * Add test for oiejq reinstall --- src/sinol_make/__init__.py | 2 +- src/sinol_make/oiejq/__init__.py | 28 ++++++++++++++------ tests/test_oiejq.py | 45 +++++++++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/sinol_make/__init__.py b/src/sinol_make/__init__.py index 29f54ae2..18b84cad 100644 --- a/src/sinol_make/__init__.py +++ b/src/sinol_make/__init__.py @@ -33,7 +33,7 @@ def configure_parsers(): def check_oiejq(): if util.is_linux() and not oiejq.check_oiejq(): - print(util.warning('`oiejq` in `~/.local/bin/` not found, installing now...')) + print(util.warning('Up to date `oiejq` in `~/.local/bin/` not found, installing new version...')) try: if oiejq.install_oiejq(): print(util.info('`oiejq` was successfully installed.')) diff --git a/src/sinol_make/oiejq/__init__.py b/src/sinol_make/oiejq/__init__.py index 981e6c7b..92f1f7f5 100644 --- a/src/sinol_make/oiejq/__init__.py +++ b/src/sinol_make/oiejq/__init__.py @@ -13,12 +13,21 @@ def _check_if_oiejq_executable(path): if not os.access(path, os.X_OK): return False - try: - p = subprocess.Popen([path], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - p.wait() - return p.returncode == 0 - except FileNotFoundError: - return False + oiejq = subprocess.Popen([path], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + oiejq.wait() + return oiejq.returncode == 0 + + +def _check_sio2jail(path): + sio2jail = subprocess.Popen(path + " --version", shell=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, _ = sio2jail.communicate() + return out == (b"SIO2jail v1.5.0 compiled on Apr 15 2024 12:34:31 Linux 6.1.0-20-amd64 with gcc 10.2.1 20210110\n" + b"libseccomp 2.5.4\n") + + +def _check_oiejq(path): + return util.get_file_md5(path) == '7225efe59cb3052fa533b9fbc9ebf099' def check_oiejq(path = None): @@ -29,9 +38,12 @@ def check_oiejq(path = None): return False if path is not None: - return _check_if_oiejq_executable(path) + return _check_if_oiejq_executable(path) and _check_sio2jail(os.path.join(os.path.dirname(path), 'sio2jail')) \ + and _check_oiejq(path) - if _check_if_oiejq_executable(os.path.expanduser('~/.local/bin/oiejq')): + if _check_if_oiejq_executable(os.path.expanduser('~/.local/bin/oiejq')) and \ + _check_sio2jail(os.path.expanduser('~/.local/bin/sio2jail')) and \ + _check_oiejq(os.path.expanduser('~/.local/bin/oiejq')): return True else: return False diff --git a/tests/test_oiejq.py b/tests/test_oiejq.py index 785d3ffd..16f69bab 100644 --- a/tests/test_oiejq.py +++ b/tests/test_oiejq.py @@ -1,6 +1,7 @@ import os import shutil import sys +from urllib.request import urlretrieve import pytest from sinol_make import oiejq, util @@ -15,7 +16,7 @@ def test_install_oiejq(): os.remove(os.path.expanduser('~/.local/bin/oiejq')) os.remove(os.path.expanduser('~/.local/bin/sio2jail')) except IsADirectoryError: - shutil.rmtree(os.path.expanduser('~/.local/bin/oiejq'), ignore_errors=True) + shutil.rmtree(os.path.expanduser('~/.local/bin/oiejq'), ignore_errors=True) except FileNotFoundError: pass assert not oiejq.check_oiejq() @@ -27,12 +28,20 @@ def test_install_oiejq(): os.remove(os.path.expanduser('~/.local/bin/sio2jail')) except FileNotFoundError: pass - + assert not oiejq.check_oiejq() os.makedirs(os.path.expanduser('~/.local/bin/oiejq')) with pytest.raises(SystemExit): oiejq.install_oiejq() + # Test if oiejq is reinstalled when oiejq.sh is changed + os.rmdir(os.path.expanduser('~/.local/bin/oiejq')) + oiejq.install_oiejq() + with open(os.path.expanduser('~/.local/bin/oiejq'), 'a') as f: + f.write('\n') + assert not oiejq.check_oiejq() + oiejq.install_oiejq() + @pytest.mark.github_runner def test_check_oiejq(): @@ -43,7 +52,7 @@ def test_check_oiejq(): os.remove(os.path.expanduser('~/.local/bin/oiejq')) os.remove(os.path.expanduser('~/.local/bin/sio2jail')) except IsADirectoryError: - shutil.rmtree(os.path.expanduser('~/.local/bin/oiejq'), ignore_errors=True) + shutil.rmtree(os.path.expanduser('~/.local/bin/oiejq'), ignore_errors=True) except FileNotFoundError: pass @@ -58,7 +67,7 @@ def test_check_oiejq(): assert not oiejq.check_oiejq() with open(os.path.expanduser('~/.local/bin/oiejq'), 'w') as f: f.write('#!/bin/bash\necho "test"') - assert oiejq.check_oiejq() + assert oiejq._check_if_oiejq_executable(os.path.expanduser('~/.local/bin/oiejq')) @pytest.mark.github_runner @@ -69,6 +78,7 @@ def test_perf_counters_not_set(): if sys.platform != 'linux': return + oiejq.install_oiejq() with pytest.raises(SystemExit): oiejq.check_perf_counters_enabled() @@ -81,3 +91,30 @@ def test_perf_counters_set(): if not util.is_linux(): return oiejq.check_perf_counters_enabled() + + +@pytest.mark.github_runner +def test_updating(): + """ + Test updating oiejq + """ + if sys.platform != 'linux': + return + try: + os.remove(os.path.expanduser('~/.local/bin/oiejq')) + os.remove(os.path.expanduser('~/.local/bin/sio2jail')) + except IsADirectoryError: + shutil.rmtree(os.path.expanduser('~/.local/bin/oiejq'), ignore_errors=True) + except FileNotFoundError: + pass + assert not oiejq.check_oiejq() + assert oiejq.install_oiejq() + assert oiejq.get_oiejq_path() == os.path.expanduser('~/.local/bin/oiejq') + + # Download older sio2jail + urlretrieve('https://github.com/sio2project/sio2jail/releases/download/v1.4.3/sio2jail', + os.path.expanduser('~/.local/bin/sio2jail')) + os.chmod(os.path.expanduser('~/.local/bin/sio2jail'), 0o777) + assert not oiejq.check_oiejq() + assert oiejq.install_oiejq() + assert oiejq.check_oiejq() From 1eecdff7d623d558b01fdab27d038fe0b04a6c78 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sun, 21 Apr 2024 10:33:26 +0200 Subject: [PATCH 04/13] `verify` command (#225) * Add verify command * Update help * Broken tests * Small startup performance improvments * Dict for command names * Fix bug and remove java from compilers * Fix tests * Add tests * Fix tests * Dont create ocen archive with no ocen tests, create dlazaw in attachments * Add tests * Remove .cache dir * Add gitkeep in inwer package * Validate tests in parallel * Less colorful printing * Refactor --cpus flag * Refactor imports * Fix test * Remove cache on compilation flags or sanitizers change * Add tests * Fix again * Fix tests * Fix * Development versions support (#230) * Add dev versioning * Update tests * Bump version * Add test * Bump version * Fixed finding newest development version * Bump version * Fix version tests * Refactor * Cache `get_task_id()` * Allow caching with cwd * Fix test * Don't create ocen when dlazaw exists * Add test * Add flag for ignoring expected scores * Add test * Bump version * Update README * Add option to verify contest type * Add tests * Fix bugs * Fix typo * Refactor creating parsers * Change version for release --- README.md | 3 + setup.cfg | 1 + src/sinol_make/__init__.py | 40 +++-- src/sinol_make/commands/doc/__init__.py | 7 +- src/sinol_make/commands/export/__init__.py | 27 ++- src/sinol_make/commands/gen/__init__.py | 7 +- src/sinol_make/commands/ingen/__init__.py | 15 +- src/sinol_make/commands/ingen/ingen_util.py | 20 ++- src/sinol_make/commands/init/__init__.py | 4 +- src/sinol_make/commands/inwer/__init__.py | 20 ++- src/sinol_make/commands/inwer/inwer_util.py | 4 +- src/sinol_make/commands/outgen/__init__.py | 14 +- src/sinol_make/commands/run/__init__.py | 32 ++-- src/sinol_make/commands/verify/__init__.py | 170 ++++++++++++++++++ src/sinol_make/contest_types/default.py | 14 +- src/sinol_make/contest_types/icpc.py | 3 + src/sinol_make/contest_types/oi.py | 16 ++ src/sinol_make/helpers/cache.py | 13 +- src/sinol_make/helpers/compile.py | 18 +- src/sinol_make/helpers/compiler.py | 17 +- src/sinol_make/helpers/func_cache.py | 30 ++++ src/sinol_make/helpers/package_util.py | 62 +++++-- src/sinol_make/helpers/parsers.py | 20 ++- src/sinol_make/structs/cache_structs.py | 12 +- src/sinol_make/util.py | 58 +++--- tests/commands/export/test_integration.py | 47 +++++ tests/commands/gen/test_integration.py | 39 +++- tests/commands/gen/test_unit.py | 9 +- tests/commands/inwer/test_integration.py | 28 +-- tests/commands/run/test_integration.py | 27 +++ tests/commands/verify/__init__.py | 0 tests/commands/verify/test_integration.py | 138 ++++++++++++++ tests/helpers/test_cache.py | 25 ++- tests/helpers/test_compile.py | 7 +- tests/helpers/test_package_util.py | 3 +- tests/packages/dlazaw/config.yml | 8 + tests/packages/dlazaw/dlazaw/epic_file | 1 + tests/packages/dlazaw/in/.gitkeep | 0 tests/packages/dlazaw/out/.gitkeep | 0 tests/packages/dlazaw/prog/dla.cpp | 9 + tests/packages/dlazaw/prog/dlaingen.cpp | 9 + tests/packages/stresstest/config.yml | 11 ++ tests/packages/stresstest/in/.gitkeep | 0 tests/packages/stresstest/out/.gitkeep | 0 tests/packages/stresstest/prog/str.cpp | 9 + tests/packages/stresstest/prog/stringen.cpp | 12 ++ .../packages/stresstest/prog/strstresstest.sh | 4 + tests/packages/wer/config.yml | 8 + tests/packages/wer/out/.gitkeep | 0 tests/packages/wer/prog/wer.cpp | 2 +- tests/test_multiple_arguments.py | 2 +- tests/test_util.py | 50 +++--- tests/util.py | 15 ++ 53 files changed, 881 insertions(+), 209 deletions(-) create mode 100644 src/sinol_make/commands/verify/__init__.py create mode 100644 src/sinol_make/helpers/func_cache.py create mode 100644 tests/commands/verify/__init__.py create mode 100644 tests/commands/verify/test_integration.py create mode 100644 tests/packages/dlazaw/config.yml create mode 100644 tests/packages/dlazaw/dlazaw/epic_file create mode 100644 tests/packages/dlazaw/in/.gitkeep create mode 100644 tests/packages/dlazaw/out/.gitkeep create mode 100644 tests/packages/dlazaw/prog/dla.cpp create mode 100644 tests/packages/dlazaw/prog/dlaingen.cpp create mode 100644 tests/packages/stresstest/config.yml create mode 100644 tests/packages/stresstest/in/.gitkeep create mode 100644 tests/packages/stresstest/out/.gitkeep create mode 100644 tests/packages/stresstest/prog/str.cpp create mode 100644 tests/packages/stresstest/prog/stringen.cpp create mode 100644 tests/packages/stresstest/prog/strstresstest.sh create mode 100644 tests/packages/wer/out/.gitkeep diff --git a/README.md b/README.md index 17a432ab..cb2d2e9e 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,9 @@ Run `sinol-make ingen --help` to see available flags. program to use, what tests to check and how many CPUs to use. Run `sinol-make inwer --help` to see available flags. - `sinol-make export` -- Creates archive ready to upload to sio2 or szkopul. Run `sinol-make export --help` to see all available flags. - `sinol-make doc` -- Compiles all LaTeX files in doc/ directory to PDF. Run `sinol-make doc --help` to see all available flags. +- `sinol-make verify` -- Verifies the package. This command runs stress tests (if available), verifies the config, +generates tests, generates problem statements, runs inwer and run all solutions. Ingen and inwer are compiled with +address and UB sanitizers. Run `sinol-make verify --help` to see all available flags. - `sinol-make init [id]` -- Creates package from template [on github](https://github.com/sio2project/sinol-make/tree/main/example_package) and sets task id to provided `[id]`. Requires an internet connection to run. You can also run multiple commands at once, for example: diff --git a/setup.cfg b/setup.cfg index 39193949..542bc314 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,7 @@ install_requires = dictdiffer importlib-resources psutil + packaging [options.packages.find] where = src diff --git a/src/sinol_make/__init__.py b/src/sinol_make/__init__.py index 18b84cad..5196abf8 100644 --- a/src/sinol_make/__init__.py +++ b/src/sinol_make/__init__.py @@ -6,7 +6,8 @@ from sinol_make import util, oiejq -__version__ = "1.5.30" + +__version__ = "1.6.0" def configure_parsers(): @@ -50,8 +51,10 @@ def main_exn(): parser = configure_parsers() arguments = [] curr_args = [] + commands = util.get_commands() + commands_dict = {command.get_name(): command for command in commands} for arg in sys.argv[1:]: - if arg in util.get_command_names() and not (len(curr_args) > 0 and curr_args[0] == 'init'): + if arg in commands_dict.keys() and not (len(curr_args) > 0 and curr_args[0] == 'init'): if curr_args: arguments.append(curr_args) curr_args = [arg] @@ -59,19 +62,19 @@ def main_exn(): curr_args.append(arg) if curr_args: arguments.append(curr_args) - commands = util.get_commands() + if not arguments: + parser.print_help() + exit(1) check_oiejq() for curr_args in arguments: args = parser.parse_args(curr_args) - command_found = False - for command in commands: - if command.get_name() == args.command: - command_found = True - if len(arguments) > 1: - print(f' {command.get_name()} command '.center(util.get_terminal_size()[1], '=')) - command.run(args) - if not command_found: + command = commands_dict.get(args.command, None) + if command: + if len(arguments) > 1: + print(f' {command.get_name()} command '.center(util.get_terminal_size()[1], '=')) + command.run(args) + else: parser.print_help() exit(1) @@ -79,6 +82,9 @@ def main_exn(): def main(): new_version = None try: + if util.is_dev(__version__): + print(util.warning('You are using a development version of sinol-make. ' + 'It may be unstable and contain bugs.')) new_version = util.check_for_updates(__version__) main_exn() except argparse.ArgumentError as err: @@ -92,6 +98,12 @@ def main(): 'https://github.com/sio2project/sinol-make/#reporting-bugs-and-contributing-code') finally: if new_version is not None: - print(util.warning( - f'New version of sinol-make is available (your version: {__version__}, available version: ' - f'{new_version}).\nYou can update it by running `pip3 install sinol-make --upgrade`.')) + if not util.is_dev(new_version): + print(util.warning( + f'New version of sinol-make is available (your version: {__version__}, available version: ' + f'{new_version}).\nYou can update it by running `pip3 install sinol-make --upgrade`.')) + elif util.is_dev(new_version): + print(util.warning( + f'New development version of sinol-make is available (your version: {__version__}, available ' + f'version: {new_version}).\nYou can update it by running `pip3 install sinol-make --pre --upgrade`.' + )) diff --git a/src/sinol_make/commands/doc/__init__.py b/src/sinol_make/commands/doc/__init__.py index 3d624bed..ace6f803 100644 --- a/src/sinol_make/commands/doc/__init__.py +++ b/src/sinol_make/commands/doc/__init__.py @@ -18,7 +18,7 @@ def get_name(self): return "doc" def compile_file_latex_div(self, file_path): - print(util.info(f'Compiling {os.path.basename(file_path)} (latex to dvi)...')) + print(f'Compiling {os.path.basename(file_path)} (latex to dvi)...') os.chdir(os.path.dirname(file_path)) subprocess.run(['latex', file_path]) dvi_file = os.path.splitext(file_path)[0] + '.dvi' @@ -35,7 +35,7 @@ def compile_file_latex_div(self, file_path): return True def compile_pdf_latex(self, file_path): - print(util.info(f'Compiling {os.path.basename(file_path)} (pdflatex)...')) + print(f'Compiling {os.path.basename(file_path)} (pdflatex)...') os.chdir(os.path.dirname(file_path)) subprocess.run(['pdflatex', file_path]) pdf_file = os.path.splitext(file_path)[0] + '.pdf' @@ -62,7 +62,7 @@ def move_logs(self): for pattern in self.LOG_PATTERNS: for file in glob.glob(os.path.join(os.getcwd(), 'doc', pattern)): os.rename(file, os.path.join(output_dir, os.path.basename(file))) - print(util.info(f'Compilation log files can be found in {os.path.relpath(output_dir, os.getcwd())}')) + print(f'Compilation log files can be found in {os.path.relpath(output_dir, os.getcwd())}') def configure_subparser(self, subparser: argparse.ArgumentParser): parser = subparser.add_parser( @@ -76,6 +76,7 @@ def configure_subparser(self, subparser: argparse.ArgumentParser): ' pdflatex - uses pdflatex. Works with .png and .jpg images.\n' ' latex_dvi - uses latex and dvipdf. Works with .ps and .eps images.', default='auto') parser.add_argument('files', type=str, nargs='*', help='files to compile') + return parser def run(self, args: argparse.Namespace): args = util.init_package_command(args) diff --git a/src/sinol_make/commands/export/__init__.py b/src/sinol_make/commands/export/__init__.py index 5d072040..358a3aff 100644 --- a/src/sinol_make/commands/export/__init__.py +++ b/src/sinol_make/commands/export/__init__.py @@ -28,15 +28,13 @@ def configure_subparser(self, subparser: argparse.ArgumentParser): self.get_name(), help='Create archive for oioioi upload', description='Creates archive in the current directory ready to upload to sio2 or szkopul.') - parser.add_argument('-c', '--cpus', type=int, - help=f'number of cpus to use to generate output files ' - f'(default: {util.default_cpu_count()})', - default=util.default_cpu_count()) + parsers.add_cpus_argument(parser, 'number of cpus to use to generate output files') parser.add_argument('--no-statement', dest='no_statement', action='store_true', help='allow export without statement') parser.add_argument('--export-ocen', dest='export_ocen', action='store_true', help='Create ocen archive') parsers.add_compilation_arguments(parser) + return parser def generate_input_tests(self): print('Generating tests...') @@ -97,6 +95,7 @@ def create_ocen(self, target_dir: str): Creates ocen archive for sio2. :param target_dir: Path to exported package. """ + print('Generating ocen archive...') attachments_dir = os.path.join(target_dir, 'attachments') if not os.path.exists(attachments_dir): os.makedirs(attachments_dir) @@ -109,12 +108,27 @@ def create_ocen(self, target_dir: str): os.makedirs(in_dir) out_dir = os.path.join(ocen_dir, 'out') os.makedirs(out_dir) + num_tests = 0 for ext in ['in', 'out']: for test in glob.glob(os.path.join(tests_dir, ext, f'{self.task_id}0*.{ext}')) + \ glob.glob(os.path.join(tests_dir, ext, f'{self.task_id}*ocen.{ext}')): shutil.copy(test, os.path.join(ocen_dir, ext, os.path.basename(test))) - - shutil.make_archive(os.path.join(attachments_dir, f'{self.task_id}ocen'), 'zip', tmpdir) + num_tests += 1 + + dlazaw_dir = os.path.join(os.getcwd(), 'dlazaw') + if num_tests == 0: + print(util.warning('No ocen tests found.')) + elif os.path.exists(dlazaw_dir): + print(util.warning('Skipping ocen archive creation because dlazaw directory exists.')) + else: + shutil.make_archive(os.path.join(attachments_dir, f'{self.task_id}ocen'), 'zip', tmpdir) + + if os.path.exists(dlazaw_dir): + print('Archiving dlazaw directory and adding to attachments.') + os.makedirs(os.path.join(tmpdir, 'dlazaw'), exist_ok=True) + shutil.copytree(dlazaw_dir, os.path.join(tmpdir, 'dlazaw', 'dlazaw')) + shutil.make_archive(os.path.join(attachments_dir, 'dlazaw'), 'zip', + os.path.join(tmpdir, 'dlazaw')) def compile_statement(self): command = DocCommand() @@ -168,7 +182,6 @@ def copy_package_required_files(self, target_dir: str): self.generate_output_files() if self.args.export_ocen: - print('Generating ocen archive...') self.create_ocen(target_dir) def clear_files(self, target_dir: str): diff --git a/src/sinol_make/commands/gen/__init__.py b/src/sinol_make/commands/gen/__init__.py index afb3b53f..18719100 100644 --- a/src/sinol_make/commands/gen/__init__.py +++ b/src/sinol_make/commands/gen/__init__.py @@ -31,13 +31,12 @@ def configure_subparser(self, subparser): help='path to ingen source file, for example prog/abcingen.cpp') parser.add_argument('-i', '--only-inputs', action='store_true', help='generate input files only') parser.add_argument('-o', '--only-outputs', action='store_true', help='generate output files only') - parser.add_argument('-c', '--cpus', type=int, - help=f'number of cpus to use to generate output files ' - f'(default: {util.default_cpu_count()})', - default=util.default_cpu_count()) + parsers.add_cpus_argument(parser, 'number of cpus to use to generate output files') parser.add_argument('-n', '--no-validate', default=False, action='store_true', help='do not validate test contents') + parsers.add_fsanitize_argument(parser) parsers.add_compilation_arguments(parser) + return parser def run(self, args: argparse.Namespace): args = util.init_package_command(args) diff --git a/src/sinol_make/commands/ingen/__init__.py b/src/sinol_make/commands/ingen/__init__.py index 87794a71..6f45acaa 100644 --- a/src/sinol_make/commands/ingen/__init__.py +++ b/src/sinol_make/commands/ingen/__init__.py @@ -30,7 +30,10 @@ def configure_subparser(self, subparser): help='path to ingen source file, for example prog/abcingen.cpp') parser.add_argument('-n', '--no-validate', default=False, action='store_true', help='do not validate test contents') + parsers.add_cpus_argument(parser, 'number of cpus used for validating tests') + parsers.add_fsanitize_argument(parser) parsers.add_compilation_arguments(parser) + return parser def delete_dangling_files(self, dates): to_delete = set() @@ -51,7 +54,7 @@ def delete_dangling_files(self, dates): static_files = set([os.path.basename(test) for test in static_files]) to_delete = to_delete - static_files if to_delete: - print(util.info('Cleaning up old input files.')) + print('Cleaning up old input files.') for test in to_delete: os.remove(os.path.join(os.getcwd(), "in", test)) @@ -61,11 +64,10 @@ def run(self, args: argparse.Namespace): self.args = args self.task_id = package_util.get_task_id() - package_util.validate_test_names(self.task_id) util.change_stack_size_to_unlimited() self.ingen = get_ingen(self.task_id, args.ingen_path) - print(util.info(f'Using ingen file {os.path.basename(self.ingen)}')) - self.ingen_exe = compile_ingen(self.ingen, self.args, self.args.compile_mode) + print(f'Using ingen file {os.path.basename(self.ingen)}') + self.ingen_exe = compile_ingen(self.ingen, self.args, self.args.compile_mode, self.args.fsanitize) previous_tests = [] try: @@ -89,8 +91,5 @@ def run(self, args: argparse.Namespace): f.write("\n".join(glob.glob(os.path.join(os.getcwd(), "in", f"{self.task_id}*.in")))) if not self.args.no_validate: - print(util.info('Validating input test contents.')) tests = sorted(glob.glob(os.path.join(os.getcwd(), "in", f"{self.task_id}*.in"))) - for test in tests: - package_util.validate_test(test) - print(util.info('Input test contents are valid!')) + package_util.validate_tests(tests, self.args.cpus, 'input') diff --git a/src/sinol_make/commands/ingen/ingen_util.py b/src/sinol_make/commands/ingen/ingen_util.py index 2b6608da..a8c03ca8 100644 --- a/src/sinol_make/commands/ingen/ingen_util.py +++ b/src/sinol_make/commands/ingen/ingen_util.py @@ -47,7 +47,7 @@ def get_ingen(task_id, ingen_path=None): return correct_ingen -def compile_ingen(ingen_path: str, args: argparse.Namespace, compilation_flags='default'): +def compile_ingen(ingen_path: str, args: argparse.Namespace, compilation_flags='default', use_fsanitize=False): """ Compiles ingen and returns path to compiled executable. If ingen_path is shell script, then it will be returned. @@ -57,7 +57,7 @@ def compile_ingen(ingen_path: str, args: argparse.Namespace, compilation_flags=' compilers = compiler.verify_compilers(args, [ingen_path]) ingen_exe, compile_log_path = compile.compile_file(ingen_path, package_util.get_executable(ingen_path), - compilers, compilation_flags, use_fsanitize=True, + compilers, compilation_flags, use_fsanitize=use_fsanitize, additional_flags='-D_INGEN') if ingen_exe is None: @@ -86,11 +86,21 @@ def run_ingen(ingen_exe, working_dir=None): print(util.bold(' Ingen output '.center(util.get_terminal_size()[1], '='))) process = subprocess.Popen([ingen_exe], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=working_dir, shell=is_shell) + whole_output = '' while process.poll() is None: - print(process.stdout.readline().decode('utf-8'), end='') - - print(process.stdout.read().decode('utf-8'), end='') + out = process.stdout.readline().decode('utf-8') + if out != '': + print(out, end='') + whole_output += out + out = process.stdout.read().decode('utf-8') + whole_output += out + print(out, end='') exit_code = process.returncode print(util.bold(' End of ingen output '.center(util.get_terminal_size()[1], '='))) + if util.has_sanitizer_error(whole_output, exit_code): + print(util.warning('Warning: if ingen failed due to sanitizer errors, you can either run ' + '`sudo sysctl vm.mmap_rnd_bits = 28` to fix this or disable sanitizers with the ' + '--no-fsanitize flag.')) + return exit_code == 0 diff --git a/src/sinol_make/commands/init/__init__.py b/src/sinol_make/commands/init/__init__.py index 8ae00b2f..d675d103 100644 --- a/src/sinol_make/commands/init/__init__.py +++ b/src/sinol_make/commands/init/__init__.py @@ -23,6 +23,7 @@ def configure_subparser(self, subparser: argparse.ArgumentParser): description='Create package from predefined template with given id.' ) parser.add_argument('task_id', type=str, help='Id of the task to create') + return parser def download_template(self): repo = 'https://github.com/sio2project/sinol-make.git' @@ -69,8 +70,7 @@ def run(self, args: argparse.Namespace): self.move_folder() self.update_config() - + self.used_tmpdir.cleanup() print(util.info(f'Successfully created task "{self.task_id}"')) - diff --git a/src/sinol_make/commands/inwer/__init__.py b/src/sinol_make/commands/inwer/__init__.py index 71245b97..e0885900 100644 --- a/src/sinol_make/commands/inwer/__init__.py +++ b/src/sinol_make/commands/inwer/__init__.py @@ -10,8 +10,7 @@ from sinol_make import util from sinol_make.structs.inwer_structs import TestResult, InwerExecution, VerificationResult, TableData -from sinol_make.helpers import package_util, compile, printer, paths -from sinol_make.helpers.parsers import add_compilation_arguments +from sinol_make.helpers import package_util, printer, paths, parsers from sinol_make.interfaces.BaseCommand import BaseCommand from sinol_make.commands.inwer import inwer_util @@ -37,9 +36,10 @@ def configure_subparser(self, subparser: argparse.ArgumentParser): help='path to inwer source file, for example prog/abcinwer.cpp') parser.add_argument('-t', '--tests', type=str, nargs='+', help='test to verify, for example in/abc{0,1}*') - parser.add_argument('-c', '--cpus', type=int, - help=f'number of cpus to use (default: {util.default_cpu_count()})') - add_compilation_arguments(parser) + parsers.add_cpus_argument(parser, 'number of cpus to use when verifying tests') + parsers.add_fsanitize_argument(parser) + parsers.add_compilation_arguments(parser) + return parser @staticmethod def verify_test(execution: InwerExecution) -> VerificationResult: @@ -94,11 +94,14 @@ def verify_and_print_table(self) -> Dict[str, TestResult]: thr.start() keyboard_interrupt = False + sanitizer_error = False try: with mp.Pool(self.cpus) as pool: for i, result in enumerate(pool.imap(self.verify_test, executions)): table_data.results[result.test_path].set_results(result.valid, result.output) table_data.i = i + if util.has_sanitizer_error(result.output, 0 if result.valid else 1): + sanitizer_error = True except KeyboardInterrupt: keyboard_interrupt = True @@ -108,6 +111,10 @@ def verify_and_print_table(self) -> Dict[str, TestResult]: print("\n".join(inwer_util.print_view(terminal_width, terminal_height, table_data)[0])) + if sanitizer_error: + print(util.warning('Warning: if inwer failed due to sanitizer errors, you can either run ' + '`sudo sysctl vm.mmap_rnd_bits = 28` to fix this or disable sanitizers with the ' + '--no-fsanitize flag.')) if keyboard_interrupt: util.exit_with_error('Keyboard interrupt.') @@ -203,7 +210,7 @@ def run(self, args: argparse.Namespace): print('Verifying tests: ' + util.bold(', '.join(self.tests))) util.change_stack_size_to_unlimited() - self.inwer_executable = inwer_util.compile_inwer(self.inwer, args, args.compile_mode) + self.inwer_executable = inwer_util.compile_inwer(self.inwer, args, args.compile_mode, args.fsanitize) results: Dict[str, TestResult] = self.verify_and_print_table() print('') @@ -218,4 +225,3 @@ def run(self, args: argparse.Namespace): print("Verifying tests order...") self.verify_tests_order() print(util.info('Verification successful.')) - exit(0) diff --git a/src/sinol_make/commands/inwer/inwer_util.py b/src/sinol_make/commands/inwer/inwer_util.py index 6d21f0c0..53618603 100644 --- a/src/sinol_make/commands/inwer/inwer_util.py +++ b/src/sinol_make/commands/inwer/inwer_util.py @@ -29,13 +29,13 @@ def get_inwer_path(task_id: str, path=None) -> Union[str, None]: return None -def compile_inwer(inwer_path: str, args: argparse.Namespace, compilation_flags='default'): +def compile_inwer(inwer_path: str, args: argparse.Namespace, compilation_flags='default', use_fsanitize=False): """ Compiles inwer and returns path to compiled executable and path to compile log. """ compilers = compiler.verify_compilers(args, [inwer_path]) inwer_exe, compile_log_path = compile.compile_file(inwer_path, package_util.get_executable(inwer_path), compilers, - compilation_flags, use_fsanitize=True, + compilation_flags, use_fsanitize=use_fsanitize, additional_flags='-D_INWER') if inwer_exe is None: diff --git a/src/sinol_make/commands/outgen/__init__.py b/src/sinol_make/commands/outgen/__init__.py index c32b7c5a..3a188484 100644 --- a/src/sinol_make/commands/outgen/__init__.py +++ b/src/sinol_make/commands/outgen/__init__.py @@ -26,14 +26,11 @@ def configure_subparser(self, subparser): help='Generate output files', description='Generate output files using the correct solution.' ) - - parser.add_argument('-c', '--cpus', type=int, - help=f'number of cpus to use to generate output files ' - f'(default: {util.default_cpu_count()})', - default=util.default_cpu_count()) + parsers.add_cpus_argument(parser, 'number of cpus to use to generate output files') parser.add_argument('-n', '--no-validate', default=False, action='store_true', help='do not validate test contents') parsers.add_compilation_arguments(parser) + return parser def generate_outputs(self, outputs_to_generate): print(f'Generating output files for {len(outputs_to_generate)} tests on {self.args.cpus} cpus.') @@ -49,7 +46,7 @@ def generate_outputs(self, outputs_to_generate): for i, result in enumerate(pool.imap(generate_output, arguments)): results.append(result) if result: - print(util.info(f'Successfully generated output file {os.path.basename(arguments[i].output_test)}')) + print(f'Successfully generated output file {os.path.basename(arguments[i].output_test)}') else: print(util.error(f'Failed to generate output file {os.path.basename(arguments[i].output_test)}')) @@ -129,7 +126,4 @@ def run(self, args: argparse.Namespace): yaml.dump(md5_sums, f) if not self.args.no_validate: - print(util.info('Validating output test contents.')) - for test in sorted(outputs_to_generate): - package_util.validate_test(test) - print(util.info('Output test contents are valid!')) + package_util.validate_tests(sorted(outputs_to_generate), self.args.cpus, 'outputs') diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index c7c6190e..161b9e5a 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -8,21 +8,23 @@ import psutil import glob import shutil +import os +import collections +import sys +import math +import dictdiffer +import multiprocessing as mp from io import StringIO from typing import Dict -from sinol_make import contest_types, oiejq +from sinol_make import contest_types, oiejq, util from sinol_make.structs.run_structs import ExecutionData, PrintData from sinol_make.structs.cache_structs import CacheTest, CacheFile -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, paths, cache +from sinol_make.helpers import compile, compiler, package_util, printer, paths, cache, parsers from sinol_make.structs.status_structs import Status, ResultChange, PointsChange, ValidationResult, ExecutionResult, \ TotalPointsChange -import sinol_make.util as util -import yaml, os, collections, sys, re, math, dictdiffer -import multiprocessing as mp def color_memory(memory, limit): @@ -288,8 +290,7 @@ def configure_subparser(self, subparser): help='solutions to be run, for example prog/abc{b,s}*.{cpp,py}') parser.add_argument('-t', '--tests', type=str, nargs='+', help='tests to be run, for example in/abc{0,1}*') - parser.add_argument('-c', '--cpus', type=int, - help=f'number of cpus to use (default: {util.default_cpu_count()}') + parsers.add_cpus_argument(parser, 'number of cpus to use when running solutions') parser.add_argument('--tl', type=float, help='time limit for all tests (in s)') parser.add_argument('--ml', type=float, help='memory limit for all tests (in MB)') parser.add_argument('--hide-memory', dest='hide_memory', action='store_true', @@ -300,9 +301,13 @@ def configure_subparser(self, subparser): help='path to oiejq executable (default: `~/.local/bin/oiejq`)') parser.add_argument('-a', '--apply-suggestions', dest='apply_suggestions', action='store_true', help='apply suggestions from expected scores report') + parser.add_argument('--ignore-expected', dest='ignore_expected', action='store_true', + help='ignore expected scores from config.yml. When this flag is set, ' + 'the expected scores are not compared with the actual scores.') parser.add_argument('--no-outputs', dest='allow_no_outputs', action='store_true', help='allow running the script without full outputs') - add_compilation_arguments(parser) + parsers.add_compilation_arguments(parser) + return parser def parse_time(self, time_str): if len(time_str) < 3: return -1 @@ -1018,7 +1023,7 @@ def set_group_result(solution, group, result): self.config["sinol_expected_scores"] = self.convert_status_to_string(config_expected_scores) util.save_config(self.config) - print(util.info("Saved suggested expected scores description.")) + print("Saved suggested expected scores description.") else: util.exit_with_error("Use flag --apply-suggestions to apply suggestions.") @@ -1217,7 +1222,7 @@ def run(self, args): checker = package_util.get_files_matching_pattern(self.ID, f'{self.ID}chk.*') if len(checker) != 0: - print(util.info("Checker found: %s" % os.path.basename(checker[0]))) + print("Checker found: %s" % os.path.basename(checker[0])) self.checker = checker[0] self.compile_checker() else: @@ -1244,6 +1249,11 @@ def run(self, args): results, all_results = self.compile_and_run(solutions) self.check_errors(all_results) + if self.args.ignore_expected: + print(util.warning("Ignoring expected scores.")) + self.exit() + return + try: validation_results = self.validate_expected_scores(results) except Exception: diff --git a/src/sinol_make/commands/verify/__init__.py b/src/sinol_make/commands/verify/__init__.py new file mode 100644 index 00000000..978432a9 --- /dev/null +++ b/src/sinol_make/commands/verify/__init__.py @@ -0,0 +1,170 @@ +import os +import stat +import shutil +import argparse +import subprocess + +from sinol_make import util, contest_types +from sinol_make.helpers import parsers, package_util, paths +from sinol_make.interfaces.BaseCommand import BaseCommand +from sinol_make.commands.gen import Command as GenCommand +from sinol_make.commands.doc import Command as DocCommand +from sinol_make.commands.inwer import Command as InwerCommand, inwer_util +from sinol_make.commands.run import Command as RunCommand + + +class Command(BaseCommand): + """ + Class for `verify` command. + """ + + def get_name(self): + return "verify" + + def configure_subparser(self, subparser): + parser = subparser.add_parser( + self.get_name(), + help='Verify the package', + description='Verify the whole package. This command will first ' + 'run stress tests (if the file `prog/{task_id}stresstest.sh` exists), ' + 'verify the config, generate tests, generate problem ' + 'statements, run inwer and run all solutions. ' + 'Ingen and inwer are compiled with sanitizers (-fsanitize=address,undefined), ' + 'which may fail on some systems. To fix this, run `sudo sysctl vm.mmap_rnd_bits = 28` ' + 'or disable sanitizers with --no-fsanitize.' + ) + + parser.add_argument('-e', '--expected-contest-type', type=str, + help='expected contest type. Fails if the actual contest type is different.') + parser.add_argument('-f', '--no-fsanitize', action='store_true', default=False, + help='do not use sanitizers for ingen and inwer programs') + parsers.add_cpus_argument(parser, 'number of cpus that sinol-make will use') + parser.add_argument('--ignore-expected', dest='ignore_expected', action='store_true', + help='ignore expected scores from config.yml. When this flag is set, ' + 'the expected scores are not compared with the actual scores. ' + 'This flag will be passed to the run command.') + parsers.add_compilation_arguments(parser) + + def correct_contest_type(self): + if self.args.expected_contest_type is not None: + if self.contest.get_type() != self.args.expected_contest_type.lower(): + util.exit_with_error(f"Invalid contest type '{self.contest.get_type()}'. " + f"Expected '{self.args.expected_contest_type}'.") + + def remove_cache(self): + """ + Remove whole cache dir + """ + cache_dir = paths.get_cache_path() + if os.path.exists(cache_dir): + shutil.rmtree(cache_dir) + + def check_extra_files(self): + """ + Checks if extra_compilation_files and extra_execution_files exist. + """ + extra_compilation_files = self.config.get('extra_compilation_files', []) + for file in extra_compilation_files: + if not os.path.exists(os.path.join(os.getcwd(), "prog", file)): + util.exit_with_error(f"Extra compilation file `{file}` does not exist. " + f"It should be in `prog` directory.") + if extra_compilation_files: + print(util.info("All extra compilation files exist.")) + + extra_execution_files = self.config.get('extra_execution_files', {}) + for lang, files in extra_execution_files.items(): + for file in files: + if not os.path.exists(os.path.join(os.getcwd(), "prog", file)): + util.exit_with_error(f"Extra execution file `{file}` for language `{lang}` does not exist. " + f"It should be in `prog` directory.") + if extra_execution_files: + print(util.info("All extra execution files exist.")) + + def verify_scores(self, scored_groups): + config_scores = self.config.get('scores', {}) + if not config_scores: + return + if '0' in scored_groups: + scored_groups.remove('0') + if 0 in scored_groups: + scored_groups.remove(0) + + for group in scored_groups: + if int(group) not in config_scores: + util.exit_with_error(f"Score for group '{group}' not found. " + f"You must either provide scores for all groups " + f"or not provide them at all (to have them assigned automatically).") + + for group in config_scores: + if int(group) not in scored_groups: + util.exit_with_error(f"Score for group '{group}' found in config, " + f"but no such test group exists in scored groups. " + f"You must either provide scores for all groups " + f"or not provide them at all (to have them assigned automatically).") + + print(util.info("All scores are provided for all groups.")) + + def prepare_args(self, command): + parser = argparse.ArgumentParser() + subparser = parser.add_subparsers(dest='command') + command_parser = command.configure_subparser(subparser) + command_args = command_parser.parse_args([]) + for key, value in vars(self.args).items(): + setattr(command_args, key, value) + setattr(command_args, 'fsanitize', not self.args.no_fsanitize) + return command_args + + def run_stresstests(self): + stresstests_path = os.path.join(os.getcwd(), 'prog', self.task_id + 'stresstest.sh') + if not os.path.exists(stresstests_path): + return + + print(util.bold(' Running stress tests '.center(util.get_terminal_size()[1], '='))) + print(f"See the comments in `prog/{self.task_id}stresstest.sh` for details.".center( + util.get_terminal_size()[1], ' ')) + st = os.stat(stresstests_path) + os.chmod(stresstests_path, st.st_mode | stat.S_IEXEC) + p = subprocess.Popen([stresstests_path], shell=True) + p.wait() + if p.returncode != 0: + util.exit_with_error("Stress tests failed.") + + def run(self, args: argparse.Namespace): + self.args = util.init_package_command(args) + self.config = package_util.get_config() + self.task_id = package_util.get_task_id() + self.contest = contest_types.get_contest_type() + + self.correct_contest_type() + self.remove_cache() + self.check_extra_files() + self.contest.verify_pre_gen() + + # Run stresstests (if present) + self.run_stresstests() + + # Generate tests + print(util.bold(' Generating tests '.center(util.get_terminal_size()[1], '='))) + gen = GenCommand() + gen.run(self.prepare_args(gen)) + self.verify_scores(package_util.get_groups(package_util.get_all_inputs(self.task_id), self.task_id)) + + # Generate problem statements + print(util.bold(' Generating problem statements '.center(util.get_terminal_size()[1], '='))) + doc = DocCommand() + doc.run(self.prepare_args(doc)) + + # Run inwer + if inwer_util.get_inwer_path(self.task_id) is None: + print(util.warning("Package doesn't have inwer.")) + else: + print(util.bold(' Running inwer '.center(util.get_terminal_size()[1], '='))) + inwer = InwerCommand() + inwer.run(self.prepare_args(inwer)) + + # Run solutions + print(util.bold(' Running solutions '.center(util.get_terminal_size()[1], '='))) + run = RunCommand() + run.run(self.prepare_args(run)) + + print(util.info('Package verification successful.')) diff --git a/src/sinol_make/contest_types/default.py b/src/sinol_make/contest_types/default.py index 07015957..6c4db0b0 100644 --- a/src/sinol_make/contest_types/default.py +++ b/src/sinol_make/contest_types/default.py @@ -16,6 +16,12 @@ class DefaultContest: Max possible score is sum of group scores. """ + def get_type(self) -> str: + """ + Returns type of contest. + """ + return "default" + def argument_overrides(self, args: argparse.Namespace) -> argparse.Namespace: """ Add contest specific arguments @@ -64,7 +70,7 @@ def get_possible_score(self, groups: List[int], scores: Dict[int, int]) -> int: :param scores: Dictionary: {"": } :return: Maximum possible score. """ - if groups[0] == 0 and len(groups) == 1: + if len(groups) == 0 or (groups[0] == 0 and len(groups) == 1): return 0 possible_score = 0 @@ -126,3 +132,9 @@ def max_score_per_test(self): Returns maximum score for single test """ return 100 + + def verify_pre_gen(self): + """ + Called by verify command before generating tests. + """ + pass diff --git a/src/sinol_make/contest_types/icpc.py b/src/sinol_make/contest_types/icpc.py index 32737b4d..ef56103d 100644 --- a/src/sinol_make/contest_types/icpc.py +++ b/src/sinol_make/contest_types/icpc.py @@ -12,6 +12,9 @@ class ICPCContest(DefaultContest): The score is 0 if any of the tests fail. """ + def get_type(self) -> str: + return "icpc" + def assign_scores(self, groups: List[int]) -> Dict[int, int]: return {group: 1 for group in groups} diff --git a/src/sinol_make/contest_types/oi.py b/src/sinol_make/contest_types/oi.py index ea4f0bb4..7e0cb4ac 100644 --- a/src/sinol_make/contest_types/oi.py +++ b/src/sinol_make/contest_types/oi.py @@ -1,5 +1,7 @@ import argparse +from sinol_make import util +from sinol_make.helpers import package_util from sinol_make.structs.status_structs import ExecutionResult from sinol_make.contest_types.default import DefaultContest @@ -9,6 +11,9 @@ class OIContest(DefaultContest): Contest type for Polish Olympiad in Informatics. """ + def get_type(self) -> str: + return "oi" + def argument_overrides(self, args: argparse.Namespace) -> argparse.Namespace: """ Add arguments for features required by OI contest @@ -28,3 +33,14 @@ def get_test_score(self, result: ExecutionResult, time_limit, memory_limit): return result.Points else: return 1 + int((result.Points - 1) * ((time_limit - result.Time) / (time_limit / 2.0))) + + def verify_pre_gen(self): + """ + Verify if scores sum up to 100. + """ + config = package_util.get_config() + if 'scores' not in config: + util.exit_with_error("Scores are not defined in config.yml.") + total_score = sum(config['scores'].values()) + if total_score != 100: + util.exit_with_error(f"Total score in config is {total_score}, but should be 100.") diff --git a/src/sinol_make/helpers/cache.py b/src/sinol_make/helpers/cache.py index 9f405c4f..cca819fd 100644 --- a/src/sinol_make/helpers/cache.py +++ b/src/sinol_make/helpers/cache.py @@ -35,7 +35,7 @@ def get_cache_file(solution_path: str) -> CacheFile: return CacheFile() -def check_compiled(file_path: str) -> Union[str, None]: +def check_compiled(file_path: str, compilation_flags: str, sanitizers: bool) -> Union[str, None]: """ Check if a file is compiled :param file_path: Path to the file @@ -44,7 +44,7 @@ def check_compiled(file_path: str) -> Union[str, None]: md5sum = util.get_file_md5(file_path) try: info = get_cache_file(file_path) - if info.md5sum == md5sum: + if info.md5sum == md5sum and info.compilation_flags == compilation_flags and info.sanitizers == sanitizers: exe_path = info.executable_path if os.path.exists(exe_path): return exe_path @@ -53,19 +53,18 @@ def check_compiled(file_path: str) -> Union[str, None]: return None -def save_compiled(file_path: str, exe_path: str, is_checker: bool = False): +def save_compiled(file_path: str, exe_path: str, compilation_flags: str, sanitizers: bool, is_checker: bool = False): """ 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 + :param compilation_flags: Compilation flags used + :param sanitizers: Whether -fsanitize=undefined,address was used :param is_checker: Whether the compiled file is a checker. If True, all cached tests are removed. """ - info = CacheFile() - info.executable_path = exe_path - info.md5sum = util.get_file_md5(file_path) + info = CacheFile(util.get_file_md5(file_path), exe_path, compilation_flags, sanitizers) info.save(file_path) - if is_checker: remove_results_cache() diff --git a/src/sinol_make/helpers/compile.py b/src/sinol_make/helpers/compile.py index 07500090..d3dc8958 100644 --- a/src/sinol_make/helpers/compile.py +++ b/src/sinol_make/helpers/compile.py @@ -37,10 +37,17 @@ def compile(program, output, compilers: Compilers = None, compile_log=None, comp if use_fsanitize and util.is_macos_arm(): use_fsanitize = False + if compilation_flags == 'w': + compilation_flags = 'weak' + elif compilation_flags == 'o': + compilation_flags = 'oioioi' + elif compilation_flags == 'd': + compilation_flags = 'default' + if extra_compilation_files is None: extra_compilation_files = [] - compiled_exe = check_compiled(program) + compiled_exe = check_compiled(program, compilation_flags, use_fsanitize) if compiled_exe is not None: if compile_log is not None: compile_log.write(f'Using cached executable {compiled_exe}\n') @@ -53,12 +60,11 @@ def compile(program, output, compilers: Compilers = None, compile_log=None, comp shutil.copy(file, os.path.join(os.path.dirname(output), os.path.basename(file))) gcc_compilation_flags = '' - if compilation_flags == 'weak' or compilation_flags == 'w': - compilation_flags = 'weak' + if compilation_flags == 'weak': gcc_compilation_flags = '' # Disable all warnings - elif compilation_flags == 'oioioi' or compilation_flags == 'o': + elif compilation_flags == 'oioioi': gcc_compilation_flags = ' -Wall -Wno-unused-result -Werror' # Same flags as oioioi - elif compilation_flags == 'default' or compilation_flags == 'd': + elif compilation_flags == 'default': gcc_compilation_flags = ' -Werror -Wall -Wextra -Wshadow -Wconversion -Wno-unused-result -Wfloat-equal' else: util.exit_with_error(f'Unknown compilation flags group: {compilation_flags}') @@ -108,7 +114,7 @@ def compile(program, output, compilers: Compilers = None, compile_log=None, comp if process.returncode != 0: raise CompilationError('Compilation failed') else: - save_compiled(program, output, is_checker) + save_compiled(program, output, compilation_flags, use_fsanitize, is_checker) return True diff --git a/src/sinol_make/helpers/compiler.py b/src/sinol_make/helpers/compiler.py index 6b11bc36..8a1a68a4 100644 --- a/src/sinol_make/helpers/compiler.py +++ b/src/sinol_make/helpers/compiler.py @@ -1,12 +1,12 @@ from typing import List - +import subprocess import argparse +import sys import os -import sinol_make.util as util -import sys, subprocess - +from sinol_make import util from sinol_make.structs.compiler_structs import Compilers +from sinol_make.helpers.func_cache import cache_result def check_if_installed(compiler): @@ -22,6 +22,7 @@ def check_if_installed(compiler): return True +@cache_result() def get_c_compiler_path(): """ Get the C compiler @@ -41,6 +42,7 @@ def get_c_compiler_path(): return None +@cache_result() def get_cpp_compiler_path(): """ Get the C++ compiler @@ -62,6 +64,7 @@ def get_cpp_compiler_path(): return None +@cache_result() def get_python_interpreter_path(): """ Get the Python interpreter @@ -75,6 +78,7 @@ def get_python_interpreter_path(): return None +@cache_result() def get_java_compiler_path(): """ Get the Java compiler @@ -94,7 +98,8 @@ def get_default_compilers(): c_compiler_path=get_c_compiler_path(), cpp_compiler_path=get_cpp_compiler_path(), python_interpreter_path=get_python_interpreter_path(), - java_compiler_path=get_java_compiler_path() + # Java is not currently supported by sinol-make + # java_compiler_path=get_java_compiler_path() ) @@ -141,5 +146,5 @@ def verify_compilers(args: argparse.Namespace, solutions: List[str]) -> Compiler c_compiler_path=args.c_compiler_path, cpp_compiler_path=args.cpp_compiler_path, python_interpreter_path=args.python_interpreter_path, - java_compiler_path=args.java_compiler_path + java_compiler_path=None ) diff --git a/src/sinol_make/helpers/func_cache.py b/src/sinol_make/helpers/func_cache.py new file mode 100644 index 00000000..6b493489 --- /dev/null +++ b/src/sinol_make/helpers/func_cache.py @@ -0,0 +1,30 @@ +import os + +__cache = {} + + +def cache_result(cwd=False): + """ + Function to cache the result of a function. + """ + def decorator(func): + def wrapper(*args, **kwargs): + if cwd: + key = (func.__name__, os.getcwd()) + else: + key = func.__name__ + + if key in __cache: + return __cache[key] + result = func(*args, **kwargs) + __cache[key] = result + return result + return wrapper + return decorator + + +def clear_cache(): + """ + Function to clear the cache. + """ + __cache.clear() diff --git a/src/sinol_make/helpers/package_util.py b/src/sinol_make/helpers/package_util.py index d155e746..98d582b8 100644 --- a/src/sinol_make/helpers/package_util.py +++ b/src/sinol_make/helpers/package_util.py @@ -3,13 +3,16 @@ import yaml import glob import fnmatch +import multiprocessing as mp from enum import Enum -from typing import List, Union, Dict, Any +from typing import List, Union, Dict, Any, Tuple +from sinol_make.helpers.func_cache import cache_result from sinol_make import util from sinol_make.helpers import paths +@cache_result(cwd=True) def get_task_id() -> str: config = get_config() if "sinol_task_id" in config: @@ -266,6 +269,14 @@ def get_memory_limit(test_path, config, lang, task_id, args=None): return _get_limit(LimitTypes.MEMORY_LIMIT, test_path, str_config, lang, task_id) +def get_in_tests_re(task_id: str) -> re.Pattern: + return re.compile(r'^%s(([0-9]+)([a-z]?[a-z0-9]*))\.in$' % re.escape(task_id)) + + +def get_out_tests_re(task_id: str) -> re.Pattern: + return re.compile(r'^%s(([0-9]+)([a-z]?[a-z0-9]*))\.out$' % re.escape(task_id)) + + def validate_test_names(task_id): """ Checks if all files in the package have valid names. @@ -278,12 +289,12 @@ def get_invalid_files(path, pattern): invalid_files.append(os.path.basename(file)) return invalid_files - in_test_re = re.compile(r'^(%s(([0-9]+)([a-z]?[a-z0-9]*))).in$' % (re.escape(task_id))) + in_test_re = get_in_tests_re(task_id) invalid_in_tests = get_invalid_files(os.path.join("in", "*.in"), in_test_re) if len(invalid_in_tests) > 0: util.exit_with_error(f'Input tests with invalid names: {", ".join(invalid_in_tests)}.') - out_test_re = re.compile(r'^(%s(([0-9]+)([a-z]?[a-z0-9]*))).out$' % (re.escape(task_id))) + out_test_re = get_out_tests_re(task_id) invalid_out_tests = get_invalid_files(os.path.join("out", "*.out"), out_test_re) if len(invalid_out_tests) > 0: util.exit_with_error(f'Output tests with invalid names: {", ".join(invalid_out_tests)}.') @@ -345,11 +356,12 @@ def save_contest_type_to_cache(contest_type): contest_type_file.write(contest_type) -def validate_test(test_path: str): +def validate_test(test_path: str) -> Tuple[bool, str]: """ Check if test doesn't contain leading/trailing whitespaces, has only one space between tokens and ends with newline. Exits with error if any of the conditions is not met. + :return: Tuple of two values: True if test is valid, error message otherwise. """ basename = os.path.basename(test_path) num_empty = 0 @@ -358,21 +370,51 @@ def validate_test(test_path: str): for i, line in enumerate(lines): line = line.decode('utf-8') if len(line) > 0 and line[0] == ' ': - util.exit_with_error(f'Leading whitespace in {basename}:{i + 1}') + return False, util.error(f'Leading whitespace in {basename}:{i + 1}') if len(line) > 0 and (line[-2:] == '\r\n' or line[-2:] == '\n\r' or line[-1] == '\r'): - util.exit_with_error(f'Carriage return at the end of {basename}:{i + 1}') + return False, util.error(f'Carriage return at the end of {basename}:{i + 1}') if len(line) > 0 and line[-1] != '\n': - util.exit_with_error(f'No newline at the end of {basename}') + return False, util.error(f'No newline at the end of {basename}') if line == '\n' or line == '': num_empty += 1 continue elif i == len(lines) - 1: num_empty = 0 if line[-2] == ' ': - util.exit_with_error(f'Trailing whitespace in {basename}:{i + 1}') + return False, util.error(f'Trailing whitespace in {basename}:{i + 1}') for j in range(len(line) - 1): if line[j] == ' ' and line[j + 1] == ' ': - util.exit_with_error(f'Tokens not separated by one space in {basename}:{i + 1}') + return False, util.error(f'Tokens not separated by one space in {basename}:{i + 1}') if num_empty != 0: - util.exit_with_error(f'Exactly one empty line expected in {basename}') + return False, util.error(f'Exactly one empty line expected in {basename}') + + return True, '' + + +def validate_tests(tests: List[str], cpus: int, type: str = 'input'): + """ + Validate all tests in parallel. + """ + if not tests: + return + print(f'Validating {type} test contents.') + num_tests = len(tests) + finished = 0 + with mp.Pool(cpus) as pool: + for valid, message in pool.imap(validate_test, tests): + if not valid: + util.exit_with_error(message) + finished += 1 + print(f'Validated {finished}/{num_tests} tests', end='\r') + print() + print(util.info(f'All {type} tests are valid!')) + + +def get_all_inputs(task_id): + in_test_re = get_in_tests_re(task_id) + inputs = [] + for file in glob.glob(os.path.join(os.getcwd(), "in", "*.in")): + if in_test_re.match(os.path.basename(file)): + inputs.append(file) + return inputs diff --git a/src/sinol_make/helpers/parsers.py b/src/sinol_make/helpers/parsers.py index 83f1ae74..00ec840a 100644 --- a/src/sinol_make/helpers/parsers.py +++ b/src/sinol_make/helpers/parsers.py @@ -1,7 +1,7 @@ import sys - import argparse +from sinol_make import util from sinol_make.helpers import compiler @@ -20,8 +20,9 @@ def add_compilation_arguments(parser: argparse.ArgumentParser): parser.add_argument('--python-interpreter-path', dest='python_interpreter_path', type=str, default=compiler.get_python_interpreter_path(), help='Python interpreter to use (default: python3)') - parser.add_argument('--java-compiler-path', dest='java_compiler_path', type=str, - default=compiler.get_java_compiler_path(), help='Java compiler to use (default: javac)') + # Java is not currently supported by sinol-make + # parser.add_argument('--java-compiler-path', dest='java_compiler_path', type=str, + # default=compiler.get_java_compiler_path(), help='Java compiler to use (default: javac)') parser.add_argument('--compile-mode', '-C', dest='compile_mode', choices=['default', 'oioioi', 'weak', 'd', 'o', 'w'], help='Warning flag groups used to compile C/C++ files. Available options:\n' ' default / d - uses default flags: \n' @@ -29,3 +30,16 @@ def add_compilation_arguments(parser: argparse.ArgumentParser): ' oioioi / o - uses the same flags as oioioi:\n' ' (-Wall -Wno-unused-result -Werror)' ' weak / w - disable all warning flags during C and C++ compilation', default='default') + + +def add_cpus_argument(parser: argparse.ArgumentParser, help: str): + parser.add_argument('-c', '--cpus', type=int, + help=f'{help} ' + f'(default: {util.default_cpu_count()})', + default=util.default_cpu_count()) + + +def add_fsanitize_argument(parser: argparse.ArgumentParser): + parser.add_argument('-f', '--fsanitize', default=False, action='store_true', + help='Use -fsanitize=address,undefined for compilation. Warning: this may fail on some ' + 'systems. Tof fix this, run `sudo sysctl vm.mmap_rnd_bits = 28`.') diff --git a/src/sinol_make/structs/cache_structs.py b/src/sinol_make/structs/cache_structs.py index 5d1f3933..249bbdc8 100644 --- a/src/sinol_make/structs/cache_structs.py +++ b/src/sinol_make/structs/cache_structs.py @@ -43,20 +43,28 @@ class CacheFile: md5sum: str # Path to the executable executable_path: str + # Compilation flags used + compilation_flags: str + # Whether -fsanitize=undefined,address was used + sanitizers: bool # Test results tests: Dict[str, CacheTest] - def __init__(self, md5sum="", executable_path="", tests=None): + def __init__(self, md5sum="", executable_path="", compilation_flags="default", sanitizers=False, tests=None): if tests is None: tests = {} self.md5sum = md5sum self.executable_path = executable_path + self.compilation_flags = compilation_flags + self.sanitizers = sanitizers self.tests = tests def to_dict(self) -> Dict: return { "md5sum": self.md5sum, "executable_path": self.executable_path, + "compilation_flags": self.compilation_flags, + "sanitizers": self.sanitizers, "tests": {k: v.to_dict() for k, v in self.tests.items()} } @@ -65,6 +73,8 @@ def from_dict(dict) -> 'CacheFile': return CacheFile( md5sum=dict.get("md5sum", ""), executable_path=dict.get("executable_path", ""), + compilation_flags=dict.get("compilation_flags", "default"), + sanitizers=dict.get("sanitizers", False), tests={k: CacheTest( time_limit=v["time_limit"], memory_limit=v["memory_limit"], diff --git a/src/sinol_make/util.py b/src/sinol_make/util.py index 656efe9b..17829e5e 100644 --- a/src/sinol_make/util.py +++ b/src/sinol_make/util.py @@ -1,28 +1,24 @@ import glob, importlib, os, sys, requests, yaml import math -import multiprocessing import platform import tarfile import hashlib import multiprocessing import resource from typing import Union +from packaging.version import parse as parse_version from sinol_make.contest_types import get_contest_type from sinol_make.helpers import paths, cache +from sinol_make.helpers.func_cache import cache_result from sinol_make.structs.status_structs import Status -__cache = {} - - +@cache_result() def get_commands(): """ Function to get an array of all available commands. """ - global __cache - if 'commands' in __cache: - return __cache['commands'] commands_path = glob.glob( os.path.join( os.path.dirname(os.path.realpath(__file__)), @@ -34,7 +30,6 @@ def get_commands(): temp = importlib.import_module('sinol_make.commands.' + os.path.basename(path), 'Command') commands.append(temp.Command()) - __cache['commands'] = commands return commands @@ -151,10 +146,18 @@ def import_importlib_resources(): return importlib -def check_for_updates(current_version) -> Union[str, None]: +def is_dev(version): + """ + Function to check if the version is a development version. + """ + return parse_version(version).is_devrelease + + +def check_for_updates(current_version, check=True) -> Union[str, None]: """ Function to check if there is a new version of sinol-make. :param current_version: current version of sinol-make + :param check: whether to check for new version :return: returns new version if there is one, None otherwise """ importlib = import_importlib_resources() @@ -165,8 +168,9 @@ def check_for_updates(current_version) -> Union[str, None]: # We check for new version asynchronously, so that it doesn't slow down the program. # If the main process exits, the check_version process will also exit. - process = multiprocessing.Process(target=check_version, daemon=True) - process.start() + if check: + process = multiprocessing.Process(target=check_version, daemon=True) + process.start() version_file = data_dir.joinpath("version") try: @@ -179,7 +183,9 @@ def check_for_updates(current_version) -> Union[str, None]: return None try: - if compare_versions(current_version, version) == -1: + if not is_dev(version) and parse_version(version) > parse_version(current_version): + return version + if is_dev(current_version) and is_dev(version) and parse_version(version) > parse_version(current_version): return version else: return None @@ -203,7 +209,9 @@ def check_version(): return data = request.json() - latest_version = data["info"]["version"] + versions = list(data["releases"].keys()) + versions.sort(key=parse_version) + latest_version = versions[-1] version_file = importlib.files("sinol_make").joinpath("data/version") try: @@ -218,26 +226,6 @@ def check_version(): pass -def compare_versions(version_a, version_b): - """ - Function to compare two versions. - Returns 1 if version_a > version_b, 0 if version_a == version_b, -1 if version_a < version_b. - """ - - def convert(version): - return tuple(map(int, version.split("."))) - - version_a = convert(version_a) - version_b = convert(version_b) - - if version_a > version_b: - return 1 - elif version_a == version_b: - return 0 - else: - return -1 - - def lines_diff(lines1, lines2): """ Function to compare two lists of lines. @@ -442,3 +430,7 @@ def exit_with_error(text, func=None): except TypeError: pass exit(1) + + +def has_sanitizer_error(output, exit_code): + return ('ELF_ET_DYN_BASE' in output or 'ASan' in output) and exit_code != 0 diff --git a/tests/commands/export/test_integration.py b/tests/commands/export/test_integration.py index a7a73647..1cc5196a 100644 --- a/tests/commands/export/test_integration.py +++ b/tests/commands/export/test_integration.py @@ -212,3 +212,50 @@ def test_no_ocen(create_package): with tarfile.open(f'{task_id}.tgz', "r") as tar: sinol_util.extract_tar(tar, tmpdir) assert not os.path.exists(os.path.join(tmpdir, task_id, "attachments", f"{task_id}ocen.zip")) + + +@pytest.mark.parametrize("create_package", [util.get_dlazaw_package()], indirect=True) +def test_no_ocen_and_dlazaw(create_package): + """ + Test if ocen archive is not created when there are no ocen tests. + Also test if dlazaw archive is created. + """ + parser = configure_parsers() + args = parser.parse_args(["export", "--no-statement"]) + command = Command() + command.run(args) + task_id = package_util.get_task_id() + + with tempfile.TemporaryDirectory() as tmpdir: + with tarfile.open(f'{task_id}.tgz', "r") as tar: + sinol_util.extract_tar(tar, tmpdir) + + assert not os.path.exists(os.path.join(tmpdir, task_id, "attachments", f"{task_id}ocen.zip")) + dlazaw_archive = os.path.join(tmpdir, task_id, "attachments", f"dlazaw.zip") + assert os.path.exists(dlazaw_archive) + + with zipfile.ZipFile(dlazaw_archive, "r") as zip: + zip.extractall(os.path.join(tmpdir)) + + assert os.path.exists(os.path.join(tmpdir, "dlazaw")) + assert os.path.exists(os.path.join(tmpdir, "dlazaw", "epic_file")) + + +@pytest.mark.parametrize("create_package", [util.get_ocen_package_path()], indirect=True) +def test_dlazaw_ocen(create_package): + """ + Test if ocen archive isn't created when dlazaw directory exists + """ + os.makedirs("dlazaw") + parser = configure_parsers() + args = parser.parse_args(["export", "--no-statement"]) + command = Command() + command.run(args) + task_id = package_util.get_task_id() + + with tempfile.TemporaryDirectory() as tmpdir: + with tarfile.open(f'{task_id}.tgz', "r") as tar: + sinol_util.extract_tar(tar, tmpdir) + + assert not os.path.exists(os.path.join(tmpdir, task_id, "attachments", f"{task_id}ocen.zip")) + assert os.path.join(tmpdir, task_id, "attachments", f"dlazaw.zip") diff --git a/tests/commands/gen/test_integration.py b/tests/commands/gen/test_integration.py index b5844e4b..53fc3208 100644 --- a/tests/commands/gen/test_integration.py +++ b/tests/commands/gen/test_integration.py @@ -60,7 +60,7 @@ def test_correct_inputs(capsys, create_package): """ task_id = package_util.get_task_id() correct_solution = package_util.get_correct_solution(task_id) - cache.save_compiled(correct_solution, "exe") + cache.save_compiled(correct_solution, "exe", "default", False) simple_run() md5_sums = get_md5_sums(create_package) @@ -79,7 +79,7 @@ def test_changed_inputs(capsys, create_package): """ task_id = package_util.get_task_id() correct_solution = package_util.get_correct_solution(task_id) - cache.save_compiled(correct_solution, "exe") + cache.save_compiled(correct_solution, "exe", "default", False) simple_run() md5_sums = get_md5_sums(create_package) correct_md5 = md5_sums.copy() @@ -245,7 +245,7 @@ def test_fsanitize(create_package): pytest.skip("-fsanitize=address,undefined is not supported on Apple Silicon") for ingen in ["prog/geningen3.cpp", "prog/geningen4.cpp"]: with pytest.raises(SystemExit) as e: - simple_run([ingen]) + simple_run(["--fsanitize", ingen]) assert e.type == SystemExit assert e.value.code == 1 @@ -360,3 +360,36 @@ def test_outgen_cache_cleaning(create_package, capsys): simple_run(command="outgen") # Run should pass, because output file was regenerated and cache for this test was cleaned. RunCommand().run(args) + + +@pytest.mark.parametrize("create_package", [util.get_simple_package_path()], indirect=True) +def test_cache_remove_after_flags_change(create_package): + """ + Test if cache for a program is removed if compilation flags change or -fsanitize is disabled. + """ + def random_key_to_cache(): + cache_file = cache.get_cache_file("abcingen.cpp") + print(cache_file) + cache_dict = cache_file.to_dict() + cache_dict["random_key"] = "random_value" + with open(paths.get_cache_path("md5sums", "abcingen.cpp"), "w") as f: + yaml.dump(cache_dict, f) + + def check_assert(): + with open(paths.get_cache_path("md5sums", "abcingen.cpp"), "r") as f: + cache_dict = yaml.load(f, Loader=yaml.FullLoader) + assert "random_key" not in cache_dict + + # Generate cache + simple_run(command="gen") + random_key_to_cache() + simple_run(["--compile-mode", "oioioi"], command="gen") + check_assert() + + if sm_util.is_macos_arm(): # -fsanitize=address,undefined is not supported on Apple Silicon + return + # Generate cache + simple_run(command="gen") + random_key_to_cache() + simple_run(["--fsanitize"], command="gen") + check_assert() diff --git a/tests/commands/gen/test_unit.py b/tests/commands/gen/test_unit.py index 662ebeb4..16ab4736 100644 --- a/tests/commands/gen/test_unit.py +++ b/tests/commands/gen/test_unit.py @@ -145,9 +145,6 @@ def test_validate_tests(create_package, capsys): ] for test, error in tests: - with pytest.raises(SystemExit) as e: - package_util.validate_test(os.path.join(package_path, "in", test)) - assert e.type == SystemExit - assert e.value.code == 1 - captured = capsys.readouterr() - assert error in captured.out, f"Expected error not found in output: {captured.out}" + valid, msg = package_util.validate_test(os.path.join(package_path, "in", test)) + assert not valid + assert error in msg diff --git a/tests/commands/inwer/test_integration.py b/tests/commands/inwer/test_integration.py index 9beb1297..3135003a 100644 --- a/tests/commands/inwer/test_integration.py +++ b/tests/commands/inwer/test_integration.py @@ -19,12 +19,7 @@ def test_default(capsys, create_package): parser = configure_parsers() args = parser.parse_args(["inwer"]) command = Command() - - with pytest.raises(SystemExit) as e: - command.run(args) - - assert e.type == SystemExit - assert e.value.code == 0 + command.run(args) out = capsys.readouterr().out assert "Verification successful." in out @@ -42,12 +37,7 @@ def test_specified_inwer(capsys, create_package): for inwer_path in ["prog/werinwer.cpp", "prog/werinwer7.cpp"]: args = parser.parse_args(["inwer", inwer_path]) command = Command() - - with pytest.raises(SystemExit) as e: - command.run(args) - - assert e.type == SystemExit - assert e.value.code == 0 + command.run(args) out = capsys.readouterr().out assert "Verification successful." in out @@ -98,12 +88,7 @@ def test_flag_tests(capsys, create_package): parser = configure_parsers() args = parser.parse_args(["inwer", "prog/werinwer.cpp", "--tests", "in/wer2a.in"]) command = Command() - - with pytest.raises(SystemExit) as e: - command.run(args) - - assert e.type == SystemExit - assert e.value.code == 0 + command.run(args) out = capsys.readouterr().out assert "Verification successful." in out @@ -137,10 +122,7 @@ def test_no_output(capsys, create_package): 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 + command.run(args) out = capsys.readouterr().out assert "No output" in out @@ -154,7 +136,7 @@ def test_fsanitize(create_package): pytest.skip("-fsanitize=address,undefined is not supported on Apple Silicon") for inwer in ["prog/werinwer5.cpp", "prog/werinwer6.cpp"]: parser = configure_parsers() - args = parser.parse_args(["inwer", inwer]) + args = parser.parse_args(["inwer", "--fsanitize", inwer]) command = Command() with pytest.raises(SystemExit) as e: command.run(args) diff --git a/tests/commands/run/test_integration.py b/tests/commands/run/test_integration.py index 6d47dab4..69b23b24 100644 --- a/tests/commands/run/test_integration.py +++ b/tests/commands/run/test_integration.py @@ -814,3 +814,30 @@ def test_ghost_checker(create_package): with pytest.raises(SystemExit) as e: command.run(args) assert e.value.code == 1 + + +@pytest.mark.parametrize("create_package", [get_simple_package_path()], indirect=True) +def test_ignore_expected_flag(create_package, capsys): + """ + Test flag --ignore-expected. + """ + config = package_util.get_config() + del config["sinol_expected_scores"] + util.save_config(config) + + package_path = create_package + create_ins_outs(package_path) + parser = configure_parsers() + args = parser.parse_args(["run"]) + command = Command() + + with pytest.raises(SystemExit): + command.run(args) + out = capsys.readouterr().out + assert "Use flag --apply-suggestions to apply suggestions." in out + + args = parser.parse_args(["run", "--ignore-expected"]) + command = Command() + command.run(args) + out = capsys.readouterr().out + assert "Ignoring expected scores." in out diff --git a/tests/commands/verify/__init__.py b/tests/commands/verify/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/commands/verify/test_integration.py b/tests/commands/verify/test_integration.py new file mode 100644 index 00000000..7a074fde --- /dev/null +++ b/tests/commands/verify/test_integration.py @@ -0,0 +1,138 @@ +import os +import pytest + +from sinol_make import configure_parsers +from sinol_make import util as sm_util +from sinol_make.commands.verify import Command +from sinol_make.helpers import package_util +from tests import util +from tests.fixtures import create_package + + +def run(args=None): + if args is None: + args = [] + parser = configure_parsers() + command = Command() + args = parser.parse_args(['verify'] + args) + command.run(args) + + +@pytest.mark.parametrize("create_package", [util.get_simple_package_path(), util.get_inwer_package_path()], + indirect=True) +def test_simple_package(capsys, create_package): + """ + Test if simple package runs successfully. + """ + run() + + +@pytest.mark.parametrize("create_package", [util.get_stresstest_package_path()], indirect=True) +def test_stresstest_package(capsys, create_package): + """ + Test if stresstest.sh script runs. Then check if after failing the stresstest.sh script, the verify command + will fail as well. + """ + run() + out = capsys.readouterr().out + assert "Running stress tests" in out + + stresstest_path = os.path.join(create_package, "prog", "strstresstest.sh") + with open(stresstest_path, "r") as f: + code = f.read() + with open(stresstest_path, "w") as f: + f.write(code.replace("exit 0", "exit 1")) + + with pytest.raises(SystemExit) as e: + run() + assert e.value.code == 1 + out = capsys.readouterr().out + assert "Running stress tests" in out + assert "Stress tests failed." in out + + +@pytest.mark.parametrize("create_package", [util.get_simple_package_path()], indirect=True) +def test_missing_extra_files(capsys, create_package): + """ + Test if missing extra files will cause the verify command to fail. + """ + config = package_util.get_config() + config["extra_compilation_files"] = ["missing_file"] + sm_util.save_config(config) + with pytest.raises(SystemExit) as e: + run() + assert e.value.code == 1 + out = capsys.readouterr().out + assert "Extra compilation file `missing_file` does not exist." in out + + del config["extra_compilation_files"] + config["extra_execution_files"] = {"cpp": ["missing_file"]} + sm_util.save_config(config) + with pytest.raises(SystemExit) as e: + run() + assert e.value.code == 1 + out = capsys.readouterr().out + assert "Extra execution file `missing_file` for language `cpp` does not exist." in out + + +@pytest.mark.parametrize("create_package", [util.get_simple_package_path()], indirect=True) +def test_invalid_scores(capsys, create_package): + """ + Test if invalid scores will cause the verify command to fail. + """ + config = package_util.get_config() + scores = config["scores"] + config["scores"] = {1: 100} + sm_util.save_config(config) + with pytest.raises(SystemExit) as e: + run() + assert e.value.code == 1 + out = capsys.readouterr().out + assert "Score for group '2' not found." in out + + config["scores"] = scores + config["scores"][20] = 0 + sm_util.save_config(config) + with pytest.raises(SystemExit) as e: + run() + assert e.value.code == 1 + out = capsys.readouterr().out + assert "Score for group '20' found in config" in out + + +@pytest.mark.parametrize("create_package", [util.get_simple_package_path()], indirect=True) +def test_scores_not_100(capsys, create_package): + """ + Test if scores not adding up to 100 will cause the verify command to fail. + """ + config = package_util.get_config() + config["sinol_contest_type"] = "oi" + config["scores"][1] -= 1 + sm_util.save_config(config) + with pytest.raises(SystemExit) as e: + run() + assert e.value.code == 1 + out = capsys.readouterr().out + assert "Total score in config is 99, but should be 100." in out + + +@pytest.mark.parametrize("create_package", [util.get_dlazaw_package()], indirect=True) +def test_expected_contest_and_no_scores(capsys, create_package): + """ + Test if --expected-contest-type flag works, + and if contest type is OI and there are no scores in config.yml, the verify command will fail. + """ + config = package_util.get_config() + with pytest.raises(SystemExit) as e: + run(["--expected-contest-type", "icpc"]) + assert e.value.code == 1 + out = capsys.readouterr().out + assert "Invalid contest type 'oi'. Expected 'icpc'." in out + + del config["scores"] + sm_util.save_config(config) + with pytest.raises(SystemExit) as e: + run(["--expected-contest-type", "oi"]) + assert e.value.code == 1 + out = capsys.readouterr().out + assert "Scores are not defined in config.yml." in out diff --git a/tests/helpers/test_cache.py b/tests/helpers/test_cache.py index be2423c7..52c37c77 100644 --- a/tests/helpers/test_cache.py +++ b/tests/helpers/test_cache.py @@ -13,25 +13,33 @@ def test_compilation_caching(): program = os.path.join(tmpdir, 'program.cpp') open(program, 'w').write('int main() { return 0; }') - assert cache.check_compiled(program) is None + assert cache.check_compiled(program, "default", False) is None assert compile.compile(program, os.path.join(tmpdir, 'program'), compile_log=None) - exe_path = cache.check_compiled(program) + exe_path = cache.check_compiled(program, "default", False) assert exe_path is not None assert compile.compile(program, os.path.join(tmpdir, 'program'), compile_log=None) - exe_path2 = cache.check_compiled(program) + exe_path2 = cache.check_compiled(program, "default", False) assert exe_path2 == exe_path open(program, 'w').write('int main() { return 1; }') - assert cache.check_compiled(program) is None + assert cache.check_compiled(program, "default", False) is None assert compile.compile(program, os.path.join(tmpdir, 'program'), compile_log=None) - assert cache.check_compiled(program) is not None + assert cache.check_compiled(program, "default", False) is not None open(program, 'w').write('int main() { return 0; }') - assert cache.check_compiled(program) is None + assert cache.check_compiled(program, "default", False) is None assert compile.compile(program, os.path.join(tmpdir, 'program'), compile_log=None) - assert cache.check_compiled(program) is not None + assert cache.check_compiled(program, "default", False) is not None + + assert cache.check_compiled(program, "default", True) is None + cache.save_compiled(program, exe_path, "default", True) + assert cache.check_compiled(program, "default", True) is not None + + assert cache.check_compiled(program, "oioioi", True) is None + cache.save_compiled(program, exe_path, "oioioi", True) + assert cache.check_compiled(program, "oioioi", True) is not None def test_cache(): @@ -72,7 +80,8 @@ def test_cache(): f.write("int main() { return 0; }") cache_file.save("abc.cpp") assert cache.get_cache_file("abc.cpp") == cache_file - cache.save_compiled("abc.cpp", "abc.e", is_checker=True) + cache.save_compiled("abc.cpp", "abc.e", "default", False, + is_checker=True) assert cache.get_cache_file("abc.cpp").tests == {} # Test that cache is cleared when extra compilation files change diff --git a/tests/helpers/test_compile.py b/tests/helpers/test_compile.py index 4447d93c..06703496 100644 --- a/tests/helpers/test_compile.py +++ b/tests/helpers/test_compile.py @@ -16,10 +16,11 @@ def test_compilation_caching(): with open(os.path.join(os.getcwd(), "test.e"), "w") as f: f.write("") - assert check_compiled(os.path.join(os.getcwd(), "test.txt")) is None + assert check_compiled(os.path.join(os.getcwd(), "test.txt"), "default", False) is None save_compiled(os.path.join(os.getcwd(), "test.txt"), - os.path.join(os.getcwd(), "test.e")) - assert check_compiled(os.path.join(os.getcwd(), "test.txt")) == os.path.join(os.getcwd(), "test.e") + os.path.join(os.getcwd(), "test.e"), "default", False) + assert check_compiled(os.path.join(os.getcwd(), "test.txt"), "default", False) == \ + os.path.join(os.getcwd(), "test.e") @pytest.mark.parametrize("create_package", [util.get_shell_ingen_pack_path()], indirect=True) diff --git a/tests/helpers/test_package_util.py b/tests/helpers/test_package_util.py index d821eb17..3d3538ce 100644 --- a/tests/helpers/test_package_util.py +++ b/tests/helpers/test_package_util.py @@ -3,7 +3,7 @@ from ..commands.run.util import create_ins from ..fixtures import * from tests import util -from sinol_make.helpers import package_util +from sinol_make.helpers import package_util, func_cache @pytest.mark.parametrize("create_package", [util.get_long_name_package_path()], indirect=True) @@ -12,6 +12,7 @@ def test_get_task_id(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") + func_cache.clear_cache() with pytest.raises(SystemExit): package_util.get_task_id() diff --git a/tests/packages/dlazaw/config.yml b/tests/packages/dlazaw/config.yml new file mode 100644 index 00000000..b13b19e9 --- /dev/null +++ b/tests/packages/dlazaw/config.yml @@ -0,0 +1,8 @@ +title: Package with no ocen files and dlazaw dir +sinol_task_id: dla +sinol_contest_type: oi +time_limit: 1000 +memory_limit: 10240 + +scores: + 1: 100 diff --git a/tests/packages/dlazaw/dlazaw/epic_file b/tests/packages/dlazaw/dlazaw/epic_file new file mode 100644 index 00000000..f81fce04 --- /dev/null +++ b/tests/packages/dlazaw/dlazaw/epic_file @@ -0,0 +1 @@ +this is a file diff --git a/tests/packages/dlazaw/in/.gitkeep b/tests/packages/dlazaw/in/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/dlazaw/out/.gitkeep b/tests/packages/dlazaw/out/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/dlazaw/prog/dla.cpp b/tests/packages/dlazaw/prog/dla.cpp new file mode 100644 index 00000000..1e57f863 --- /dev/null +++ b/tests/packages/dlazaw/prog/dla.cpp @@ -0,0 +1,9 @@ +#include + +using namespace std; + +int main() { + int a, b; + cin >> a >> b; + cout << a + b << "\n"; +} diff --git a/tests/packages/dlazaw/prog/dlaingen.cpp b/tests/packages/dlazaw/prog/dlaingen.cpp new file mode 100644 index 00000000..ecc40f72 --- /dev/null +++ b/tests/packages/dlazaw/prog/dlaingen.cpp @@ -0,0 +1,9 @@ +#include + +using namespace std; + +int main() { + ofstream f("dla1a.in"); + f << "1 1\n"; + f.close(); +} diff --git a/tests/packages/stresstest/config.yml b/tests/packages/stresstest/config.yml new file mode 100644 index 00000000..2da7a23e --- /dev/null +++ b/tests/packages/stresstest/config.yml @@ -0,0 +1,11 @@ +title: Package with stresstest.sh (for `verify` command) +sinol_task_id: str +time_limit: 1000 +memory_limit: 10240 + +sinol_expected_scores: + str.cpp: + expected: + 1: {points: 50, status: OK} + 2: {points: 50, status: OK} + points: 100 diff --git a/tests/packages/stresstest/in/.gitkeep b/tests/packages/stresstest/in/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/stresstest/out/.gitkeep b/tests/packages/stresstest/out/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/stresstest/prog/str.cpp b/tests/packages/stresstest/prog/str.cpp new file mode 100644 index 00000000..1e57f863 --- /dev/null +++ b/tests/packages/stresstest/prog/str.cpp @@ -0,0 +1,9 @@ +#include + +using namespace std; + +int main() { + int a, b; + cin >> a >> b; + cout << a + b << "\n"; +} diff --git a/tests/packages/stresstest/prog/stringen.cpp b/tests/packages/stresstest/prog/stringen.cpp new file mode 100644 index 00000000..e947e92a --- /dev/null +++ b/tests/packages/stresstest/prog/stringen.cpp @@ -0,0 +1,12 @@ +#include + +using namespace std; + +int main() { + ofstream f("str1a.in"); + f << "1 3\n"; + f.close(); + f.open("str2a.in"); + f << "2 5\n"; + f.close(); +} diff --git a/tests/packages/stresstest/prog/strstresstest.sh b/tests/packages/stresstest/prog/strstresstest.sh new file mode 100644 index 00000000..de5d8729 --- /dev/null +++ b/tests/packages/stresstest/prog/strstresstest.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo Very hard stress test +exit 0 diff --git a/tests/packages/wer/config.yml b/tests/packages/wer/config.yml index 388f9a4d..c5b12807 100644 --- a/tests/packages/wer/config.yml +++ b/tests/packages/wer/config.yml @@ -6,3 +6,11 @@ scores: 1: 33 2: 33 3: 34 + +sinol_expected_scores: + wer.cpp: + expected: + 1: {points: 33, status: OK} + 2: {points: 33, status: OK} + 3: {points: 34, status: OK} + points: 100 diff --git a/tests/packages/wer/out/.gitkeep b/tests/packages/wer/out/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/wer/prog/wer.cpp b/tests/packages/wer/prog/wer.cpp index 8c8f29dd..ac799965 100644 --- a/tests/packages/wer/prog/wer.cpp +++ b/tests/packages/wer/prog/wer.cpp @@ -11,5 +11,5 @@ int main() { s += a; } s *= n; - cout << s; + cout << s << "\n"; } diff --git a/tests/test_multiple_arguments.py b/tests/test_multiple_arguments.py index 4a508c70..4607a318 100644 --- a/tests/test_multiple_arguments.py +++ b/tests/test_multiple_arguments.py @@ -21,7 +21,7 @@ def test_simple_package(create_package): @pytest.mark.parametrize("create_package", [util.get_inwer_package_path()], indirect=True) def test_inwer_package(create_package): - run("sinol-make ingen inwer run") + run("sinol-make gen inwer run") run("sinol-make ingen prog/weringen.cpp inwer prog/werinwer.cpp --tests wer1a.in run --tests wer2a.in") run("sinol-make ingen inwer run export --no-statement") diff --git a/tests/test_util.py b/tests/test_util.py index e95f69bf..3d259fab 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -44,28 +44,6 @@ def test_file_diff(): assert util.file_diff(a_file, b_file) is False -def test_compare_versions(): - """ - Tests for compare_versions function - """ - - assert util.compare_versions('1.0.0', '1.0.0') == 0 - assert util.compare_versions('1.0.0', '1.0.1') == -1 - assert util.compare_versions('1.0.1', '1.0.0') == 1 - assert util.compare_versions('1.0.0', '1.1.0') == -1 - assert util.compare_versions('1.1.0', '1.0.0') == 1 - assert util.compare_versions('1.0.0', '2.0.0') == -1 - assert util.compare_versions('2.0.0', '1.0.0') == 1 - with pytest.raises(ValueError): - util.compare_versions('1.0.0', '') - with pytest.raises(ValueError): - util.compare_versions('', '1.0.0') - with pytest.raises(ValueError): - util.compare_versions('1.0.0', 'abc') - with pytest.raises(ValueError): - util.compare_versions('abc', '1.0.0') - - @requests_mock.Mocker(kw="mocker") def test_check_version(**kwargs): """ @@ -74,6 +52,32 @@ def test_check_version(**kwargs): """ mocker = kwargs["mocker"] + def create_response(version): + return {"releases": {version: "something"}} + + mocker.get("https://pypi.python.org/pypi/sinol-make/json", json=create_response("1.0.0.dev2")) + util.check_version() + version = util.check_for_updates("1.0.0.dev1", False) + assert version == "1.0.0.dev2" + assert util.is_dev(version) + + mocker.get("https://pypi.python.org/pypi/sinol-make/json", json=create_response("1.0.0")) + util.check_version() + version = util.check_for_updates("1.0.0.dev1", False) + assert version == "1.0.0" + assert not util.is_dev(version) + + mocker.get("https://pypi.python.org/pypi/sinol-make/json", json=create_response("2.0.0.dev1")) + util.check_version() + version = util.check_for_updates("1.0.0", False) + assert version is None + + mocker.get("https://pypi.python.org/pypi/sinol-make/json", json=create_response("1.0.1")) + util.check_version() + version = util.check_for_updates("1.0.0", False) + assert version == "1.0.1" + assert not util.is_dev(version) + importlib = util.import_importlib_resources() data_dir = importlib.files('sinol_make').joinpath("data") @@ -85,7 +89,7 @@ def test_check_version(**kwargs): version_file.unlink() # Test correct request - mocker.get("https://pypi.python.org/pypi/sinol-make/json", json={"info": {"version": "1.0.0"}}) + mocker.get("https://pypi.python.org/pypi/sinol-make/json", json=create_response(("1.0.0"))) util.check_version() assert version_file.is_file() assert version_file.read_text() == "1.0.0" diff --git a/tests/util.py b/tests/util.py index c29d99c3..d4158735 100644 --- a/tests/util.py +++ b/tests/util.py @@ -164,6 +164,20 @@ def get_bad_tests_package_path(): return os.path.join(os.path.dirname(__file__), "packages", "bad_tests") +def get_dlazaw_package(): + """ + Get path to package with dlazaw dir and no ocen tests (/tests/packages/dlazaw) + """ + return os.path.join(os.path.dirname(__file__), "packages", "dlazaw") + + +def get_stresstest_package_path(): + """ + Get path to package with stresstest.sh (/tests/packages/stresstest) + """ + return os.path.join(os.path.dirname(__file__), "packages", "stresstest") + + def create_ins(package_path, task_id): """ Create .in files for package. @@ -202,6 +216,7 @@ def create_ins_outs(package_path): """ os.chdir(package_path) task_id = package_util.get_task_id() + print(task_id) create_ins(package_path, task_id) has_lib = package_util.any_files_matching_pattern(task_id, f"{task_id}lib.*") if not has_lib: From 1776869aab3491b675890ace9b12eb3ff6acc2eb Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sun, 21 Apr 2024 11:08:48 +0200 Subject: [PATCH 05/13] OIJ contest type (#237) * Add OIJ contest type * Add tests --- src/sinol_make/contest_types/__init__.py | 3 + src/sinol_make/contest_types/oij.py | 32 ++++++ tests/commands/export/test_integration.py | 119 +++++++++++++--------- tests/commands/verify/test_integration.py | 38 ++++--- 4 files changed, 127 insertions(+), 65 deletions(-) create mode 100644 src/sinol_make/contest_types/oij.py diff --git a/src/sinol_make/contest_types/__init__.py b/src/sinol_make/contest_types/__init__.py index 98887606..d1815492 100644 --- a/src/sinol_make/contest_types/__init__.py +++ b/src/sinol_make/contest_types/__init__.py @@ -1,6 +1,7 @@ from sinol_make.contest_types.default import DefaultContest from sinol_make.contest_types.icpc import ICPCContest from sinol_make.contest_types.oi import OIContest +from sinol_make.contest_types.oij import OIJContest from sinol_make.helpers.package_util import get_config from sinol_make.interfaces.Errors import UnknownContestType @@ -15,5 +16,7 @@ def get_contest_type(): return OIContest() elif contest_type == "icpc": return ICPCContest() + elif contest_type == "oij": + return OIJContest() else: raise UnknownContestType(f'Unknown contest type "{contest_type}"') diff --git a/src/sinol_make/contest_types/oij.py b/src/sinol_make/contest_types/oij.py new file mode 100644 index 00000000..3536b93b --- /dev/null +++ b/src/sinol_make/contest_types/oij.py @@ -0,0 +1,32 @@ +import argparse + +from sinol_make import util +from sinol_make.helpers import package_util +from sinol_make.contest_types.default import DefaultContest + + +class OIJContest(DefaultContest): + """ + Contest type for Polish Junior Olympiad in Informatics. + """ + + def get_type(self) -> str: + return "oij" + + def argument_overrides(self, args: argparse.Namespace) -> argparse.Namespace: + """ + Add arguments for features required by OIJ contest + """ + args.export_ocen = True + return args + + def verify_pre_gen(self): + """ + Verify if scores sum up to 100. + """ + config = package_util.get_config() + if 'scores' not in config: + util.exit_with_error("Scores are not defined in config.yml.") + total_score = sum(config['scores'].values()) + if total_score != 100: + util.exit_with_error(f"Total score in config is {total_score}, but should be 100.") diff --git a/tests/commands/export/test_integration.py b/tests/commands/export/test_integration.py index 1cc5196a..6f71f012 100644 --- a/tests/commands/export/test_integration.py +++ b/tests/commands/export/test_integration.py @@ -1,6 +1,7 @@ import yaml import stat import glob +import shutil import pytest import tarfile import zipfile @@ -9,6 +10,7 @@ from sinol_make import configure_parsers from sinol_make import util as sinol_util from sinol_make.commands.doc import Command as DocCommand +from sinol_make.helpers import paths from tests import util from tests.fixtures import create_package from .util import * @@ -43,6 +45,17 @@ def _test_archive(package_path, out, tar): assert glob.glob(os.path.join(extracted, "out", "*")) == [] +def _set_contest_type(contest_type): + config = package_util.get_config() + config["sinol_contest_type"] = contest_type + sinol_util.save_config(config) + task_id = package_util.get_task_id() + if os.path.exists(os.path.join(os.getcwd(), f'{task_id}.tgz')): + os.remove(f'{task_id}.tgz') + if os.path.exists(paths.get_cache_path()): + shutil.rmtree(paths.get_cache_path()) + + @pytest.mark.parametrize("create_package", [util.get_simple_package_path(), util.get_library_package_path(), util.get_library_string_args_package_path(), util.get_shell_ingen_pack_path(), util.get_handwritten_package_path()], @@ -166,35 +179,37 @@ def test_ocen_archive(create_package): """ Test creation of ocen archive. """ - parser = configure_parsers() - args = parser.parse_args(["export", "--no-statement"]) - command = Command() - command.run(args) - task_id = package_util.get_task_id() - in_handwritten = ["ocen0.in", "ocen0a.in", "ocen1a.in", "ocen1ocen.in"] - out_handwritten = ["ocen0.out"] - ocen_tests = ["ocen0", "ocen0a", "ocen0b", "ocen1ocen", "ocen2ocen"] + for contest_type in ["oi", "oij"]: + _set_contest_type(contest_type) + parser = configure_parsers() + args = parser.parse_args(["export", "--no-statement"]) + command = Command() + command.run(args) + task_id = package_util.get_task_id() + in_handwritten = ["ocen0.in", "ocen0a.in", "ocen1a.in", "ocen1ocen.in"] + out_handwritten = ["ocen0.out"] + ocen_tests = ["ocen0", "ocen0a", "ocen0b", "ocen1ocen", "ocen2ocen"] - with tempfile.TemporaryDirectory() as tmpdir: - package_path = os.path.join(tmpdir, task_id) - os.mkdir(package_path) - with tarfile.open(f'{task_id}.tgz', "r") as tar: - sinol_util.extract_tar(tar, tmpdir) + with tempfile.TemporaryDirectory() as tmpdir: + package_path = os.path.join(tmpdir, task_id) + os.mkdir(package_path) + with tarfile.open(f'{task_id}.tgz', "r") as tar: + sinol_util.extract_tar(tar, tmpdir) - for ext in ["in", "out"]: - tests = [os.path.basename(f) for f in glob.glob(os.path.join(package_path, ext, f'*.{ext}'))] - assert set(tests) == set(in_handwritten if ext == "in" else out_handwritten) + for ext in ["in", "out"]: + tests = [os.path.basename(f) for f in glob.glob(os.path.join(package_path, ext, f'*.{ext}'))] + assert set(tests) == set(in_handwritten if ext == "in" else out_handwritten) - ocen_archive = os.path.join(package_path, "attachments", f"{task_id}ocen.zip") - assert os.path.exists(ocen_archive) - ocen_dir = os.path.join(package_path, "ocen_dir") + ocen_archive = os.path.join(package_path, "attachments", f"{task_id}ocen.zip") + assert os.path.exists(ocen_archive) + ocen_dir = os.path.join(package_path, "ocen_dir") - with zipfile.ZipFile(ocen_archive, "r") as zip: - zip.extractall(ocen_dir) + with zipfile.ZipFile(ocen_archive, "r") as zip: + zip.extractall(ocen_dir) - for ext in ["in", "out"]: - tests = [os.path.basename(f) for f in glob.glob(os.path.join(ocen_dir, task_id, ext, f'*.{ext}'))] - assert set(tests) == set([f'{test}.{ext}' for test in ocen_tests]) + for ext in ["in", "out"]: + tests = [os.path.basename(f) for f in glob.glob(os.path.join(ocen_dir, task_id, ext, f'*.{ext}'))] + assert set(tests) == set([f'{test}.{ext}' for test in ocen_tests]) @pytest.mark.parametrize("create_package", [util.get_icpc_package_path()], indirect=True) @@ -220,25 +235,27 @@ def test_no_ocen_and_dlazaw(create_package): Test if ocen archive is not created when there are no ocen tests. Also test if dlazaw archive is created. """ - parser = configure_parsers() - args = parser.parse_args(["export", "--no-statement"]) - command = Command() - command.run(args) - task_id = package_util.get_task_id() + for contest_type in ["oi", "oij"]: + _set_contest_type(contest_type) + parser = configure_parsers() + args = parser.parse_args(["export", "--no-statement"]) + command = Command() + command.run(args) + task_id = package_util.get_task_id() - with tempfile.TemporaryDirectory() as tmpdir: - with tarfile.open(f'{task_id}.tgz', "r") as tar: - sinol_util.extract_tar(tar, tmpdir) + with tempfile.TemporaryDirectory() as tmpdir: + with tarfile.open(f'{task_id}.tgz', "r") as tar: + sinol_util.extract_tar(tar, tmpdir) - assert not os.path.exists(os.path.join(tmpdir, task_id, "attachments", f"{task_id}ocen.zip")) - dlazaw_archive = os.path.join(tmpdir, task_id, "attachments", f"dlazaw.zip") - assert os.path.exists(dlazaw_archive) + assert not os.path.exists(os.path.join(tmpdir, task_id, "attachments", f"{task_id}ocen.zip")) + dlazaw_archive = os.path.join(tmpdir, task_id, "attachments", f"dlazaw.zip") + assert os.path.exists(dlazaw_archive) - with zipfile.ZipFile(dlazaw_archive, "r") as zip: - zip.extractall(os.path.join(tmpdir)) + with zipfile.ZipFile(dlazaw_archive, "r") as zip: + zip.extractall(os.path.join(tmpdir)) - assert os.path.exists(os.path.join(tmpdir, "dlazaw")) - assert os.path.exists(os.path.join(tmpdir, "dlazaw", "epic_file")) + assert os.path.exists(os.path.join(tmpdir, "dlazaw")) + assert os.path.exists(os.path.join(tmpdir, "dlazaw", "epic_file")) @pytest.mark.parametrize("create_package", [util.get_ocen_package_path()], indirect=True) @@ -246,16 +263,18 @@ def test_dlazaw_ocen(create_package): """ Test if ocen archive isn't created when dlazaw directory exists """ - os.makedirs("dlazaw") - parser = configure_parsers() - args = parser.parse_args(["export", "--no-statement"]) - command = Command() - command.run(args) - task_id = package_util.get_task_id() + for contest_type in ["oi", "oij"]: + _set_contest_type(contest_type) + os.makedirs("dlazaw", exist_ok=True) + parser = configure_parsers() + args = parser.parse_args(["export", "--no-statement"]) + command = Command() + command.run(args) + task_id = package_util.get_task_id() - with tempfile.TemporaryDirectory() as tmpdir: - with tarfile.open(f'{task_id}.tgz', "r") as tar: - sinol_util.extract_tar(tar, tmpdir) + with tempfile.TemporaryDirectory() as tmpdir: + with tarfile.open(f'{task_id}.tgz', "r") as tar: + sinol_util.extract_tar(tar, tmpdir) - assert not os.path.exists(os.path.join(tmpdir, task_id, "attachments", f"{task_id}ocen.zip")) - assert os.path.join(tmpdir, task_id, "attachments", f"dlazaw.zip") + assert not os.path.exists(os.path.join(tmpdir, task_id, "attachments", f"{task_id}ocen.zip")) + assert os.path.join(tmpdir, task_id, "attachments", f"dlazaw.zip") diff --git a/tests/commands/verify/test_integration.py b/tests/commands/verify/test_integration.py index 7a074fde..0c23d4f9 100644 --- a/tests/commands/verify/test_integration.py +++ b/tests/commands/verify/test_integration.py @@ -1,10 +1,11 @@ import os +import shutil import pytest from sinol_make import configure_parsers from sinol_make import util as sm_util from sinol_make.commands.verify import Command -from sinol_make.helpers import package_util +from sinol_make.helpers import package_util, paths from tests import util from tests.fixtures import create_package @@ -103,24 +104,29 @@ def test_invalid_scores(capsys, create_package): @pytest.mark.parametrize("create_package", [util.get_simple_package_path()], indirect=True) def test_scores_not_100(capsys, create_package): """ - Test if scores not adding up to 100 will cause the verify command to fail. + Test if scores not adding up to 100 will cause the verify command to fail if contest type is OI or OIJ. """ config = package_util.get_config() - config["sinol_contest_type"] = "oi" config["scores"][1] -= 1 sm_util.save_config(config) - with pytest.raises(SystemExit) as e: - run() - assert e.value.code == 1 - out = capsys.readouterr().out - assert "Total score in config is 99, but should be 100." in out + for contest_type in ["oi", "oij"]: + if os.path.exists(paths.get_cache_path()): + shutil.rmtree(paths.get_cache_path()) + config = package_util.get_config() + config["sinol_contest_type"] = contest_type + sm_util.save_config(config) + with pytest.raises(SystemExit) as e: + run() + assert e.value.code == 1 + out = capsys.readouterr().out + assert "Total score in config is 99, but should be 100." in out @pytest.mark.parametrize("create_package", [util.get_dlazaw_package()], indirect=True) def test_expected_contest_and_no_scores(capsys, create_package): """ Test if --expected-contest-type flag works, - and if contest type is OI and there are no scores in config.yml, the verify command will fail. + and if contest type is OI or OIJ and there are no scores in config.yml, the verify command will fail. """ config = package_util.get_config() with pytest.raises(SystemExit) as e: @@ -130,9 +136,11 @@ def test_expected_contest_and_no_scores(capsys, create_package): assert "Invalid contest type 'oi'. Expected 'icpc'." in out del config["scores"] - sm_util.save_config(config) - with pytest.raises(SystemExit) as e: - run(["--expected-contest-type", "oi"]) - assert e.value.code == 1 - out = capsys.readouterr().out - assert "Scores are not defined in config.yml." in out + for contest_type in ["oi", "oij"]: + config["sinol_contest_type"] = contest_type + sm_util.save_config(config) + with pytest.raises(SystemExit) as e: + run(["--expected-contest-type", contest_type]) + assert e.value.code == 1 + out = capsys.readouterr().out + assert "Scores are not defined in config.yml." in out From 60fcd9c0d7406a5c7285dff936b046f63e54e394 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sun, 21 Apr 2024 11:08:56 +0200 Subject: [PATCH 06/13] Fix conftest on non linux machines (#238) --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6405706d..8882f8a2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -111,6 +111,6 @@ def pytest_collection_modifyitems(config, items: List[pytest.Item]): for item in items: if "oiejq" in item.keywords: - if sys.platform != "linux" or config.getoption("--time-tool") == ["time"] or \ + if not util.is_linux() or config.getoption("--time-tool") == ["time"] or \ config.getoption("--github-runner"): item.add_marker(pytest.mark.skip(reason="oiejq required")) From 9f77650d68b0f998137ce3ebc6cbebce70f12d55 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Wed, 1 May 2024 19:46:59 +0200 Subject: [PATCH 07/13] Bump python versions in tests (#240) --- .github/workflows/Arch.yaml | 2 +- .github/workflows/GithubRunner.yaml | 2 +- .github/workflows/Ubuntu.yaml | 2 +- .github/workflows/macOS.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/Arch.yaml b/.github/workflows/Arch.yaml index 832bfd01..cea57a4d 100644 --- a/.github/workflows/Arch.yaml +++ b/.github/workflows/Arch.yaml @@ -9,7 +9,7 @@ jobs: runs-on: self-hosted strategy: matrix: - python-version: ["3.7.17", "3.12.0"] + python-version: ["3.8", "3.12"] name: pytest-arch-python-${{ matrix.python-version }} container: image: archlinux:latest diff --git a/.github/workflows/GithubRunner.yaml b/.github/workflows/GithubRunner.yaml index e8bdd071..53015cea 100644 --- a/.github/workflows/GithubRunner.yaml +++ b/.github/workflows/GithubRunner.yaml @@ -8,7 +8,7 @@ jobs: pytest: strategy: matrix: - python-version: ["3.7.17", "3.12.0"] + python-version: ["3.8", "3.12"] name: pytest-github-runner-python-${{ matrix.python-version }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/Ubuntu.yaml b/.github/workflows/Ubuntu.yaml index a6c364d7..bab3026b 100644 --- a/.github/workflows/Ubuntu.yaml +++ b/.github/workflows/Ubuntu.yaml @@ -9,7 +9,7 @@ jobs: runs-on: self-hosted strategy: matrix: - python-version: ["3.7.17", "3.12.0"] + python-version: ["3.8", "3.12"] name: pytest-ubuntu-python-${{ matrix.python-version }} container: image: ubuntu:latest diff --git a/.github/workflows/macOS.yaml b/.github/workflows/macOS.yaml index 66876d00..457df818 100644 --- a/.github/workflows/macOS.yaml +++ b/.github/workflows/macOS.yaml @@ -10,7 +10,7 @@ jobs: runs-on: macos-latest strategy: matrix: - python-version: ["3.7", "3.12"] + python-version: ["3.10", "3.12"] name: pytest-macos-python-${{ matrix.python-version }} steps: - name: Checkout From 4f30e3fac948b923b4a800c43c2690854ae57fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Putra?= Date: Mon, 27 May 2024 14:37:04 +0200 Subject: [PATCH 08/13] Fix typo in README.md (#241) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cb2d2e9e..4f47d199 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ activate-global-python-argcomplete ### Usage -The availabe commands (see `sinol-make --help`) are: +The available commands (see `sinol-make --help`) are: - `sinol-make run` -- Runs selected solutions (by default all solutions) on selected tests (by default all tests) with a given number of CPUs. Measures the solutions' time with oiejq, unless specified otherwise. After running the solutions, it From d93ae39a61bab835385138b5c59c23581ccba962 Mon Sep 17 00:00:00 2001 From: Olaf Targowski Date: Sat, 20 Jul 2024 20:40:45 +0200 Subject: [PATCH 09/13] Switch to a non-upstream autoupdate cache path --- src/sinol_make/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sinol_make/util.py b/src/sinol_make/util.py index 9e5a8003..771de73b 100644 --- a/src/sinol_make/util.py +++ b/src/sinol_make/util.py @@ -177,7 +177,7 @@ def check_for_updates(current_version, check=True) -> Union[str, None]: version = version_file.read_text() except (PermissionError, FileNotFoundError): try: - with open(paths.get_cache_path("sinol_make_version"), "r") as f: + with open(paths.get_cache_path("st_make_version"), "r") as f: version = f.read() except (FileNotFoundError, PermissionError): return None @@ -220,7 +220,7 @@ def check_version(): if find_and_chdir_package(): try: os.makedirs(paths.get_cache_path(), exist_ok=True) - with open(paths.get_cache_path("sinol_make_version"), "w") as f: + with open(paths.get_cache_path("st_make_version"), "w") as f: f.write(latest_version) except PermissionError: pass From 011b0a63844fdc10e3e2932b1206d104956f97cd Mon Sep 17 00:00:00 2001 From: Olaf Targowski Date: Sat, 20 Jul 2024 21:00:58 +0200 Subject: [PATCH 10/13] Change sinol-make to st-make in more places --- example_package/README.md | 2 +- example_package/config.yml | 8 ++++---- src/sinol_make/__init__.py | 10 +++++----- src/sinol_make/commands/verify/__init__.py | 2 +- src/sinol_make/helpers/compiler.py | 2 +- src/sinol_make/helpers/parsers.py | 2 +- tests/test_util.py | 8 ++++---- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/example_package/README.md b/example_package/README.md index c6778c50..421a6baf 100644 --- a/example_package/README.md +++ b/example_package/README.md @@ -449,7 +449,7 @@ an archive with the proper name, which sio2 uses as the task id. sinol_contest_type: talent ``` -sinol-make can behave differently depending on the value of `sinol_contest_type` key. +st-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 (in st-make) `talent` is used. In sinol-make (OI version) is used 'default'. diff --git a/example_package/config.yml b/example_package/config.yml index a1f8c0e0..696d81b7 100644 --- a/example_package/config.yml +++ b/example_package/config.yml @@ -90,14 +90,14 @@ scores: # sinol_contest_type: talent -# sinol-make can behave differently depending on the value of `sinol_contest_type` key. +# st-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 (in st-make) `talent` is used. In sinol-make (OI version) is used 'default'. +# If the key is not specified, then (in st-make) `talent` is used. In st-make (OI version) is used 'default'. ### handwritten tests -# You can specify which tests are static (handwritten). This allows sinol-make to differentiate between +# You can specify which tests are static (handwritten). This allows st-make to differentiate between # old and handwritten tests. If this key is not present old tests won't be removed. # This key is optional and should be a list of tests. sinol_static_tests: ["abc0.in", "abc0a.in"] @@ -107,7 +107,7 @@ sinol_static_tests: ["abc0.in", "abc0a.in"] # sinol_expected_scores: {} -# sinol-make can check if the solutions run as expected when using `run` command. +# st-make can check if the solutions run as expected when using `run` command. # Key `sinol_expected_scores` defines expected scores for each solution on each tests. # There should be no reason to change this key manually. # It is automatically generated and managed by st-make. diff --git a/src/sinol_make/__init__.py b/src/sinol_make/__init__.py index fd8ab7f8..b31e24f4 100644 --- a/src/sinol_make/__init__.py +++ b/src/sinol_make/__init__.py @@ -82,7 +82,7 @@ def main(): new_version = None try: if util.is_dev(__version__): - print(util.warning('You are using a development version of sinol-make. ' + print(util.warning('You are using a development version of st-make. ' 'It may be unstable and contain bugs.')) new_version = util.check_for_updates(__version__) main_exn() @@ -99,10 +99,10 @@ def main(): if new_version is not None: if not util.is_dev(new_version): print(util.warning( - f'New version of sinol-make is available (your version: {__version__}, available version: ' - f'{new_version}).\nYou can update it by running `pip3 install sinol-make --upgrade`.')) + f'New version of st-make is available (your version: {__version__}, available version: ' + f'{new_version}).\nYou can update it by running `pip3 install st-make --upgrade`.')) elif util.is_dev(new_version): print(util.warning( - f'New development version of sinol-make is available (your version: {__version__}, available ' - f'version: {new_version}).\nYou can update it by running `pip3 install sinol-make --pre --upgrade`.' + f'New development version of st-make is available (your version: {__version__}, available ' + f'version: {new_version}).\nYou can update it by running `pip3 install st-make --pre --upgrade`.' )) diff --git a/src/sinol_make/commands/verify/__init__.py b/src/sinol_make/commands/verify/__init__.py index 978432a9..337698f1 100644 --- a/src/sinol_make/commands/verify/__init__.py +++ b/src/sinol_make/commands/verify/__init__.py @@ -38,7 +38,7 @@ def configure_subparser(self, subparser): help='expected contest type. Fails if the actual contest type is different.') parser.add_argument('-f', '--no-fsanitize', action='store_true', default=False, help='do not use sanitizers for ingen and inwer programs') - parsers.add_cpus_argument(parser, 'number of cpus that sinol-make will use') + parsers.add_cpus_argument(parser, 'number of cpus that st-make will use') parser.add_argument('--ignore-expected', dest='ignore_expected', action='store_true', help='ignore expected scores from config.yml. When this flag is set, ' 'the expected scores are not compared with the actual scores. ' diff --git a/src/sinol_make/helpers/compiler.py b/src/sinol_make/helpers/compiler.py index 8a1a68a4..c9cc52d1 100644 --- a/src/sinol_make/helpers/compiler.py +++ b/src/sinol_make/helpers/compiler.py @@ -98,7 +98,7 @@ def get_default_compilers(): c_compiler_path=get_c_compiler_path(), cpp_compiler_path=get_cpp_compiler_path(), python_interpreter_path=get_python_interpreter_path(), - # Java is not currently supported by sinol-make + # Java is not currently supported by st-make # java_compiler_path=get_java_compiler_path() ) diff --git a/src/sinol_make/helpers/parsers.py b/src/sinol_make/helpers/parsers.py index 00ec840a..53e23f5d 100644 --- a/src/sinol_make/helpers/parsers.py +++ b/src/sinol_make/helpers/parsers.py @@ -20,7 +20,7 @@ def add_compilation_arguments(parser: argparse.ArgumentParser): parser.add_argument('--python-interpreter-path', dest='python_interpreter_path', type=str, default=compiler.get_python_interpreter_path(), help='Python interpreter to use (default: python3)') - # Java is not currently supported by sinol-make + # Java is not currently supported by st-make # parser.add_argument('--java-compiler-path', dest='java_compiler_path', type=str, # default=compiler.get_java_compiler_path(), help='Java compiler to use (default: javac)') parser.add_argument('--compile-mode', '-C', dest='compile_mode', choices=['default', 'oioioi', 'weak', 'd', 'o', 'w'], diff --git a/tests/test_util.py b/tests/test_util.py index 69e410c7..4933551f 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -55,24 +55,24 @@ def test_check_version(**kwargs): def create_response(version): return {"releases": {version: "something"}} - mocker.get("https://pypi.python.org/pypi/sinol-make/json", json=create_response("1.0.0.dev2")) + mocker.get("https://pypi.python.org/pypi/st-make/json", json=create_response("1.0.0.dev2")) util.check_version() version = util.check_for_updates("1.0.0.dev1", False) assert version == "1.0.0.dev2" assert util.is_dev(version) - mocker.get("https://pypi.python.org/pypi/sinol-make/json", json=create_response("1.0.0")) + mocker.get("https://pypi.python.org/pypi/st-make/json", json=create_response("1.0.0")) util.check_version() version = util.check_for_updates("1.0.0.dev1", False) assert version == "1.0.0" assert not util.is_dev(version) - mocker.get("https://pypi.python.org/pypi/sinol-make/json", json=create_response("2.0.0.dev1")) + mocker.get("https://pypi.python.org/pypi/st-make/json", json=create_response("2.0.0.dev1")) util.check_version() version = util.check_for_updates("1.0.0", False) assert version is None - mocker.get("https://pypi.python.org/pypi/sinol-make/json", json=create_response("1.0.1")) + mocker.get("https://pypi.python.org/pypi/st-make/json", json=create_response("1.0.1")) util.check_version() version = util.check_for_updates("1.0.0", False) assert version == "1.0.1" From cef07810fa7191f5ac72b63ec13941ade79b167e Mon Sep 17 00:00:00 2001 From: Olaf Targowski Date: Sun, 21 Jul 2024 09:49:57 +0200 Subject: [PATCH 11/13] Try to fix GithubRunner.yaml It fails to install ghostscript. --- .github/workflows/GithubRunner.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/GithubRunner.yaml b/.github/workflows/GithubRunner.yaml index 96c9a5bb..427dbbda 100644 --- a/.github/workflows/GithubRunner.yaml +++ b/.github/workflows/GithubRunner.yaml @@ -18,6 +18,7 @@ jobs: - name: Install dependencies run: | pip install .[tests] + sudo apt update sudo apt install texlive-latex-recommended -y sudo apt install ghostscript -y - name: Run github runner pytest From e6d52474caf7b170b1ee1ae85683edb6f710863b Mon Sep 17 00:00:00 2001 From: Olaf Targowski Date: Sun, 21 Jul 2024 16:50:03 +0200 Subject: [PATCH 12/13] Loosen the memory limit in a test package Without this a test was failing because on macos. --- tests/packages/example_tests/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/packages/example_tests/config.yml b/tests/packages/example_tests/config.yml index 1b480e60..45e76cb0 100644 --- a/tests/packages/example_tests/config.yml +++ b/tests/packages/example_tests/config.yml @@ -1,6 +1,6 @@ title: Package with example tests sinol_task_id: exa -memory_limit: 10240 +memory_limit: 20480 time_limit: 1000 sinol_expected_scores: From 0de071fe725bbabec4d93afe67bcc0cf5537bad6 Mon Sep 17 00:00:00 2001 From: Olaf Targowski Date: Sun, 21 Jul 2024 17:11:09 +0200 Subject: [PATCH 13/13] And in another package... --- tests/packages/stresstest/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/packages/stresstest/config.yml b/tests/packages/stresstest/config.yml index 2da7a23e..ca6cfd2a 100644 --- a/tests/packages/stresstest/config.yml +++ b/tests/packages/stresstest/config.yml @@ -1,7 +1,7 @@ title: Package with stresstest.sh (for `verify` command) sinol_task_id: str time_limit: 1000 -memory_limit: 10240 +memory_limit: 20480 sinol_expected_scores: str.cpp: