Repository: sio2project/sinol-make (language: python)

Issue:
Input and output content verification should run in parallel 



Discussion / hints:

Fix patch:
diff --git a/src/sinol_make/commands/ingen/__init__.py b/src/sinol_make/commands/ingen/__init__.py
index c75401eb..552ccdbe 100644
--- a/src/sinol_make/commands/ingen/__init__.py
+++ b/src/sinol_make/commands/ingen/__init__.py
@@ -30,6 +30,7 @@ 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')
         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`.')
@@ -74,8 +75,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 ab139610..3a188484 100644
--- a/src/sinol_make/commands/outgen/__init__.py
+++ b/src/sinol_make/commands/outgen/__init__.py
@@ -126,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/helpers/package_util.py b/src/sinol_make/helpers/package_util.py
index a4789697..98d582b8 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.helpers.func_cache import cache_result
 from sinol_make import util
@@ -355,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
@@ -368,24 +370,45 @@ 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):


Test patch:
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