Skip to content

Commit 1b16738

Browse files
authored
Bug fixes for making changes after updating sinol-make (#131)
* Fix changing version breaking config * Delete cached tests after contest type change * Add tests * Add tests for removing cache after contest type change * Remove debug * Remove version changes, catch errors while validating expected scores * Add tests * Add TotalPointsChange in validating expected scores * Refactor * Add description for `try_fix_config` function * Refactor * Bump version for release
1 parent 13351c2 commit 1b16738

File tree

12 files changed

+260
-88
lines changed

12 files changed

+260
-88
lines changed

src/sinol_make/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from sinol_make import util, oiejq
1010

1111

12-
__version__ = "1.5.9"
12+
__version__ = "1.5.10"
1313

1414

1515
def configure_parsers():
@@ -61,7 +61,6 @@ def main_exn():
6161
except Exception as err:
6262
util.exit_with_error('`oiejq` could not be installed.\n' + err)
6363

64-
util.make_version_changes()
6564
command.run(args)
6665
exit(0)
6766

src/sinol_make/commands/run/__init__.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
from sinol_make.interfaces.BaseCommand import BaseCommand
1818
from sinol_make.interfaces.Errors import CompilationError, CheckerOutputException, UnknownContestType
1919
from sinol_make.helpers import compile, compiler, package_util, printer, paths, cache
20-
from sinol_make.structs.status_structs import Status, ResultChange, PointsChange, ValidationResult, ExecutionResult
20+
from sinol_make.structs.status_structs import Status, ResultChange, PointsChange, ValidationResult, ExecutionResult, \
21+
TotalPointsChange
2122
import sinol_make.util as util
2223
import yaml, os, collections, sys, re, math, dictdiffer
2324
import multiprocessing as mp
@@ -827,6 +828,7 @@ def validate_expected_scores(self, results):
827828
added_groups = set()
828829
removed_groups = set()
829830
changes = []
831+
unknown_change = False
830832

831833
for type, field, change in list(expected_scores_diff):
832834
if type == "add":
@@ -877,6 +879,16 @@ def validate_expected_scores(self, results):
877879
old_result=change[0],
878880
result=change[1]
879881
))
882+
elif field[1] == "points": # Points for at least one solution has changed
883+
solution = field[0]
884+
changes.append(TotalPointsChange(
885+
solution=solution,
886+
old_points=change[0],
887+
new_points=change[1]
888+
))
889+
else:
890+
unknown_change = True
891+
880892

881893
return ValidationResult(
882894
added_solutions,
@@ -885,14 +897,19 @@ def validate_expected_scores(self, results):
885897
removed_groups,
886898
changes,
887899
expected_scores,
888-
new_expected_scores
900+
new_expected_scores,
901+
unknown_change,
889902
)
890903

891904

892905
def print_expected_scores_diff(self, validation_results: ValidationResult):
893906
diff = validation_results
894907
config_expected_scores = self.config.get("sinol_expected_scores", {})
895908

909+
if diff.unknown_change:
910+
print(util.error("There was an unknown change in expected scores. "
911+
"You should apply the suggested changes to avoid errors."))
912+
896913
def warn_if_not_empty(set, message):
897914
if len(set) > 0:
898915
print(util.warning(message + ": "), end='')
@@ -916,8 +933,11 @@ def print_points_change(solution, group, new_points, old_points):
916933
print_points_change(change.solution, change.group, change.result, change.old_result)
917934
elif isinstance(change, PointsChange):
918935
print_points_change(change.solution, change.group, change.new_points, change.old_points)
936+
elif isinstance(change, TotalPointsChange):
937+
print(util.warning("Solution %s passed all groups with %d points while it should pass with %d points." %
938+
(change.solution, change.new_points, change.old_points)))
919939

920-
if diff.expected_scores == diff.new_expected_scores:
940+
if diff.expected_scores == diff.new_expected_scores and not diff.unknown_change:
921941
print(util.info("Expected scores are correct!"))
922942
else:
923943
def delete_group(solution, group):
@@ -935,7 +955,6 @@ def set_group_result(solution, group, result):
935955
self.possible_score
936956
)
937957

