Skip to content

Commit b848101

Browse files
Add support for packages with library (#64)
* Add support for packages with library * Add test package with library * Add package with library to tests * Change to use `with` statements everywhere * Apply suggestions from code review Co-authored-by: Tomasz Nowak <[email protected]> * Use `with statements in oiejq installation` * Fix oiejq installation * Add comment describing oiejq installation * Add short flag for --weak-compilation-flags * Bump version for release --------- Co-authored-by: Tomasz Nowak <[email protected]>
1 parent dd1aa04 commit b848101

20 files changed

+337
-67
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.4.0"
9+
__version__ = "1.4.1"
1010

1111
def configure_parsers():
1212
parser = argparse.ArgumentParser(

src/sinol_make/commands/run/__init__.py

+33-12
Original file line numberDiff line numberDiff line change
@@ -315,20 +315,33 @@ def compile_solutions(self, solutions):
315315
os.makedirs(self.COMPILATION_DIR, exist_ok=True)
316316
os.makedirs(self.EXECUTABLES_DIR, exist_ok=True)
317317
print("Compiling %d solutions..." % len(solutions))
318+
args = [(solution, True) for solution in solutions]
318319
with mp.Pool(self.cpus) as pool:
319-
compilation_results = pool.map(self.compile, solutions)
320+
compilation_results = pool.starmap(self.compile, args)
320321
return compilation_results
321322

322323

323-
def compile(self, solution):
324+
def compile(self, solution, use_extras = False):
324325
compile_log_file = os.path.join(
325326
self.COMPILATION_DIR, "%s.compile_log" % package_util.get_file_name(solution))
326327
source_file = os.path.join(os.getcwd(), "prog", self.get_solution_from_exe(solution))
327328
output = os.path.join(self.EXECUTABLES_DIR, package_util.get_executable(solution))
328329

330+
extra_compilation_args = []
331+
extra_compilation_files = []
332+
if use_extras:
333+
if "extra_compilation_args" in self.config:
334+
lang = os.path.splitext(source_file)[1][1:]
335+
for file in self.config["extra_compilation_args"].get(lang, []):
336+
extra_compilation_args.append(os.path.join(os.getcwd(), "prog", file))
337+
338+
for file in self.config.get("extra_compilation_files", []):
339+
extra_compilation_files.append(os.path.join(os.getcwd(), "prog", file))
340+
329341
try:
330-
compile.compile(source_file, output, self.compilers,
331-
open(compile_log_file, "w"), self.args.weak_compilation_flags)
342+
with open(compile_log_file, "w") as compile_log:
343+
compile.compile(source_file, output, self.compilers, compile_log, self.args.weak_compilation_flags,
344+
extra_compilation_args, extra_compilation_files)
332345
print(util.info("Compilation of file %s was successful."
333346
% package_util.get_file_name(solution)))
334347
return True
@@ -376,10 +389,12 @@ def check_output(self, name, input_file, output_file_path, output, answer_file_p
376389
Returns a tuple (is correct, number of points).
377390
"""
378391
if not hasattr(self, "checker") or self.checker is None:
379-
correct = util.lines_diff(output, open(answer_file_path, "r").readlines())
392+
with open(answer_file_path, "r") as answer_file:
393+
correct = util.lines_diff(output, answer_file.readlines())
380394
return correct, 100 if correct else 0
381395
else:
382-
open(output_file_path, "w").write("\n".join(output))
396+
with open(output_file_path, "w") as output_file:
397+
output_file.write("\n".join(output))
383398
return self.check_output_checker(name, input_file, output_file_path, answer_file_path)
384399

385400

@@ -479,7 +494,8 @@ def sigint_handler(signum, frame):
479494
program_exit_code = None
480495
if not timeout:
481496
output = output.decode("utf-8").splitlines()
482-
lines = open(result_file_path).readlines()
497+
with open(result_file_path, "r") as result_file:
498+
lines = result_file.readlines()
483499
if len(lines) == 3:
484500
"""
485501
If programs runs successfully, the output looks like this:
@@ -967,7 +983,8 @@ def check_are_any_tests_to_run(self):
967983
if len(example_tests) == len(self.tests):
968984
print(util.warning('Running only on example tests.'))
969985

970-
self.validate_existence_of_outputs()
986+
if not self.has_lib:
987+
self.validate_existence_of_outputs()
971988
else:
972989
print(util.warning('There are no tests to run.'))
973990

@@ -988,10 +1005,11 @@ def run(self, args):
9881005

9891006
self.set_constants()
9901007
self.args = args
991-
try:
992-
self.config = yaml.load(open("config.yml"), Loader=yaml.FullLoader)
993-
except AttributeError:
994-
self.config = yaml.load(open("config.yml"))
1008+
with open(os.path.join(os.getcwd(), "config.yml"), 'r') as config:
1009+
try:
1010+
self.config = yaml.load(config, Loader=yaml.FullLoader)
1011+
except AttributeError:
1012+
self.config = yaml.load(config)
9951013

9961014
if not 'title' in self.config.keys():
9971015
util.exit_with_error('Title was not defined in config.yml.')
@@ -1033,6 +1051,9 @@ def run(self, args):
10331051
else:
10341052
self.checker = None
10351053

1054+
lib = glob.glob(os.path.join(os.getcwd(), "prog", f'{self.ID}lib.*'))
1055+
self.has_lib = len(lib) != 0
1056+
10361057
self.set_scores()
10371058
self.check_are_any_tests_to_run()
10381059

src/sinol_make/helpers/compile.py

+38-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Tuple
22
import os
33
import sys
4+
import shutil
45
import stat
56
import subprocess
67

@@ -9,33 +10,52 @@
910
from sinol_make.structs.compiler_structs import Compilers
1011

1112

12-
def compile(program, output, compilers: Compilers = None, compile_log = None, weak_compilation_flags = False):
13+
def compile(program, output, compilers: Compilers = None, compile_log = None, weak_compilation_flags = False,
14+
extra_compilation_args = None, extra_compilation_files = None):
1315
"""
14-
Compile a program
15-
compilers - A Compilers object with compilers to use. If None, default compilers will be used.
16+
Compile a program.
17+
:param program: Path to the program to compile
18+
:param output: Path to the output file
19+
:param compilers: Compilers object
20+
:param compile_log: File to write the compilation log to
21+
:param weak_compilation_flags: If True, disable all warnings
22+
:param extra_compilation_args: Extra compilation arguments
23+
:param extra_compilation_files: Extra compilation files
1624
"""
25+
if extra_compilation_args is None:
26+
extra_compilation_args = []
27+
if extra_compilation_files is None:
28+
extra_compilation_files = []
29+
30+
for file in extra_compilation_files:
31+
shutil.copy(file, os.path.join(os.path.dirname(output), os.path.basename(file)))
32+
1733
gcc_compilation_flags = '-Werror -Wall -Wextra -Wshadow -Wconversion -Wno-unused-result -Wfloat-equal'
1834
if weak_compilation_flags:
19-
gcc_compilation_flags = '-w' # Disable all warnings
35+
gcc_compilation_flags = '-w' # Disable all warnings
2036

2137
if compilers is None:
2238
compilers = Compilers()
2339

2440
ext = os.path.splitext(program)[1]
2541
arguments = []
2642
if ext == '.cpp':
27-
arguments = [compilers.cpp_compiler_path or compiler.get_cpp_compiler_path(), program, '-o', output] + \
43+
arguments = [compilers.cpp_compiler_path or compiler.get_cpp_compiler_path(), program] + \
44+
extra_compilation_args + ['-o', output] + \
2845
f'--std=c++17 -O3 -lm {gcc_compilation_flags} -fdiagnostics-color'.split(' ')
2946
elif ext == '.c':
30-
arguments = [compilers.c_compiler_path, program, '-o', output] + \
47+
arguments = [compilers.c_compiler_path or compiler.get_c_compiler_path(), program] + \
48+
extra_compilation_args + ['-o', output] + \
3149
f'--std=c17 -O3 -lm {gcc_compilation_flags} -fdiagnostics-color'.split(' ')
3250
elif ext == '.py':
3351
if sys.platform == 'win32' or sys.platform == 'cygwin':
3452
# TODO: Make this work on Windows
3553
pass
3654
else:
37-
open(output, 'w').write('#!/usr/bin/python3\n')
38-
open(output, 'a').write(open(program, 'r').read())
55+
with open(output, 'w') as output_file, open(program, 'r') as program_file:
56+
output_file.write('#!/usr/bin/python3\n')
57+
output_file.write(program_file.read())
58+
3959
st = os.stat(output)
4060
os.chmod(output, st.st_mode | stat.S_IEXEC)
4161
arguments = [compilers.python_interpreter_path, '-m', 'py_compile', program]
@@ -76,14 +96,12 @@ def compile_file(file_path: str, name: str, compilers: Compilers, weak_compilati
7696

7797
output = os.path.join(executable_dir, name)
7898
compile_log_path = os.path.join(compile_log_dir, os.path.splitext(name)[0] + '.compile_log')
79-
compile_log = open(compile_log_path, 'w')
80-
81-
try:
82-
if compile(file_path, output, compilers, compile_log, weak_compilation_flags):
83-
return output, compile_log_path
84-
else:
85-
return None, compile_log_path
86-
except CompilationError:
99+
with open(compile_log_path, 'w') as compile_log:
100+
try:
101+
if compile(file_path, output, compilers, compile_log, weak_compilation_flags):
102+
return output, compile_log_path
103+
except CompilationError:
104+
pass
87105
return None, compile_log_path
88106

89107

@@ -93,8 +111,7 @@ def print_compile_log(compile_log_path: str):
93111
:param compile_log_path: path to the compilation log
94112
"""
95113

96-
compile_log = open(compile_log_path, 'r')
97-
lines = compile_log.readlines()
98-
compile_log.close()
99-
for line in lines[:500]:
100-
print(line, end='')
114+
with open(compile_log_path, 'r') as compile_log:
115+
lines = compile_log.readlines()
116+
for line in lines[:500]:
117+
print(line, end='')

src/sinol_make/helpers/parsers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ def add_compilation_arguments(parser: argparse.ArgumentParser):
2121
help='Python interpreter to use (default: python3)')
2222
parser.add_argument('--java-compiler-path', dest='java_compiler_path', type=str, default=compiler.get_java_compiler_path(),
2323
help='Java compiler to use (default: javac)')
24-
parser.add_argument('--weak-compilation-flags', dest='weak_compilation_flags', action='store_true',
24+
parser.add_argument('-W', '--weak-compilation-flags', dest='weak_compilation_flags', action='store_true',
2525
help='use weaker compilation flags')

src/sinol_make/util.py

+30-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import glob, importlib, os, sys, subprocess, requests, tarfile, yaml
2+
import tempfile
23
import importlib.resources
34
import threading
45

@@ -79,19 +80,25 @@ def install_oiejq():
7980
raise Exception('Couldn\'t download oiejq (https://oij.edu.pl/zawodnik/srodowisko/oiejq.tar.gz couldn\'t connect)')
8081
if request.status_code != 200:
8182
raise Exception('Couldn\'t download oiejq (https://oij.edu.pl/zawodnik/srodowisko/oiejq.tar.gz returned status code: ' + str(request.status_code) + ')')
82-
open('/tmp/oiejq.tar.gz', 'wb').write(request.content)
8383

84-
def strip(tar):
85-
l = len('oiejq/')
86-
for member in tar.getmembers():
87-
member.name = member.name[l:]
88-
yield member
89-
90-
tar = tarfile.open('/tmp/oiejq.tar.gz')
91-
tar.extractall(path=os.path.expanduser('~/.local/bin'), members=strip(tar))
92-
tar.close()
93-
os.remove('/tmp/oiejq.tar.gz')
94-
os.rename(os.path.expanduser('~/.local/bin/oiejq.sh'), os.path.expanduser('~/.local/bin/oiejq'))
84+
# oiejq is downloaded to a temporary directory and not to the `cache` dir,
85+
# as there is no guarantee that the current directory is the package directory.
86+
# The `cache` dir is only used for files that are part of the package and those
87+
# that the package creator might want to look into.
88+
with tempfile.TemporaryDirectory() as tmpdir:
89+
oiejq_path = os.path.join(tmpdir, 'oiejq.tar.gz')
90+
with open(oiejq_path, 'wb') as oiejq_file:
91+
oiejq_file.write(request.content)
92+
93+
def strip(tar):
94+
l = len('oiejq/')
95+
for member in tar.getmembers():
96+
member.name = member.name[l:]
97+
yield member
98+
99+
with tarfile.open(oiejq_path) as tar:
100+
tar.extractall(path=os.path.expanduser('~/.local/bin'), members=strip(tar))
101+
os.rename(os.path.expanduser('~/.local/bin/oiejq.sh'), os.path.expanduser('~/.local/bin/oiejq'))
95102

96103
return check_oiejq()
97104

@@ -131,7 +138,14 @@ def save_config(config):
131138
"time_limits",
132139
"override_limits",
133140
"scores",
134-
"extra_compilation_files",
141+
{
142+
"key": "extra_compilation_files",
143+
"default_flow_style": None
144+
},
145+
{
146+
"key": "extra_compilation_args",
147+
"default_flow_style": None
148+
},
135149
{
136150
"key": "sinol_expected_scores",
137151
"default_flow_style": None
@@ -240,12 +254,13 @@ def lines_diff(lines1, lines2):
240254
return True
241255

242256

243-
def file_diff(file1, file2):
257+
def file_diff(file1_path, file2_path):
244258
"""
245259
Function to compare two files.
246260
Returns True if they are the same, False otherwise.
247261
"""
248-
return lines_diff(open(file1).readlines(), open(file2).readlines())
262+
with open(file1_path) as file1, open(file2_path) as file2:
263+
return lines_diff(file1.readlines(), file2.readlines())
249264

250265

251266
def get_terminal_size():

tests/commands/run/test_integration.py

+23-13
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55

66
@pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(),
7-
get_checker_package_path()], indirect=True)
7+
get_checker_package_path(), get_library_package_path()], indirect=True)
88
def test_simple(create_package, time_tool):
99
"""
1010
Test a simple run.
@@ -20,7 +20,7 @@ def test_simple(create_package, time_tool):
2020

2121

2222
@pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(),
23-
get_checker_package_path()], indirect=True)
23+
get_checker_package_path(), get_library_package_path()], indirect=True)
2424
def test_no_expected_scores(capsys, create_package, time_tool):
2525
"""
2626
Test with no sinol_expected_scores in config.yml.
@@ -31,9 +31,11 @@ def test_no_expected_scores(capsys, create_package, time_tool):
3131
create_ins_outs(package_path)
3232

3333
config_path = os.path.join(package_path, "config.yml")
34-
config = yaml.load(open(config_path, "r"), Loader=yaml.SafeLoader)
34+
with open(config_path, "r") as config_file:
35+
config = yaml.load(config_file, Loader=yaml.SafeLoader)
3536
del config["sinol_expected_scores"]
36-
open(config_path, "w").write(yaml.dump(config))
37+
with open(config_path, "w") as config_file:
38+
config_file.write(yaml.dump(config))
3739

3840
parser = configure_parsers()
3941
args = parser.parse_args(["run", "--time-tool", time_tool])
@@ -51,7 +53,7 @@ def test_no_expected_scores(capsys, create_package, time_tool):
5153

5254

5355
@pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(),
54-
get_checker_package_path()], indirect=True)
56+
get_checker_package_path(), get_library_package_path()], indirect=True)
5557
def test_apply_suggestions(create_package, time_tool):
5658
"""
5759
Test with no sinol_expected_scores in config.yml.
@@ -62,17 +64,20 @@ def test_apply_suggestions(create_package, time_tool):
6264
create_ins_outs(package_path)
6365

