Skip to content

Commit dd1aa04

Browse files
Input and output file generation command (#63)
* Simple generation * Fix problems * Add support for shell ingens * Fix executing shell ingens * Add comments for struct * Add package with shell ingen * Add unit tests for gen command * Bash parallel input generation * Apply suggestions from code review Co-authored-by: Tomasz Nowak <[email protected]> * Rename variable * Change compile_ingen function * Change compile_correct_solution function * Add lambda function for compiling solutions * Refactor ingen execution * Use weak-compilation-flags flag * Change to use imap instead of imap_unordered * Fix tests * Refactor * Rename files and add correct solution in gen test package * Fix typo * Add integration tests for `gen` command * Add execution permission to ingen.sh when running * Small refactor * Change ingen.sh in package * Refactor code * Return less Nones in gen * Fix tests * Add function for calling function and exiting * Refactor * Refactor `util.exit_with_error` * Use python for giving execution permission and cleanup imports * Apply suggestions from code review Co-authored-by: Tomasz Nowak <[email protected]> * Fix tests * Bump version for release * Add function for fixing line endings * Refactor .md5sums parsing --------- Co-authored-by: Tomasz Nowak <[email protected]>
1 parent 82df741 commit dd1aa04

20 files changed

+596
-13
lines changed

src/sinol_make/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from sinol_make import util
88

9-
__version__ = "1.3.2"
9+
__version__ = "1.4.0"
1010

1111
def configure_parsers():
1212
parser = argparse.ArgumentParser(
+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import argparse
2+
import glob
3+
import os
4+
import hashlib
5+
import yaml
6+
7+
import multiprocessing as mp
8+
9+
from sinol_make import util
10+
from sinol_make.commands.gen import gen_util
11+
from sinol_make.commands.gen.structs import OutputGenerationArguments
12+
from sinol_make.helpers import parsers, package_util, compile
13+
from sinol_make.interfaces.BaseCommand import BaseCommand
14+
15+
16+
class Command(BaseCommand):
17+
"""
18+
Class for `gen` command.
19+
"""
20+
21+
def get_name(self):
22+
return "gen"
23+
24+
def configure_subparser(self, subparser):
25+
parser = subparser.add_parser(
26+
self.get_name(),
27+
help='Generate input and output files',
28+
description='Generate input files using ingen program '
29+
'(for example prog/abcingen.cpp for abc task). Whenever '
30+
'the new input differs from the previous one, '
31+
'the model solution will be used to generate the new output '
32+
'file. You can also specify your ingen source '
33+
'file which will be used.'
34+
)
35+
36+
parser.add_argument('ingen_path', type=str, nargs='?',
37+
help='path to ingen source file, for example prog/abcingen.cpp')
38+
parser.add_argument('-c', '--cpus', type=int,
39+
help=f'number of cpus to use, by default {mp.cpu_count()} (all available). '
40+
f'Used when generating output files.', default=mp.cpu_count())
41+
parsers.add_compilation_arguments(parser)
42+
43+
def generate_outputs(self, outputs_to_generate):
44+
print(f'Generating output files for {len(outputs_to_generate)} tests on {self.args.cpus} cpus.')
45+
arguments = []
46+
for output in outputs_to_generate:
47+
output_basename = os.path.basename(output)
48+
input = os.path.join(os.getcwd(), 'in', os.path.splitext(output_basename)[0] + '.in')
49+
arguments.append(OutputGenerationArguments(self.correct_solution_exe, input, output))
50+
51+
with mp.Pool(self.args.cpus) as pool:
52+
results = []
53+
for i, result in enumerate(pool.imap(gen_util.generate_output, arguments)):
54+
results.append(result)
55+
if result:
56+
print(util.info(f'Successfully generated output file {os.path.basename(arguments[i].output_test)}'))
57+
else:
58+
print(util.error(f'Failed to generate output file {os.path.basename(arguments[i].output_test)}'))
59+
60+
if not all(results):
61+
util.exit_with_error('Failed to generate some output files.')
62+
else:
63+
print(util.info('Successfully generated all output files.'))
64+
65+
def calculate_md5_sums(self):
66+
"""
67+
Calculates md5 sums for each test.
68+
:return: Tuple (dictionary of md5 sums, list of outputs tests that need to be generated)
69+
"""
70+
old_md5_sums = None
71+
try:
72+
with open(os.path.join(os.getcwd(), 'in', '.md5sums'), 'r') as f:
73+
file_content = yaml.load(f, Loader=yaml.FullLoader)
74+
if isinstance(file_content, dict):
75+
old_md5_sums = file_content
76+
except (yaml.YAMLError, OSError):
77+
pass
78+
79+
md5_sums = {}
80+
outputs_to_generate = []
81+
for file in glob.glob(os.path.join(os.getcwd(), 'in', '*.in')):
82+
basename = os.path.basename(file)
83+
md5_sums[basename] = hashlib.md5(open(file, 'rb').read()).hexdigest()
84+
if old_md5_sums is None or old_md5_sums.get(basename, '') != md5_sums[basename]:
85+
output_basename = os.path.splitext(os.path.basename(basename))[0] + '.out'
86+
outputs_to_generate.append(os.path.join(os.getcwd(), "out", output_basename))
87+
88+
return md5_sums, outputs_to_generate
89+
90+
def run(self, args: argparse.Namespace):
91+
if not util.check_if_project():
92+
util.exit_with_error('You are not in a project directory (couldn\'t find config.yml in current directory).')
93+
94+
self.args = args
95+
self.task_id = package_util.get_task_id()
96+
self.ingen = gen_util.get_ingen(self.task_id, args.ingen_path)
97+
print(util.info(f'Using ingen file {os.path.basename(self.ingen)}'))
98+
99+
self.correct_solution = gen_util.get_correct_solution(self.task_id)
100+
101+
if os.path.splitext(self.ingen)[1] != '.sh':
102+
self.ingen_exe = gen_util.compile_ingen(self.ingen, self.args, self.args.weak_compilation_flags)
103+
else:
104+
self.ingen_exe = self.ingen
105+
106+
if gen_util.run_ingen(self.ingen_exe):
107+
print(util.info('Successfully generated input files.'))
108+
else:
109+
util.exit_with_error('Failed to generate input files.')
110+
md5_sums, outputs_to_generate = self.calculate_md5_sums()
111+
if len(outputs_to_generate) == 0:
112+
print(util.info('All output files are up to date.'))
113+
else:
114+
self.correct_solution_exe = gen_util.compile_correct_solution(self.correct_solution, self.args,
115+
self.args.weak_compilation_flags)
116+
self.generate_outputs(outputs_to_generate)
117+
118+
with open(os.path.join(os.getcwd(), 'in', '.md5sums'), 'w') as f:
119+
yaml.dump(md5_sums, f)
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import glob
2+
import os
3+
import stat
4+
import argparse
5+
import signal
6+
import subprocess
7+
import sys
8+
9+
from sinol_make import util
10+
from sinol_make.helpers import compiler, package_util, compile
11+
12+
13+
def get_ingen(task_id=None, ingen_path=None):
14+
"""
15+
Find ingen source file in `prog/` directory.
16+
If `ingen_path` is specified, then it will be used (if exists).
17+
:param task_id: task id, for example abc. If None, then
18+
will return any ingen matching "*ingen.*"
19+
:param ingen_path: path to ingen source file
20+
:return: path to ingen source file or None if not found
21+
"""
22+
23+
if ingen_path is not None:
24+
if os.path.exists(ingen_path):
25+
return ingen_path
26+
else:
27+
util.exit_with_error(f'Ingen source file {ingen_path} does not exist.')
28+
29+
if task_id is None:
30+
task_id = '*'
31+
ingen = glob.glob(os.path.join(os.getcwd(), 'prog', task_id + 'ingen.*'))
32+
if len(ingen) == 0:
33+
util.exit_with_error(f'Ingen source file for task {task_id} does not exist.')
34+
35+
# Sio2 first chooses shell scripts, then non-shell source codes.
36+
if os.path.splitext(ingen[0])[1] == '.sh' and len(ingen) > 1:
37+
return ingen[1]
38+
else:
39+
return ingen[0]
40+
41+
42+
def compile_ingen(ingen_path: str, args: argparse.Namespace, weak_compilation_flags=False):
43+
"""
44+
Compiles ingen and returns path to compiled executable.
45+
"""
46+
compilers = compiler.verify_compilers(args, [ingen_path])
47+
ingen_exe, compile_log_path = compile.compile_file(ingen_path, package_util.get_executable(ingen_path), compilers, weak_compilation_flags)
48+
49+
if ingen_exe is None:
50+
util.exit_with_error('Failed ingen compilation.', lambda: compile.print_compile_log(compile_log_path))
51+
else:
52+
print(util.info('Successfully compiled ingen.'))
53+
return ingen_exe
54+
55+
56+
def get_correct_solution(task_id):
57+
"""
58+
Returns path to correct solution for given task.
59+
:param task_id: task id, for example abc
60+
:return: path to correct solution or None if not found
61+
"""
62+
correct_solution = glob.glob(os.path.join(os.getcwd(), 'prog', task_id + '.*'))
63+
if len(correct_solution) == 0:
64+
util.exit_with_error(f'Correct solution for task {task_id} does not exist.')
65+
return correct_solution[0]
66+
67+
68+
def compile_correct_solution(solution_path: str, args: argparse.Namespace, weak_compilation_flags=False):
69+
"""
70+
Compiles correct solution and returns path to compiled executable.
71+
"""
72+
compilers = compiler.verify_compilers(args, [solution_path])
73+
correct_solution_exe, compile_log_path = compile.compile_file(solution_path, package_util.get_executable(solution_path), compilers,
74+
weak_compilation_flags)
75+
if correct_solution_exe is None:
76+
util.exit_with_error('Failed compilation of correct solution.',
77+
lambda: compile.print_compile_log(compile_log_path))
78+
else:
79+
print(util.info('Successfully compiled correct solution.'))
80+
81+
return correct_solution_exe
82+
83+
84+
85+
def run_ingen(ingen_exe):
86+
"""
87+
Runs ingen and generates all input files.
88+
:param ingen_exe: path to ingen executable
89+
:return: True if ingen was successful, False otherwise
90+
"""
91+
is_shell = os.path.splitext(ingen_exe)[1] == '.sh'
92+
if is_shell:
93+
util.fix_file_endings(ingen_exe)
94+
st = os.stat(ingen_exe)
95+
os.chmod(ingen_exe, st.st_mode | stat.S_IEXEC)
96+
97+
process = subprocess.Popen([ingen_exe], stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
98+
cwd=os.path.join(os.getcwd(), 'in'), shell=is_shell)
99+
while process.poll() is None:
100+
print(process.stdout.readline().decode('utf-8'), end='')
101+
102+
print(process.stdout.read().decode('utf-8'), end='')
103+
exit_code = process.returncode
104+
105+
return exit_code == 0
106+
107+
108+
def generate_output(arguments):
109+
"""
110+
Generates output file for given input file.
111+
:param arguments: arguments for output generation (type OutputGenerationArguments)
112+
:return: True if the output was successfully generated, False otherwise
113+
"""
114+
input_test = arguments.input_test
115+
output_test = arguments.output_test
116+
correct_solution_exe = arguments.correct_solution_exe
117+
118+
input_file = open(input_test, 'r')
119+
output_file = open(output_test, 'w')
120+
process = subprocess.Popen([correct_solution_exe], stdin=input_file, stdout=output_file, preexec_fn=os.setsid)
121+
previous_sigint_handler = signal.getsignal(signal.SIGINT)
122+
123+
def sigint_handler(signum, frame):
124+
try:
125+
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
126+
except ProcessLookupError:
127+
pass
128+
sys.exit(1)
129+
signal.signal(signal.SIGINT, sigint_handler)
130+
131+
process.wait()
132+
signal.signal(signal.SIGINT, previous_sigint_handler)
133+
exit_code = process.returncode
134+
input_file.close()
135+
output_file.close()
136+
137+
return exit_code == 0
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class OutputGenerationArguments:
6+
"""
7+
Arguments used for function that generates output file.
8+
"""
9+
# Path to correct solution executable
10+
correct_solution_exe: str
11+
# Path to input file
12+
input_test: str
13+
# Path to output file
14+
output_test: str

src/sinol_make/commands/inwer/__init__.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ def configure_subparser(self, subparser: argparse.ArgumentParser):
4242
def compile_inwer(self, args: argparse.Namespace):
4343
self.inwer_executable, compile_log_path = inwer_util.compile_inwer(self.inwer, args, args.weak_compilation_flags)
4444
if self.inwer_executable is None:
45-
print(util.error('Compilation failed.'))
46-
compile.print_compile_log(compile_log_path)
47-
exit(1)
45+
util.exit_with_error('Compilation failed.', lambda: compile.print_compile_log(compile_log_path))
4846
else:
4947
print(util.info('Compilation successful.'))
5048

src/sinol_make/helpers/compile.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from typing import Tuple
2+
import os
3+
import sys
4+
import stat
5+
import subprocess
26

37
import sinol_make.helpers.compiler as compiler
48
from sinol_make.interfaces.Errors import CompilationError
59
from sinol_make.structs.compiler_structs import Compilers
6-
import os, subprocess, sys
10+
711

812
def compile(program, output, compilers: Compilers = None, compile_log = None, weak_compilation_flags = False):
913
"""
@@ -32,7 +36,8 @@ def compile(program, output, compilers: Compilers = None, compile_log = None, we
3236
else:
3337
open(output, 'w').write('#!/usr/bin/python3\n')
3438
open(output, 'a').write(open(program, 'r').read())
35-
subprocess.call(['chmod', '+x', output])
39+
st = os.stat(output)
40+
os.chmod(output, st.st_mode | stat.S_IEXEC)
3641
arguments = [compilers.python_interpreter_path, '-m', 'py_compile', program]
3742
elif ext == '.java':
3843
raise NotImplementedError('Java compilation is not implemented')

src/sinol_make/helpers/compiler.py

+12
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@ def get_java_compiler_path():
8080
return 'javac'
8181

8282

83+
def get_default_compilers():
84+
"""
85+
Get the default compilers
86+
"""
87+
return argparse.Namespace(
88+
c_compiler_path=get_c_compiler_path(),
89+
cpp_compiler_path=get_cpp_compiler_path(),
90+
python_interpreter_path=get_python_interpreter_path(),
91+
java_compiler_path=get_java_compiler_path()
92+
)
93+
94+
8395
def verify_compilers(args: argparse.Namespace, solutions: list[str]) -> Compilers:
8496
for solution in solutions:
8597
ext = os.path.splitext(solution)[1]

src/sinol_make/util.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,14 @@ def get_terminal_size():
263263
terminal_height = 30
264264
return has_terminal, terminal_width, terminal_height
265265

266+
267+
def fix_file_endings(file):
268+
with open(file, "rb") as f:
269+
content = f.read()
270+
with open(file, "wb") as f:
271+
f.write(content.replace(b"\r\n", b"\n"))
272+
273+
266274
def color_red(text): return "\033[91m{}\033[00m".format(text)
267275
def color_green(text): return "\033[92m{}\033[00m".format(text)
268276
def color_yellow(text): return "\033[93m{}\033[00m".format(text)
@@ -275,6 +283,9 @@ def warning(text):
275283
def error(text):
276284
return bold(color_red(text))
277285

278-
def exit_with_error(text):
286+
287+
def exit_with_error(text, func=None):
279288
print(error(text))
289+
if func is not None:
290+
func()
280291
exit(1)

tests/commands/gen/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)