Skip to content

Commit

Permalink
Refactor and cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
MasloMaslane committed May 30, 2024
1 parent b992a7f commit b1093d7
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 321 deletions.
263 changes: 0 additions & 263 deletions src/sinol_make/commands/run/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,16 +310,6 @@ def configure_subparser(self, subparser):
parsers.add_compilation_arguments(parser)
return parser

def parse_time(self, time_str):
if len(time_str) < 3: return -1
return int(time_str[:-2])


def parse_memory(self, memory_str):
if len(memory_str) < 3: return -1
return int(memory_str[:-2])


def extract_file_name(self, file_path):
return os.path.split(file_path)[1]

Expand All @@ -335,9 +325,6 @@ def get_solution_from_exe(self, executable):
return file + ext
util.exit_with_error("Source file not found for executable %s" % executable)

def get_executables(self, args_solutions):
return [package_util.get_executable(solution) for solution in package_util.get_solutions(self.ID, args_solutions)]


def get_possible_score(self, groups):
possible_score = 0
Expand Down Expand Up @@ -397,246 +384,6 @@ def compile(self, solution, use_extras=False, remove_all_cache=False):
compile.print_compile_log(compile_log_file)
return False

def check_output_diff(self, output_file, answer_file):
"""
Checks whether the output file and the answer file are the same.
"""
return util.file_diff(output_file, answer_file)

def check_output_checker(self, name, input_file, output_file, answer_file):
"""
Checks if the output file is correct with the checker.
Returns True if the output file is correct, False otherwise and number of points.
"""
command = [self.checker_executable, input_file, output_file, answer_file]
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
process.wait()
checker_output = process.communicate()[0].decode("utf-8").splitlines()

if len(checker_output) == 0:
raise CheckerOutputException("Checker output is empty.")

if checker_output[0].strip() == "OK":
points = 100
if len(checker_output) >= 3:
try:
points = int(checker_output[2].strip())
except ValueError:
pass

return True, points
else:
return False, 0


def check_output(self, name, input_file, output_file_path, output, answer_file_path):
"""
Checks if the output file is correct.
Returns a tuple (is correct, number of points).
"""
try:
has_checker = self.checker is not None
except AttributeError:
has_checker = False

if not has_checker:
with open(answer_file_path, "r") as answer_file:
correct = util.lines_diff(output, answer_file.readlines())
return correct, 100 if correct else 0
else:
with open(output_file_path, "w") as output_file:
output_file.write("\n".join(output) + "\n")
return self.check_output_checker(name, input_file, output_file_path, answer_file_path)

def execute_oiejq(self, name, timetool_path, executable, result_file_path, input_file_path, output_file_path, answer_file_path,
time_limit, memory_limit, hard_time_limit, execution_dir):
command = f'"{timetool_path}" "{executable}"'
env = os.environ.copy()
env["MEM_LIMIT"] = f'{memory_limit}K'
env["MEASURE_MEM"] = "1"
env["UNDER_OIEJQ"] = "1"

timeout = False
with open(input_file_path, "r") as input_file, open(output_file_path, "w") as output_file, \
open(result_file_path, "w") as result_file:
process = subprocess.Popen(command, shell=True, stdin=input_file, stdout=output_file,
stderr=result_file, env=env, preexec_fn=os.setsid, cwd=execution_dir)

def sigint_handler(signum, frame):
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
except ProcessLookupError:
pass
sys.exit(1)
signal.signal(signal.SIGINT, sigint_handler)

try:
process.wait(timeout=hard_time_limit)
except subprocess.TimeoutExpired:
timeout = True
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
except ProcessLookupError:
pass
process.communicate()

with open(result_file_path, "r") as result_file:
lines = result_file.read()
with open(output_file_path, "r") as output_file:
output = output_file.read()
result = ExecutionResult()

if not timeout:
lines = lines.splitlines()
output = output.splitlines()

for line in lines:
line = line.strip()
if ": " in line:
(key, value) = line.split(": ")[:2]
if key == "Time":
result.Time = self.parse_time(value)
elif key == "Memory":
result.Memory = self.parse_memory(value)
else:
setattr(result, key, value)

if timeout:
result.Status = Status.TL
elif getattr(result, "Time") is not None and result.Time > time_limit:
result.Status = Status.TL
elif getattr(result, "Memory") is not None and result.Memory > memory_limit:
result.Status = Status.ML
elif getattr(result, "Status") is None:
result.Status = Status.RE
elif result.Status == "OK": # Here OK is a string, because it is set while parsing oiejq's output.
if result.Time > time_limit:
result.Status = Status.TL
elif result.Memory > memory_limit:
result.Status = Status.ML
else:
try:
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:
result.Status = Status.CE
result.Error = e.message
else:
result.Status = result.Status[:2]

return result


def execute_time(self, name, executable, result_file_path, input_file_path, output_file_path, answer_file_path,
time_limit, memory_limit, hard_time_limit, execution_dir):
if sys.platform == 'darwin':
time_name = 'gtime'
elif sys.platform == 'linux':
time_name = 'time'
elif sys.platform == 'win32' or sys.platform == 'cygwin':
raise Exception("Measuring time with GNU time on Windows is not supported.")

command = [f'{time_name}', '-f', '%U\\n%M\\n%x', '-o', result_file_path, executable]
timeout = False
mem_limit_exceeded = False
with open(input_file_path, "r") as input_file, open(output_file_path, "w") as output_file:
process = subprocess.Popen(command, stdin=input_file, stdout=output_file, stderr=subprocess.DEVNULL,
preexec_fn=os.setsid, cwd=execution_dir)

