Skip to content

Support for interactive tasks via IO #233

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/sinol_make/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from sinol_make import util, oiejq

__version__ = "1.5.29"
__version__ = "1.7.0.dev1"


def configure_parsers():
Expand Down
70 changes: 28 additions & 42 deletions src/sinol_make/commands/run/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
import signal
import threading
import time
import traceback
import psutil
import glob
import shutil
from io import StringIO
from typing import Dict

from sinol_make import contest_types, oiejq
from sinol_make import contest_types, oiejq, task_type
from sinol_make.structs.run_structs import ExecutionData, PrintData
from sinol_make.structs.cache_structs import CacheTest, CacheFile
from sinol_make.helpers.parsers import add_compilation_arguments
Expand Down Expand Up @@ -357,6 +358,8 @@ def compile_solutions(self, solutions, is_checker=False):


def compile(self, solution, use_extras = False, is_checker = False):
os.makedirs(paths.get_compilation_log_path(), exist_ok=True)
os.makedirs(paths.get_executables_path(), exist_ok=True)
compile_log_file = paths.get_compilation_log_path("%s.compile_log" % package_util.get_file_name(solution))
source_file = os.path.join(os.getcwd(), "prog", self.get_solution_from_exe(solution))
output = paths.get_executables_path(package_util.get_executable(solution))
Expand Down Expand Up @@ -506,7 +509,8 @@ def sigint_handler(signum, frame):
result.Status = Status.ML
else:
try:
correct, result.Points = self.check_output(name, input_file_path, output_file_path, output, answer_file_path)
correct, result.Points = self.task_type.check_output(input_file_path, output_file_path,
output, answer_file_path)
if not correct:
result.Status = Status.WA
except CheckerOutputException as e:
Expand Down Expand Up @@ -613,8 +617,8 @@ def sigint_handler(signum, frame):
result.Status = Status.ML
else:
try:
correct, result.Points = self.check_output(name, input_file_path, output_file_path, output,
answer_file_path)
correct, result.Points = self.task_type.check_output(input_file_path, output_file_path,
output, answer_file_path)
if correct:
result.Status = Status.OK
else:
Expand All @@ -631,18 +635,16 @@ def run_solution(self, data_for_execution: ExecutionData):
Run an execution and return the result as ExecutionResult object.
"""

(name, executable, test, time_limit, memory_limit, timetool_path, execution_dir) = data_for_execution
(name, executable, test, output_test, time_limit, memory_limit, timetool_path, execution_dir) = data_for_execution
file_no_ext = paths.get_executions_path(name, package_util.extract_test_id(test, self.ID))
output_file = file_no_ext + ".out"
result_file = file_no_ext + ".res"
hard_time_limit_in_s = math.ceil(2 * time_limit / 1000.0)
hard_time_limit = math.ceil(2 * time_limit / 1000.0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this 2 times time limit in any way tested to be enough? I have an example, where sio2jail runs 1.5 times longer than time measured by the jail. If not, given "worse" program or slower machine, this hard time limit may be exceeded, without exceeding time_limit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to work well in real sioworkers. But I guess on some systems it may fail

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be another discrepancy between upstream sioworkers and those on sio2.mimuw, but the first repo contains a multiplier of 16 with an additional second, see here and here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I didn't know about this. On sio2.mimuw we also have 16x time limit for sio2jail


oiejq = self.timetool_name == 'oiejq'
return self.task_type.run(oiejq, timetool_path, executable, result_file, os.path.join(os.getcwd(), test), output_file,
output_test, time_limit, memory_limit, hard_time_limit, execution_dir)

if self.timetool_name == 'oiejq':
return self.execute_oiejq(name, timetool_path, executable, result_file, test, output_file, self.get_output_file(test),
time_limit, memory_limit, hard_time_limit_in_s, execution_dir)
elif self.timetool_name == 'time':
return self.execute_time(name, executable, result_file, test, output_file, self.get_output_file(test),
time_limit, memory_limit, hard_time_limit_in_s, execution_dir)

def run_solutions(self, compiled_commands, names, solutions, executables_dir):
"""
Expand Down Expand Up @@ -677,8 +679,9 @@ def run_solutions(self, compiled_commands, names, solutions, executables_dir):
test_result.time_tool == self.timetool_name:
all_results[name][self.get_group(test)][test] = test_result.result
else:
executions.append((name, executable, test, test_time_limit, test_memory_limit,
self.timetool_path, os.path.dirname(executable)))
executions.append((name, executable, test,
os.path.join(os.getcwd(), self.get_output_file(test)), test_time_limit,
test_memory_limit, self.timetool_path, os.path.dirname(executable)))
all_results[name][self.get_group(test)][test] = ExecutionResult(Status.PENDING)
os.makedirs(paths.get_executions_path(name), exist_ok=True)
else:
Expand Down Expand Up @@ -800,7 +803,7 @@ def validate_expected_scores(self, results):
if group not in self.scores:
util.exit_with_error(f'Group {group} doesn\'t have points specified in config file.')