938-
939958
if self.args.apply_suggestions:
940959
for solution in diff.removed_solutions:
941960
del config_expected_scores[solution]
@@ -951,7 +970,6 @@ def set_group_result(solution, group, result):
951970
else:
952971
config_expected_scores[solution] = diff.new_expected_scores[solution]
953972

954-
955973
self.config["sinol_expected_scores"] = self.convert_status_to_string(config_expected_scores)
956974
util.save_config(self.config)
957975
print(util.info("Saved suggested expected scores description."))
@@ -1129,6 +1147,7 @@ def run(self, args):
11291147
print("Task: %s (tag: %s)" % (title, self.ID))
11301148
self.cpus = args.cpus or mp.cpu_count()
11311149
cache.save_to_cache_extra_compilation_files(self.config.get("extra_compilation_files", []), self.ID)
1150+
cache.remove_results_if_contest_type_changed(self.config.get("sinol_contest_type", "default"))
11321151

11331152
checker = package_util.get_files_matching_pattern(self.ID, f'{self.ID}chk.*')
11341153
if len(checker) != 0:
@@ -1158,6 +1177,15 @@ def run(self, args):
11581177

11591178
results, all_results = self.compile_and_run(solutions)
11601179
self.check_errors(all_results)
1161-
validation_results = self.validate_expected_scores(results)
1180+
try:
1181+
validation_results = self.validate_expected_scores(results)
1182+
except:
1183+
self.config = util.try_fix_config(self.config)
1184+
try:
1185+
validation_results = self.validate_expected_scores(results)
1186+
except:
1187+
util.exit_with_error("Validating expected scores failed. "
1188+
"This probably means that `sinol_expected_scores` is broken. "
1189+
"Delete it and run `sinol-make run --apply-suggestions` again.")
11621190
self.print_expected_scores_diff(validation_results)
11631191
self.exit()

src/sinol_make/helpers/cache.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,7 @@ def save_compiled(file_path: str, exe_path: str, is_checker: bool = False):
6767
info.save(file_path)
6868

6969
if is_checker:
70-
for solution in os.listdir(paths.get_cache_path('md5sums')):
71-
info = get_cache_file(solution)
72-
info.tests = {}
73-
info.save(solution)
70+
remove_results_cache()
7471

7572

7673
def save_to_cache_extra_compilation_files(extra_compilation_files, task_id):
@@ -100,3 +97,23 @@ def save_to_cache_extra_compilation_files(extra_compilation_files, task_id):
10097

10198
info.md5sum = md5sum
10299
info.save(file_path)
100+
101+
102+
def remove_results_cache():
103+
"""
104+
Removes all cached test results
105+
"""
106+
for solution in os.listdir(paths.get_cache_path('md5sums')):
107+
info = get_cache_file(solution)
108+
info.tests = {}
109+
info.save(solution)
110+
111+
112+
def remove_results_if_contest_type_changed(contest_type):
113+
"""
114+
Checks if contest type has changed and removes all cached test results if it has.
115+
:param contest_type: Contest type
116+
"""
117+
if package_util.check_if_contest_type_changed(contest_type):
118+
remove_results_cache()
119+
package_util.save_contest_type_to_cache(contest_type)