def sigint_handler(signum, frame):
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
except ProcessLookupError:
pass
sys.exit(1)
signal.signal(signal.SIGINT, sigint_handler)

start_time = time.time()
while process.poll() is None:
try:
time_process = psutil.Process(process.pid)
executable_process = None
for child in time_process.children():
if child.name() == executable:
executable_process = child
break
if executable_process is not None and executable_process.memory_info().rss > memory_limit * 1024:
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
except ProcessLookupError:
pass
mem_limit_exceeded = True
break
except psutil.NoSuchProcess:
pass

if time.time() - start_time > hard_time_limit:
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
except ProcessLookupError:
pass
timeout = True
break

with open(output_file_path, "r") as output_file:
output = output_file.read()
result = ExecutionResult()
program_exit_code = None
if not timeout:
output = output.splitlines()
with open(result_file_path, "r") as result_file:
lines = result_file.readlines()
if len(lines) == 3:
"""
If programs runs successfully, the output looks like this:
- first line is CPU time in seconds
- second line is memory in KB
- third line is exit code
This format is defined by -f flag in time command.
"""
result.Time = round(float(lines[0].strip()) * 1000)
result.Memory = int(lines[1].strip())
program_exit_code = int(lines[2].strip())
elif len(lines) > 0 and "Command terminated by signal " in lines[0]:
"""
If there was a runtime error, the first line is the error message with signal number.
For example:
Command terminated by signal 11
"""
program_exit_code = int(lines[0].strip().split(" ")[-1])
elif not mem_limit_exceeded:
result.Status = Status.RE
result.Error = "Unexpected output from time command: " + "".join(lines)
return result

if program_exit_code is not None and program_exit_code != 0:
result.Status = Status.RE
elif timeout:
result.Status = Status.TL
elif mem_limit_exceeded:
result.Memory = memory_limit + 1 # Add one so that the memory is red in the table
result.Status = Status.ML
elif result.Time > time_limit:
result.Status = Status.TL
elif result.Memory > memory_limit:
result.Status = Status.ML
else:
try:
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:
result.Status = Status.WA
except CheckerOutputException as e:
result.Status = Status.CE
result.Error = e.message

return result


def run_solution(self, data_for_execution: ExecutionData):
"""
Run an execution and return the result as ExecutionResult object.
Expand Down Expand Up @@ -1173,16 +920,6 @@ def check_errors(self, results: Dict[str, Dict[str, Dict[str, ExecutionResult]]]
if error_msg != "":
print(util.error(error_msg))

def compile_checker(self):
checker_basename = os.path.basename(self.checker)
self.checker_executable = paths.get_executables_path(checker_basename + ".e")

checker_compilation = self.compile_solutions([self.checker], remove_all_cache=True)
if not checker_compilation[0]:
util.exit_with_error('Checker compilation failed.')



def compile_additional_files(self, files):
for name, args, kwargs in files:
print(f'Compiling {name}...')
Expand Down
28 changes: 7 additions & 21 deletions src/sinol_make/task_type/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ def __init__(self, task_id):
def run_outgen(self):
return True

def require_outputs(self):
return True

def get_files_to_compile(self) -> List[Tuple[str, List[str], Dict[str, Any]]]:
"""
Returns a list of tuples where:
Expand All @@ -34,6 +37,9 @@ def has_checker(self) -> bool:
def _run_checker(self, input_file, output_file_path, answer_file_path) -> List[str]:
return []

def _raise_empty_output(self):
raise CheckerOutputException("Checker output is empty.")

def _parse_checker_output(self, checker_output: List[str]) -> Tuple[bool, int]:
"""
Parse the output of the checker
Expand Down Expand Up @@ -140,15 +146,6 @@ def _parse_oiejq_output(self, result_file_path: str):
return result

def _run_subprocess(self, oiejq: bool, sigint_handler, executable, memory_limit, hard_time_limit, *args, **kwargs):
# print("oiejq", oiejq)
# print("executable", executable)
# print("memory_limit", memory_limit)
# print("hard_time_limit", hard_time_limit)
# print(args, kwargs)
# stdin_stat = os.fstat(kwargs['stdin'])
# print("stdin", kwargs['stdin'], "stdin_stat", stdin_stat)
# stdout_stat = os.fstat(kwargs['stdout'])
# print("stdout", kwargs['stdout'], "stdout_stat", stdout_stat)
process = subprocess.Popen(*args, **kwargs)
if 'pass_fds' in kwargs:
for fd in kwargs['pass_fds']:
Expand Down Expand Up @@ -215,9 +212,6 @@ def _run_program_time(self, command, env, executable, result_file_path, input_fi
answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir):
raise NotImplementedError()

def _update_result_RE(self, result, program_exit_code):
pass

def _parse_additional_time(self, result_file_path) -> Union[ExecutionResult, None]:
return None

Expand All @@ -238,16 +232,11 @@ def run(self, oiejq: bool, timetool_path, executable, result_file_path, input_fi
execution_dir)
if not timeout:
result, program_exit_code = self._parse_time_output(result_file_path)
if program_exit_code is not None and program_exit_code != 0:
if program_exit_code is not None and program_exit_code != 0 and result.Status != Status.RE:
result.Status = Status.RE
result.Error = f"Program exited with code {program_exit_code}."
self._update_result_RE(result, program_exit_code)
return result

additional_result = self._parse_additional_time(result_file_path)
if additional_result is not None:
return additional_result

try:
with open(output_file_path, "r") as output_file:
output = output_file.readlines()
Expand Down Expand Up @@ -282,6 +271,3 @@ def getattrd(obj, attr, default):
result.Error = str(e)

return result

def require_outputs(self):
return True
Loading

0 comments on commit b1093d7

Please sign in to comment.