From f65ae38e8b6aa91081e01b1dfd3d9f61590229b5 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Thu, 4 Apr 2024 17:29:35 +0200 Subject: [PATCH 1/3] Validate tests in parallel --- src/sinol_make/commands/ingen/__init__.py | 7 ++-- src/sinol_make/commands/outgen/__init__.py | 5 +-- src/sinol_make/helpers/package_util.py | 39 +++++++++++++++++----- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/sinol_make/commands/ingen/__init__.py b/src/sinol_make/commands/ingen/__init__.py index 9be7a6ef..af30fe2a 100644 --- a/src/sinol_make/commands/ingen/__init__.py +++ b/src/sinol_make/commands/ingen/__init__.py @@ -30,6 +30,8 @@ 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') + parser.add_argument('-c', '--cpus', type=int, + help=f'number of cpus to use (default: {util.default_cpu_count()})') parsers.add_compilation_arguments(parser) def run(self, args: argparse.Namespace): @@ -70,8 +72,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/outgen/__init__.py b/src/sinol_make/commands/outgen/__init__.py index c32b7c5a..c9c16d0b 100644 --- a/src/sinol_make/commands/outgen/__init__.py +++ b/src/sinol_make/commands/outgen/__init__.py @@ -129,7 +129,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(outputs_to_generate, self.args.cpus, 'outputs') diff --git a/src/sinol_make/helpers/package_util.py b/src/sinol_make/helpers/package_util.py index d155e746..52dab16a 100644 --- a/src/sinol_make/helpers/package_util.py +++ b/src/sinol_make/helpers/package_util.py @@ -3,8 +3,9 @@ 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 import util from sinol_make.helpers import paths @@ -345,11 +346,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 +360,42 @@ 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!')) From 2439c422d927f0fdde6942ae604a6920fcd61b42 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Thu, 4 Apr 2024 17:58:55 +0200 Subject: [PATCH 2/3] Fix test --- src/sinol_make/commands/outgen/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sinol_make/commands/outgen/__init__.py b/src/sinol_make/commands/outgen/__init__.py index c9c16d0b..126b7049 100644 --- a/src/sinol_make/commands/outgen/__init__.py +++ b/src/sinol_make/commands/outgen/__init__.py @@ -129,4 +129,4 @@ def run(self, args: argparse.Namespace): yaml.dump(md5_sums, f) if not self.args.no_validate: - package_util.validate_tests(outputs_to_generate, self.args.cpus, 'outputs') + package_util.validate_tests(sorted(outputs_to_generate), self.args.cpus, 'outputs') From fa47c27c7a97d34ce852caf2c44fba0ad26115a0 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Thu, 4 Apr 2024 18:25:44 +0200 Subject: [PATCH 3/3] Fix again --- tests/commands/gen/test_unit.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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