if self.checker is None:
if self.task_type.has_checker():
for solution in results.keys():
new_expected_scores[solution] = {
"expected": results[solution],
Expand Down Expand Up @@ -1025,6 +1028,7 @@ def set_constants(self):
self.ID = package_util.get_task_id()
self.SOURCE_EXTENSIONS = ['.c', '.cpp', '.py', '.java']
self.SOLUTIONS_RE = package_util.get_solutions_re(self.ID)
self.task_type = task_type.get_task_type()


def validate_arguments(self, args):
Expand Down Expand Up @@ -1155,7 +1159,7 @@ def check_errors(self, results: Dict[str, Dict[str, Dict[str, ExecutionResult]]]
if results[solution][group][test].Error is not None:
error_msg += f'Solution {solution} had an error on test {test}: {results[solution][group][test].Error}\n'
if error_msg != "":
util.exit_with_error(error_msg)
print(util.error(error_msg))

def compile_checker(self):
checker_basename = os.path.basename(self.checker)
Expand All @@ -1165,25 +1169,13 @@ def compile_checker(self):
if not checker_compilation[0]:
util.exit_with_error('Checker compilation failed.')

def check_had_checker(self, has_checker):
"""
Checks if there was a checker and if it is now removed (or the other way around) and if so, removes tests cache.
In theory, removing cache after adding a checker is redundant, because during its compilation, the cache is
removed.
"""
had_checker = os.path.exists(paths.get_cache_path("checker"))
if (had_checker and not has_checker) or (not had_checker and has_checker):
cache.remove_results_cache()
if has_checker:
with open(paths.get_cache_path("checker"), "w") as f:
f.write("")
else:
try:
os.remove(paths.get_cache_path("checker"))
except FileNotFoundError:
pass


def compile_additional_files(self, files):
for name, args, kwargs in files:
print(f'Compiling {name}...')
self.compile(*args, **kwargs)

def run(self, args):
args = util.init_package_command(args)

Expand All @@ -1208,14 +1200,7 @@ def run(self, args):
cache.process_extra_execution_files(self.config.get("extra_execution_files", {}), self.ID)
cache.remove_results_if_contest_type_changed(self.config.get("sinol_contest_type", "default"))

checker = package_util.get_files_matching_pattern(self.ID, f'{self.ID}chk.*')
if len(checker) != 0:
print(util.info("Checker found: %s" % os.path.basename(checker[0])))
self.checker = checker[0]
self.compile_checker()
else:
self.checker = None
self.check_had_checker(self.checker is not None)
self.compile_additional_files(self.task_type.get_files_to_compile())

lib = package_util.get_files_matching_pattern(self.ID, f'{self.ID}lib.*')
self.has_lib = len(lib) != 0
Expand Down Expand Up @@ -1243,7 +1228,8 @@ def run(self, args):
self.config = util.try_fix_config(self.config)
try:
validation_results = self.validate_expected_scores(results)
except Exception:
except Exception as e:
print(traceback.format_exc())
util.exit_with_error("Validating expected scores failed. "
"This probably means that `sinol_expected_scores` is broken. "
"Delete it and run `sinol-make run --apply-suggestions` again.")
Expand Down
3 changes: 1 addition & 2 deletions src/sinol_make/helpers/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ def compile(program, output, compilers: Compilers = None, compile_log=None, comp


def compile_file(file_path: str, name: str, compilers: Compilers, compilation_flags='default',
use_fsanitize=False, additional_flags=None) \
-> Tuple[Union[str, None], str]:
use_fsanitize=False, additional_flags=None) -> Tuple[Union[str, None], str]:
"""
Compile a file
:param file_path: Path to the file to compile
Expand Down
1 change: 1 addition & 0 deletions src/sinol_make/helpers/package_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from sinol_make import util
from sinol_make.helpers import paths
from sinol_make.structs.package_structs import TaskType


def get_task_id() -> str:
Expand Down
7 changes: 7 additions & 0 deletions src/sinol_make/structs/package_structs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from enum import Enum


class TaskType(Enum):
NORMAL = 1
INTERACTIVE_IO = 2
ENCDEC = 3
2 changes: 2 additions & 0 deletions src/sinol_make/structs/run_structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class ExecutionData:
executable: str
# Filename of the test
test: str
#Filename of the output file
output_test: str
# Time limit for this test in milliseconds
time_limit: int
# Memory limit in KB
Expand Down
17 changes: 17 additions & 0 deletions src/sinol_make/task_type/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import os

from sinol_make.helpers import package_util
from sinol_make.task_type.base import BaseTaskType
from sinol_make.task_type.interactive_io import InteractiveIOTask
from sinol_make.task_type.normal import NormalTask


def get_task_type() -> BaseTaskType:
if 'encdec' in os.listdir(os.getcwd()):
# Encdec is not actually supported by sinol-make, as it isn't yet merged in OIOIOI.
# (And probably never will)
raise NotImplementedError("Encdec is not supported by sinol-make.")
task_id = package_util.get_task_id()
if package_util.any_files_matching_pattern(task_id, f"{task_id}soc.*"):
return InteractiveIOTask(task_id)
return NormalTask(task_id)
Loading