6466
config_path = os.path.join(package_path, "config.yml")
65-
config = yaml.load(open(config_path, "r"), Loader=yaml.SafeLoader)
67+
with open(config_path, "r") as config_file:
68+
config = yaml.load(config_file, Loader=yaml.SafeLoader)
6669
expected_scores = config["sinol_expected_scores"]
6770
del config["sinol_expected_scores"]
68-
open(config_path, "w").write(yaml.dump(config))
71+
with open(config_path, "w") as config_file:
72+
config_file.write(yaml.dump(config))
6973

7074
parser = configure_parsers()
7175
args = parser.parse_args(["run", "--apply-suggestions", "--time-tool", time_tool])
7276
command = Command()
7377
command.run(args)
7478

75-
config = yaml.load(open(config_path, "r"), Loader=yaml.SafeLoader)
79+
with open(config_path, "r") as config_file:
80+
config = yaml.load(config_file, Loader=yaml.SafeLoader)
7681
assert config["sinol_expected_scores"] == expected_scores
7782

7883

@@ -86,10 +91,12 @@ def test_incorrect_expected_scores(capsys, create_package, time_tool):
8691
create_ins_outs(package_path)
8792

8893
config_path = os.path.join(package_path, "config.yml")
89-
config = yaml.load(open(config_path, "r"), Loader=yaml.SafeLoader)
94+
with open(config_path, "r") as config_file:
95+
config = yaml.load(config_file, Loader=yaml.SafeLoader)
9096
config["sinol_expected_scores"]["abc.cpp"]["expected"][1] = "WA"
9197
config["sinol_expected_scores"]["abc.cpp"]["points"] = 75
92-
open(config_path, "w").write(yaml.dump(config))
98+
with open(config_path, "w") as config_file:
99+
config_file.write(yaml.dump(config))
93100