src/sinol_make/helpers/package_util.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,26 @@ def any_files_matching_pattern(task_id: str, pattern: str) -> bool:
294294
:return: True if any file in package matches given pattern.
295295
"""
296296
return len(get_files_matching_pattern(task_id, pattern)) > 0
297+
298+
299+
def check_if_contest_type_changed(contest_type):
300+
"""
301+
Checks if contest type in cache is different then contest type specified in config.yml.
302+
:param contest_type: Contest type specified in config.yml.
303+
:return: True if contest type in cache is different then contest type specified in config.yml.
304+
"""
305+
if not os.path.isfile(paths.get_cache_path("contest_type")):
306+
return False
307+
with open(paths.get_cache_path("contest_type"), "r") as contest_type_file:
308+
cached_contest_type = contest_type_file.read()
309+
return cached_contest_type != contest_type
310+
311+
312+
def save_contest_type_to_cache(contest_type):
313+
"""
314+
Saves contest type to cache.
315+
:param contest_type: Contest type.
316+
"""
317+
os.makedirs(paths.get_cache_path(), exist_ok=True)
318+
with open(paths.get_cache_path("contest_type"), "w") as contest_type_file:
319+
contest_type_file.write(contest_type)

src/sinol_make/structs/status_structs.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ def from_str(status):
3737
else:
3838
raise ValueError(f"Unknown status: '{status}'")
3939

40+
@staticmethod
41+
def possible_statuses():
42+
return [Status.PENDING, Status.CE, Status.TL, Status.ML, Status.RE, Status.WA, Status.OK]
43+
4044

4145
@dataclass
4246
class ResultChange:
@@ -46,6 +50,12 @@ class ResultChange:
4650
result: Status
4751

4852

53+
@dataclass
54+
class TotalPointsChange:
55+
solution: str
56+
old_points: int
57+
new_points: int
58+
4959
@dataclass
5060
class PointsChange:
5161
solution: str
@@ -63,6 +73,7 @@ class ValidationResult:
6373
changes: List[ResultChange]
6474
expected_scores: dict
6575
new_expected_scores: dict
76+
unknown_change: bool
6677

6778

6879
@dataclass

src/sinol_make/util.py

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import sinol_make
1212
from sinol_make.contest_types import get_contest_type
13+
from sinol_make.structs.status_structs import Status
1314

1415

1516
def get_commands():
@@ -87,7 +88,7 @@ def save_config(config):
8788
{
8889
"key": "sinol_expected_scores",
8990
"default_flow_style": None
90-
}
91+
},
9192
]
9293

9394
config = config.copy()
@@ -293,41 +294,56 @@ def get_file_md5(path):
293294
return hashlib.md5(f.read()).hexdigest()
294295

295296

296-
def make_version_changes():
297-
if compare_versions(sinol_make.__version__, "1.5.8") == 1:
298-
# In version 1.5.9 we changed the format of sinol_expected_scores.
299-
# Now all groups have specified points and status.
300-
301-
if find_and_chdir_package():
302-
with open("config.yml", "r") as config_file:
303-
config = yaml.load(config_file, Loader=yaml.FullLoader)
304-
305-
try:
306-
new_expected_scores = {}
307-
expected_scores = config["sinol_expected_scores"]
308-
contest = get_contest_type()
309-
groups = []
310-
for solution, results in expected_scores.items():
311-
for group in results["expected"].keys():
312-
if group not in groups:
313-
groups.append(int(group))
314-
315-
scores = contest.assign_scores(groups)
316-
for solution, results in expected_scores.items():
317-
new_expected_scores[solution] = {"expected": {}, "points": results["points"]}
318-
for group, result in results["expected"].items():
319-
new_expected_scores[solution]["expected"][group] = {"status": result}
320-
if result == "OK":
321-
new_expected_scores[solution]["expected"][group]["points"] = scores[group]
322-
else:
323-
new_expected_scores[solution]["expected"][group]["points"] = 0
324-
config["sinol_expected_scores"] = new_expected_scores
325-
save_config(config)
326-
except:
327-
# If there is an error, we just delete the field.
328-
if "sinol_expected_scores" in config:
329-
del config["sinol_expected_scores"]
330-
save_config(config)
297+
def try_fix_config(config):
298+
"""
299+
Function to try to fix the config.yml file.
300+
Tries to:
301+
- reformat `sinol_expected_scores` field
302+
:param config: config.yml file as a dict
303+
:return: config.yml file as a dict
304+
"""
305+
# The old format was:
306+
# sinol_expected_scores:
307+
# solution1:
308+
# expected: {1: OK, 2: OK, ...}
309+
# points: 100
310+
#
311+
# We change it to:
312+
# sinol_expected_scores:
313+
# solution1:
314+
# expected: {1: {status: OK, points: 100}, 2: {status: OK, points: 100}, ...}
315+
# points: 100
316+
try:
317+
new_expected_scores = {}
318+
expected_scores = config["sinol_expected_scores"]
319+
contest = get_contest_type()
320+
groups = []
321+
for solution, results in expected_scores.items():
322+
for group in results["expected"].keys():
323+
if group not in groups:
324+
groups.append(int(group))
325+
326+
scores = contest.assign_scores(groups)
327+
for solution, results in expected_scores.items():
328+
new_expected_scores[solution] = {"expected": {}, "points": results["points"]}
329+
for group, result in results["expected"].items():
330+
if result in Status.possible_statuses():
331+
new_expected_scores[solution]["expected"][group] = {"status": result}
332+
if result == "OK":
333+
new_expected_scores[solution]["expected"][group]["points"] = scores[group]
334+
else:
335+
new_expected_scores[solution]["expected"][group]["points"] = 0
336+
else:
337+
# This means that the result is probably valid.
338+
new_expected_scores[solution]["expected"][group] = result
339+
config["sinol_expected_scores"] = new_expected_scores
340+
save_config(config)
341+
except:
342+
# If there is an error, we just delete the field.
343+
if "sinol_expected_scores" in config:
344+
del config["sinol_expected_scores"]
345+
save_config(config)
346+
return config
331347

332348

333349
def color_red(text): return "\033[91m{}\033[00m".format(text)

tests/commands/run/test_integration.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,20 @@ def test_simple(create_package, time_tool):
2222
"""
2323
package_path = create_package
2424
create_ins_outs(package_path)
25-
2625
parser = configure_parsers()
2726

