Skip to content

Commit 1eecdff

Browse files
authored
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
1 parent 6cafe41 commit 1eecdff

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+881
-209
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ Run `sinol-make ingen --help` to see available flags.
8383
program to use, what tests to check and how many CPUs to use. Run `sinol-make inwer --help` to see available flags.
8484
- `sinol-make export` -- Creates archive ready to upload to sio2 or szkopul. Run `sinol-make export --help` to see all available flags.
8585
- `sinol-make doc` -- Compiles all LaTeX files in doc/ directory to PDF. Run `sinol-make doc --help` to see all available flags.
86+
- `sinol-make verify` -- Verifies the package. This command runs stress tests (if available), verifies the config,
87+
generates tests, generates problem statements, runs inwer and run all solutions. Ingen and inwer are compiled with
88+
address and UB sanitizers. Run `sinol-make verify --help` to see all available flags.
8689
- `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.
8790

8891
You can also run multiple commands at once, for example:

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ install_requires =
2929
dictdiffer
3030
importlib-resources
3131
psutil
32+
packaging
3233

3334
[options.packages.find]
3435
where = src

src/sinol_make/__init__.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
from sinol_make import util, oiejq
88

9-
__version__ = "1.5.30"
9+
10+
__version__ = "1.6.0"
1011

1112

1213
def configure_parsers():
@@ -50,35 +51,40 @@ def main_exn():
5051
parser = configure_parsers()
5152
arguments = []
5253
curr_args = []
54+
commands = util.get_commands()
55+
commands_dict = {command.get_name(): command for command in commands}
5356
for arg in sys.argv[1:]:
54-
if arg in util.get_command_names() and not (len(curr_args) > 0 and curr_args[0] == 'init'):
57+
if arg in commands_dict.keys() and not (len(curr_args) > 0 and curr_args[0] == 'init'):
5558
if curr_args:
5659
arguments.append(curr_args)
5760
curr_args = [arg]
5861
else:
5962
curr_args.append(arg)
6063
if curr_args:
6164
arguments.append(curr_args)
62-
commands = util.get_commands()
65+
if not arguments:
66+
parser.print_help()
67+
exit(1)
6368
check_oiejq()
6469

6570
for curr_args in arguments:
6671
args = parser.parse_args(curr_args)
67-
command_found = False
68-
for command in commands:
69-
if command.get_name() == args.command:
70-
command_found = True
71-
if len(arguments) > 1:
72-
print(f' {command.get_name()} command '.center(util.get_terminal_size()[1], '='))
73-
command.run(args)
74-
if not command_found:
72+
command = commands_dict.get(args.command, None)
73+
if command:
74+
if len(arguments) > 1:
75+
print(f' {command.get_name()} command '.center(util.get_terminal_size()[1], '='))
76+
command.run(args)
77+
else:
7578
parser.print_help()
7679
exit(1)
7780

7881

7982
def main():
8083
new_version = None
8184
try:
85+
if util.is_dev(__version__):
86+
print(util.warning('You are using a development version of sinol-make. '
87+
'It may be unstable and contain bugs.'))
8288
new_version = util.check_for_updates(__version__)
8389
main_exn()
8490
except argparse.ArgumentError as err:
@@ -92,6 +98,12 @@ def main():
9298
'https://github.com/sio2project/sinol-make/#reporting-bugs-and-contributing-code')
9399
finally:
94100
if new_version is not None:
95-
print(util.warning(
96-
f'New version of sinol-make is available (your version: {__version__}, available version: '
97-
f'{new_version}).\nYou can update it by running `pip3 install sinol-make --upgrade`.'))
101+
if not util.is_dev(new_version):
102+
print(util.warning(
103+
f'New version of sinol-make is available (your version: {__version__}, available version: '
104+
f'{new_version}).\nYou can update it by running `pip3 install sinol-make --upgrade`.'))
105+
elif util.is_dev(new_version):
106+
print(util.warning(
107+
f'New development version of sinol-make is available (your version: {__version__}, available '
108+
f'version: {new_version}).\nYou can update it by running `pip3 install sinol-make --pre --upgrade`.'
109+
))

src/sinol_make/commands/doc/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def get_name(self):
1818
return "doc"
1919

2020
def compile_file_latex_div(self, file_path):
21-
print(util.info(f'Compiling {os.path.basename(file_path)} (latex to dvi)...'))
21+
print(f'Compiling {os.path.basename(file_path)} (latex to dvi)...')
2222
os.chdir(os.path.dirname(file_path))
2323
subprocess.run(['latex', file_path])
2424
dvi_file = os.path.splitext(file_path)[0] + '.dvi'
@@ -35,7 +35,7 @@ def compile_file_latex_div(self, file_path):
3535
return True
3636

3737
def compile_pdf_latex(self, file_path):
38-
print(util.info(f'Compiling {os.path.basename(file_path)} (pdflatex)...'))
38+
print(f'Compiling {os.path.basename(file_path)} (pdflatex)...')
3939
os.chdir(os.path.dirname(file_path))
4040
subprocess.run(['pdflatex', file_path])
4141
pdf_file = os.path.splitext(file_path)[0] + '.pdf'
@@ -62,7 +62,7 @@ def move_logs(self):
6262
for pattern in self.LOG_PATTERNS:
6363
for file in glob.glob(os.path.join(os.getcwd(), 'doc', pattern)):
6464
os.rename(file, os.path.join(output_dir, os.path.basename(file)))
65-
print(util.info(f'Compilation log files can be found in {os.path.relpath(output_dir, os.getcwd())}'))
65+
print(f'Compilation log files can be found in {os.path.relpath(output_dir, os.getcwd())}')
6666

6767
def configure_subparser(self, subparser: argparse.ArgumentParser):
6868
parser = subparser.add_parser(
@@ -76,6 +76,7 @@ def configure_subparser(self, subparser: argparse.ArgumentParser):
7676
' pdflatex - uses pdflatex. Works with .png and .jpg images.\n'
7777
' latex_dvi - uses latex and dvipdf. Works with .ps and .eps images.', default='auto')
7878
parser.add_argument('files', type=str, nargs='*', help='files to compile')
79+
return parser
7980

8081
def run(self, args: argparse.Namespace):
8182
args = util.init_package_command(args)

src/sinol_make/commands/export/__init__.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,13 @@ def configure_subparser(self, subparser: argparse.ArgumentParser):
2828
self.get_name(),
2929
help='Create archive for oioioi upload',
3030
description='Creates archive in the current directory ready to upload to sio2 or szkopul.')
31-
parser.add_argument('-c', '--cpus', type=int,
32-
help=f'number of cpus to use to generate output files '
33-
f'(default: {util.default_cpu_count()})',
34-
default=util.default_cpu_count())
31+
parsers.add_cpus_argument(parser, 'number of cpus to use to generate output files')
3532
parser.add_argument('--no-statement', dest='no_statement', action='store_true',
3633
help='allow export without statement')
3734
parser.add_argument('--export-ocen', dest='export_ocen', action='store_true',
3835
help='Create ocen archive')
3936
parsers.add_compilation_arguments(parser)
37+
return parser
4038

4139
def generate_input_tests(self):
4240
print('Generating tests...')
@@ -97,6 +95,7 @@ def create_ocen(self, target_dir: str):
9795
Creates ocen archive for sio2.
9896
:param target_dir: Path to exported package.
9997
"""
98+
print('Generating ocen archive...')
10099
attachments_dir = os.path.join(target_dir, 'attachments')
101100
if not os.path.exists(attachments_dir):
102101
os.makedirs(attachments_dir)
@@ -109,12 +108,27 @@ def create_ocen(self, target_dir: str):
109108
os.makedirs(in_dir)
110109
out_dir = os.path.join(ocen_dir, 'out')
111110
os.makedirs(out_dir)
111+
num_tests = 0
112112
for ext in ['in', 'out']:
113113
for test in glob.glob(os.path.join(tests_dir, ext, f'{self.task_id}0*.{ext}')) + \
114114
glob.glob(os.path.join(tests_dir, ext, f'{self.task_id}*ocen.{ext}')):
115115
shutil.copy(test, os.path.join(ocen_dir, ext, os.path.basename(test)))
116-
117-
shutil.make_archive(os.path.join(attachments_dir, f'{self.task_id}ocen'), 'zip', tmpdir)
116+
num_tests += 1
117+
118+
dlazaw_dir = os.path.join(os.getcwd(), 'dlazaw')
119+
if num_tests == 0:
120+
print(util.warning('No ocen tests found.'))
121+
elif os.path.exists(dlazaw_dir):
122+
print(util.warning('Skipping ocen archive creation because dlazaw directory exists.'))
123+
else:
124+
shutil.make_archive(os.path.join(attachments_dir, f'{self.task_id}ocen'), 'zip', tmpdir)
125+
126+
if os.path.exists(dlazaw_dir):
127+
print('Archiving dlazaw directory and adding to attachments.')
128+
os.makedirs(os.path.join(tmpdir, 'dlazaw'), exist_ok=True)
129+
shutil.copytree(dlazaw_dir, os.path.join(tmpdir, 'dlazaw', 'dlazaw'))
130+
shutil.make_archive(os.path.join(attachments_dir, 'dlazaw'), 'zip',
131+
os.path.join(tmpdir, 'dlazaw'))
118132

119133
def compile_statement(self):
120134
command = DocCommand()
@@ -168,7 +182,6 @@ def copy_package_required_files(self, target_dir: str):
168182

169183
self.generate_output_files()
170184
if self.args.export_ocen:
171-
print('Generating ocen archive...')
172185
self.create_ocen(target_dir)
173186

174187
def clear_files(self, target_dir: str):

src/sinol_make/commands/gen/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,12 @@ def configure_subparser(self, subparser):
3131
help='path to ingen source file, for example prog/abcingen.cpp')
3232
parser.add_argument('-i', '--only-inputs', action='store_true', help='generate input files only')
3333
parser.add_argument('-o', '--only-outputs', action='store_true', help='generate output files only')
34-
parser.add_argument('-c', '--cpus', type=int,
35-
help=f'number of cpus to use to generate output files '
36-
f'(default: {util.default_cpu_count()})',
37-
default=util.default_cpu_count())
34+
parsers.add_cpus_argument(parser, 'number of cpus to use to generate output files')
3835
parser.add_argument('-n', '--no-validate', default=False, action='store_true',
3936
help='do not validate test contents')
37+
parsers.add_fsanitize_argument(parser)
4038
parsers.add_compilation_arguments(parser)
39+
return parser
4140

4241
def run(self, args: argparse.Namespace):
4342
args = util.init_package_command(args)

src/sinol_make/commands/ingen/__init__.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ def configure_subparser(self, subparser):
3030
help='path to ingen source file, for example prog/abcingen.cpp')
3131
parser.add_argument('-n', '--no-validate', default=False, action='store_true',
3232
help='do not validate test contents')
33+
parsers.add_cpus_argument(parser, 'number of cpus used for validating tests')
34+
parsers.add_fsanitize_argument(parser)
3335
parsers.add_compilation_arguments(parser)
36+
return parser
3437

3538
def delete_dangling_files(self, dates):
3639
to_delete = set()
@@ -51,7 +54,7 @@ def delete_dangling_files(self, dates):
5154
static_files = set([os.path.basename(test) for test in static_files])
5255
to_delete = to_delete - static_files
5356
if to_delete:
54-
print(util.info('Cleaning up old input files.'))
57+
print('Cleaning up old input files.')
5558
for test in to_delete:
5659
os.remove(os.path.join(os.getcwd(), "in", test))
5760

@@ -61,11 +64,10 @@ def run(self, args: argparse.Namespace):
6164
self.args = args
6265

6366
self.task_id = package_util.get_task_id()
64-
package_util.validate_test_names(self.task_id)
6567
util.change_stack_size_to_unlimited()
6668
self.ingen = get_ingen(self.task_id, args.ingen_path)
67-
print(util.info(f'Using ingen file {os.path.basename(self.ingen)}'))
68-
self.ingen_exe = compile_ingen(self.ingen, self.args, self.args.compile_mode)
69+
print(f'Using ingen file {os.path.basename(self.ingen)}')
70+
self.ingen_exe = compile_ingen(self.ingen, self.args, self.args.compile_mode, self.args.fsanitize)
6971

7072
previous_tests = []
7173
try:
@@ -89,8 +91,5 @@ def run(self, args: argparse.Namespace):
8991
f.write("\n".join(glob.glob(os.path.join(os.getcwd(), "in", f"{self.task_id}*.in"))))
9092

9193
if not self.args.no_validate:
92-
print(util.info('Validating input test contents.'))
9394
tests = sorted(glob.glob(os.path.join(os.getcwd(), "in", f"{self.task_id}*.in")))
94-
for test in tests:
95-
package_util.validate_test(test)
96-
print(util.info('Input test contents are valid!'))
95+
package_util.validate_tests(tests, self.args.cpus, 'input')

src/sinol_make/commands/ingen/ingen_util.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def get_ingen(task_id, ingen_path=None):
4747
return correct_ingen
4848

4949

50-
def compile_ingen(ingen_path: str, args: argparse.Namespace, compilation_flags='default'):
50+
def compile_ingen(ingen_path: str, args: argparse.Namespace, compilation_flags='default', use_fsanitize=False):
5151
"""
5252
Compiles ingen and returns path to compiled executable.
5353
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='
5757

5858
compilers = compiler.verify_compilers(args, [ingen_path])
5959
ingen_exe, compile_log_path = compile.compile_file(ingen_path, package_util.get_executable(ingen_path),
60-
compilers, compilation_flags, use_fsanitize=True,
60+
compilers, compilation_flags, use_fsanitize=use_fsanitize,
6161
additional_flags='-D_INGEN')
6262

6363
if ingen_exe is None:
@@ -86,11 +86,21 @@ def run_ingen(ingen_exe, working_dir=None):
8686
print(util.bold(' Ingen output '.center(util.get_terminal_size()[1], '=')))
8787
process = subprocess.Popen([ingen_exe], stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
8888
cwd=working_dir, shell=is_shell)
89+
whole_output = ''
8990
while process.poll() is None:
90-
print(process.stdout.readline().decode('utf-8'), end='')
91-
92-
print(process.stdout.read().decode('utf-8'), end='')
91+
out = process.stdout.readline().decode('utf-8')
92+
if out != '':
93+
print(out, end='')
94+
whole_output += out
95+
out = process.stdout.read().decode('utf-8')
96+
whole_output += out
97+
print(out, end='')
9398
exit_code = process.returncode
9499
print(util.bold(' End of ingen output '.center(util.get_terminal_size()[1], '=')))
95100

101+
if util.has_sanitizer_error(whole_output, exit_code):
102+
print(util.warning('Warning: if ingen failed due to sanitizer errors, you can either run '
103+
'`sudo sysctl vm.mmap_rnd_bits = 28` to fix this or disable sanitizers with the '
104+
'--no-fsanitize flag.'))
105+
96106
return exit_code == 0

src/sinol_make/commands/init/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def configure_subparser(self, subparser: argparse.ArgumentParser):
2323
description='Create package from predefined template with given id.'
2424
)
2525
parser.add_argument('task_id', type=str, help='Id of the task to create')
26+
return parser
2627

2728
def download_template(self):
2829
repo = 'https://github.com/sio2project/sinol-make.git'
@@ -69,8 +70,7 @@ def run(self, args: argparse.Namespace):
6970

7071
self.move_folder()
7172
self.update_config()
72-
73+
7374
self.used_tmpdir.cleanup()
7475

7576
print(util.info(f'Successfully created task "{self.task_id}"'))
76-

src/sinol_make/commands/inwer/__init__.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010

1111
from sinol_make import util
1212
from sinol_make.structs.inwer_structs import TestResult, InwerExecution, VerificationResult, TableData
13-
from sinol_make.helpers import package_util, compile, printer, paths
14-
from sinol_make.helpers.parsers import add_compilation_arguments
13+
from sinol_make.helpers import package_util, printer, paths, parsers
1514
from sinol_make.interfaces.BaseCommand import BaseCommand
1615
from sinol_make.commands.inwer import inwer_util
1716

@@ -37,9 +36,10 @@ def configure_subparser(self, subparser: argparse.ArgumentParser):
3736
help='path to inwer source file, for example prog/abcinwer.cpp')
3837
parser.add_argument('-t', '--tests', type=str, nargs='+',
3938
help='test to verify, for example in/abc{0,1}*')
40-
parser.add_argument('-c', '--cpus', type=int,
41-
help=f'number of cpus to use (default: {util.default_cpu_count()})')
42-
add_compilation_arguments(parser)
39+
parsers.add_cpus_argument(parser, 'number of cpus to use when verifying tests')
40+
parsers.add_fsanitize_argument(parser)
41+
parsers.add_compilation_arguments(parser)
42+
return parser
4343

4444
@staticmethod
4545
def verify_test(execution: InwerExecution) -> VerificationResult:
@@ -94,11 +94,14 @@ def verify_and_print_table(self) -> Dict[str, TestResult]:
9494
thr.start()
9595

9696
keyboard_interrupt = False
97+
sanitizer_error = False
9798
try:
9899
with mp.Pool(self.cpus) as pool:
99100
for i, result in enumerate(pool.imap(self.verify_test, executions)):
100101
table_data.results[result.test_path].set_results(result.valid, result.output)
101102
table_data.i = i
103+
if util.has_sanitizer_error(result.output, 0 if result.valid else 1):
104+
sanitizer_error = True
102105
except KeyboardInterrupt:
103106
keyboard_interrupt = True
104107

@@ -108,6 +111,10 @@ def verify_and_print_table(self) -> Dict[str, TestResult]:
108111

109112
print("\n".join(inwer_util.print_view(terminal_width, terminal_height, table_data)[0]))
110113

114+
if sanitizer_error:
115+
print(util.warning('Warning: if inwer failed due to sanitizer errors, you can either run '
116+
'`sudo sysctl vm.mmap_rnd_bits = 28` to fix this or disable sanitizers with the '
117+
'--no-fsanitize flag.'))
111118
if keyboard_interrupt:
112119
util.exit_with_error('Keyboard interrupt.')
113120

@@ -203,7 +210,7 @@ def run(self, args: argparse.Namespace):
203210
print('Verifying tests: ' + util.bold(', '.join(self.tests)))
204211

205212
util.change_stack_size_to_unlimited()
206-
self.inwer_executable = inwer_util.compile_inwer(self.inwer, args, args.compile_mode)
213+
self.inwer_executable = inwer_util.compile_inwer(self.inwer, args, args.compile_mode, args.fsanitize)
207214
results: Dict[str, TestResult] = self.verify_and_print_table()
208215
print('')
209216

@@ -218,4 +225,3 @@ def run(self, args: argparse.Namespace):
218225
print("Verifying tests order...")
219226
self.verify_tests_order()
220227
print(util.info('Verification successful.'))
221-
exit(0)

0 commit comments

Comments
 (0)