94101
parser = configure_parsers()
95102
args = parser.parse_args(["run", "--time-tool", time_tool])
@@ -105,7 +112,8 @@ def test_incorrect_expected_scores(capsys, create_package, time_tool):
105112
assert "Solution abc.cpp passed group 1 with status OK while it should pass with status WA." in out
106113

107114

108-
@pytest.mark.parametrize("create_package", [get_simple_package_path(), get_checker_package_path()], indirect=True)
115+
@pytest.mark.parametrize("create_package", [get_simple_package_path(), get_checker_package_path(),
116+
get_library_package_path()], indirect=True)
109117
def test_flag_tests(create_package, time_tool):
110118
"""
111119
Test flag --tests.
@@ -178,9 +186,11 @@ def test_no_scores(capsys, create_package, time_tool):
178186
create_ins_outs(package_path)
179187

180188
config_path = os.path.join(package_path, "config.yml")
181-
config = yaml.load(open(config_path, "r"), Loader=yaml.SafeLoader)
189+
with open(config_path, "r") as config_file:
190+
config = yaml.load(config_file, Loader=yaml.SafeLoader)
182191
del config["scores"]
183-
open(config_path, "w").write(yaml.dump(config))
192+
with open(config_path, "w") as config_file:
193+
config_file.write(yaml.dump(config))
184194

185195
parser = configure_parsers()
186196
args = parser.parse_args(["run", "--time-tool", time_tool])

0 commit comments

Comments
 (0)