27+
with open(os.path.join(os.getcwd(), "config.yml"), "r") as config_file:
28+
config = yaml.load(config_file, Loader=yaml.SafeLoader)
29+
expected_scores = config["sinol_expected_scores"]
30+
2831
args = parser.parse_args(["run", "--time-tool", time_tool])
2932
command = Command()
3033
command.run(args)
3134

35+
with open(os.path.join(os.getcwd(), "config.yml"), "r") as config_file:
36+
config = yaml.load(config_file, Loader=yaml.SafeLoader)
37+
assert config["sinol_expected_scores"] == expected_scores
38+
3239

3340
@pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(),
3441
get_checker_package_path(), get_library_package_path(),
@@ -634,6 +641,43 @@ def test(file_to_change, lang, comment_character):
634641
test("liblib.py", "py", "#")
635642

636643

644+
@pytest.mark.parametrize("create_package", [get_simple_package_path()], indirect=True)
645+
def test_contest_type_change(create_package, time_tool):
646+
"""
647+
Test if after changing contest type, all cached test results are removed.
648+
"""
649+
package_path = create_package
650+
create_ins_outs(package_path)
651+
parser = configure_parsers()
652+
args = parser.parse_args(["run", "--time-tool", time_tool])
653+
command = Command()
654+
655+
# First run to cache test results.
656+
command.run(args)
657+
658+
# Change contest type.
659+
config_path = os.path.join(os.getcwd(), "config.yml")
660+
with open(config_path, "r") as f:
661+
config = yaml.load(f, Loader=yaml.SafeLoader)
662+
config["sinol_contest_type"] = "oi"
663+
with open(config_path, "w") as f:
664+
f.write(yaml.dump(config))
665+
666+
# Compile checker check if test results are removed.
667+
command = Command()
668+
# We remove tests, so that `run()` exits before creating new cached test results.
669+
for test in glob.glob("in/*.in"):
670+
os.unlink(test)
671+
with pytest.raises(SystemExit):
672+
command.run(args)
673+
674+
task_id = package_util.get_task_id()
675+
solutions = package_util.get_solutions(task_id, None)
676+
for solution in solutions:
677+
cache_file: CacheFile = cache.get_cache_file(solution)
678+
assert cache_file.tests == {}
679+
680+
637681
@pytest.mark.parametrize("create_package", [get_simple_package_path()], indirect=True)
638682
def test_cwd_in_prog(create_package):
639683
"""

0 commit comments

Comments
 (0)