Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
12 changes: 10 additions & 2 deletions src/sio3pack/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import traceback


class SIO3PackException(Exception):
"""A wrapper for all exceptions raised by SIO3Pack."""

def __init__(self, message, original_exception=None):
def __init__(self, message, original_exception):
super().__init__(message)
self.original_exception = original_exception
self.message = message
self.original_exception = original_exception.__class__.__name__
self.traceback = traceback.format_exc()

def __str__(self):
return f"{self.message}\nOriginal exception: {self.original_exception}\nTraceback:\n{self.traceback}"
4 changes: 4 additions & 0 deletions src/sio3pack/files/file.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import os


class File:
"""
Base class for all files in a package.
Expand All @@ -7,6 +10,7 @@ class File:

def __init__(self, path: str):
self.path = path
self.name = os.path.basename(path)

def __str__(self):
return f"<{self.__class__.__name__} {self.path}>"
Expand Down
103 changes: 95 additions & 8 deletions src/sio3pack/packages/sinolpack/model.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import os
import re
import tempfile
from typing import Any

import yaml

from sio3pack.files import File, LocalFile
from sio3pack.packages.exceptions import ImproperlyConfigured
from sio3pack.packages.package import Package
from sio3pack.packages.sinolpack.enums import ModelSolutionKind
from sio3pack.test import Test
from sio3pack.util import naturalsort_key
from sio3pack.utils.archive import Archive, UnrecognizedArchiveFormat
from sio3pack.workflow import Workflow, WorkflowManager, WorkflowOperation
Expand All @@ -24,8 +26,8 @@ class Sinolpack(Package):
:param dict[str, File] lang_statements: A dictionary of problem
statements, where keys are language codes and values are files.
:param dict[str, Any] config: Configuration of the problem.
:param list[tuple[ModelSolutionKind, File]] model_solutions: A list
of model solutions, where each element is a tuple containing
:param list[dict[str, Any]] model_solutions: A list
of model solutions, where each element is a list of dicts containing
a model solution kind and a file.
:param list[File] additional_files: A list of additional files for
the problem.
Expand Down Expand Up @@ -167,13 +169,26 @@ def get_attachments_dir(self) -> str:
"""
return os.path.join(self.rootdir, "attachments")

def get_in_test_dir(self) -> str:
"""
Returns the path to the directory containing inputs to the problem's tests.
"""
return os.path.join(self.rootdir, "in")

def get_out_test_dir(self) -> str:
"""
Returns the path to the directory containing outputs to the problem's tests.
"""
return os.path.join(self.rootdir, "out")

def _process_package(self):
self._process_config_yml()
self._detect_full_name()
self._detect_full_name_translations()
self._process_prog_files()
self._process_statements()
self._process_attachments()
self._process_tests() # TODO: Delete when SIO3Worker will be implemented.

if not self.has_custom_graph:
# Create the workflow with processed files.
Expand Down Expand Up @@ -248,9 +263,9 @@ def get_model_solution_regex(self):
extensions = self.get_submittable_extensions()
return rf"^{self.short_name}[0-9]*([bs]?)[0-9]*(_.*)?\.({'|'.join(extensions)})"

def _get_model_solutions(self) -> list[tuple[ModelSolutionKind, File]]:
def _get_model_solutions(self) -> list[dict[str, Any]]:
"""
Returns a list of model solutions, where each element is a tuple of model solution kind and filename.
Returns a list of model solutions, where each element is a list of dicts of model solution kind and filename.
"""
if not os.path.exists(self.get_prog_dir()):
return []
Expand All @@ -261,19 +276,20 @@ def _get_model_solutions(self) -> list[tuple[ModelSolutionKind, File]]:
match = re.match(regex, file)
if match and os.path.isfile(os.path.join(self.get_prog_dir(), file)):
file = LocalFile(os.path.join(self.get_prog_dir(), file))
model_solutions.append((ModelSolutionKind.from_regex(match.group(1)), file))
model_solutions.append({"file": file, "kind": ModelSolutionKind.from_regex(match.group(1))})

return model_solutions

def sort_model_solutions(
self, model_solutions: list[tuple[ModelSolutionKind, File]]
) -> list[tuple[ModelSolutionKind, File]]:
self, model_solutions: list[dict[str, Any]]
) -> list[dict[str, Any]]:
"""
Sorts model solutions by kind.
"""

def sort_key(model_solution):
kind, file = model_solution
kind: ModelSolutionKind = model_solution['kind']
file: LocalFile = model_solution['file']
return kind.value, naturalsort_key(file.filename[: file.filename.index(".")])

return list(sorted(model_solutions, key=sort_key))
Expand Down Expand Up @@ -398,3 +414,74 @@ def save_to_db(self, problem_id: int):
if not self.django_enabled:
raise ImproperlyConfigured("sio3pack is not installed with Django support.")
self.django.save_to_db()

def _process_tests(self):
"""
Process the tests in the problem's directory.
"""
self.tests = []
in_dir = self.get_in_test_dir()
out_dir = self.get_out_test_dir()
if not os.path.exists(in_dir):
raise ImproperlyConfigured("Package does not contain a directory for test inputs.")
if not os.path.exists(out_dir):
raise ImproperlyConfigured("Package does not contain a directory for test outputs.")

test_dict: dict[str, (str, str)] = {}
for in_file in os.listdir(in_dir):
test_name = os.path.splitext(in_file)[0]
if os.path.splitext(in_file)[1] != '.in':
continue
if test_name not in test_dict:
test_dict[test_name] = (in_file, None)
elif test_dict[test_name][1] is not None:
test_dict[test_name] = (in_file, test_dict[test_name][1])
else:
raise ImproperlyConfigured(f"Duplicate test input for: {test_name}")

for out_file in os.listdir(out_dir):
test_name = os.path.splitext(out_file)[0]
if os.path.splitext(out_file)[1] != '.out':
continue
if test_name not in test_dict:
test_dict[test_name] = (None, out_file)
elif test_dict[test_name][0] is not None:
test_dict[test_name] = (test_dict[test_name][0], out_file)
else:
raise ImproperlyConfigured(f"Duplicate test output for: {test_name}")

for test_name, (in_file, out_file) in test_dict.items():
test_id = self._extract_test_id(test_name)
group = self._extract_test_group(test_name)
self.tests.append(
Test(
test_name,
test_id,
LocalFile(os.path.join(in_dir, in_file)) if in_file else None,
LocalFile(os.path.join(out_dir, out_file)) if out_file else None,
group,
)
)

def _extract_test_id(self, test_name: str) -> str:
"""
Extracts the test ID from the test name.
"""
return test_name.removeprefix(self.short_name)

def _extract_test_group(self, test_name: str) -> str:
"""
Extracts the test group from the test name.
"""
test_id = self._extract_test_id(test_name)
# Get leading numbers
return re.match(r"^\d+", test_id).group(0)

def get_additional_files(self) -> list[File]:
"""
Returns the list of additional files.
"""
return self.additional_files

def reload_tests(self):
self._process_tests()
11 changes: 10 additions & 1 deletion src/sio3pack/test/test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
from sio3pack.files import File


class Test:
"""
Represents an input or output test.
Represents an input and output test.
"""
def __init__(self, test_name: str, test_id: str, in_file: File, out_file: File, group: str):
self.test_name = test_name
self.test_id = test_id
self.in_file = in_file
self.out_file = out_file
self.group = group

pass