Skip to content
Merged
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
4 changes: 4 additions & 0 deletions problemtools/checks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from .attachments import check_attachments
from .graders import check_graders
from .includes import check_includes
from .statements import check_statements
from .submissions import check_submissions
from .testdata import check_testdata
from .validators import check_input_validators, check_output_validators

__all__ = [
'check_attachments',
'check_graders',
'check_includes',
'check_input_validators',
'check_output_validators',
'check_statements',
'check_submissions',
'check_testdata',
]
13 changes: 13 additions & 0 deletions problemtools/checks/attachments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Checks for a problem package's attachments."""

from __future__ import annotations

from ..diagnostics import Diagnostics
from ..model import Attachments


def check_attachments(attachments: Attachments, diag: Diagnostics) -> None:
"""Run all checks on a problem's attachments."""
for attachment_path in attachments.paths:
if attachment_path.is_dir():
diag.error(f'Directories are not allowed as attachments ({attachment_path} is a directory)')
101 changes: 101 additions & 0 deletions problemtools/checks/statements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Checks for a problem package's statements."""

from __future__ import annotations

import glob
import os
import traceback
from pathlib import Path

from .. import problem2html, problem2pdf
from ..diagnostics import Diagnostics
from ..formatversion import FormatVersion
from ..metadata import Metadata
from ..model import Statements

# Temporary local copy of checks/validators.py's _warn_directory. Only two consumers so far;
# promote to a shared helper if a third one shows up.


def _warn_directory(format: FormatVersion, probdir: Path, name: str, prop: str, diag: Diagnostics) -> None:
good_dir = getattr(format, prop)
bad_dirs = {getattr(version, prop) for version in FormatVersion} - {good_dir}
for directory in bad_dirs:
if (probdir / directory).exists():
diag.warning(f'Found directory "{directory}". Version {format} looks for {name} in "{good_dir}"')


def check_statements(
statements: Statements,
metadata: Metadata,
format: FormatVersion,
probdir: Path,
work_dir: str,
diag: Diagnostics,
) -> None:
"""Run all checks on a problem's statements."""
_warn_directory(format, probdir, 'problem statements', 'statement_directory', diag)

for ifilename in glob.glob(os.path.join(str(probdir), 'data/sample/*.interaction')):
if not metadata.is_interactive() and not metadata.is_multi_pass():
diag.error(f'Problem is not interactive, but there is an interaction sample {ifilename}')
with open(ifilename, 'r') as interaction:
for i, line in enumerate(interaction):
valid_new_pass = metadata.is_multi_pass() and line.strip() == '---'
if len(line) == 0 or (line[0] != '<' and line[0] != '>' and not valid_new_pass):
diag.error(
f'Interaction {ifilename}: line {i + 1} does not start with < or > {"or ---" if metadata.is_multi_pass() else ""}'
)
break

if not statements.by_language:
if format is FormatVersion.LEGACY:
allowed_statements = ', '.join(f'problem.{ext}, problem.<language>.{ext}' for ext in format.statement_extensions)
else:
allowed_statements = ', '.join(f'problem.<language>.{ext}' for ext in format.statement_extensions)

diag.error(
f'No problem statements found (expected file of one of following forms in directory {format.statement_directory}/: {allowed_statements})'
)

def _latex_heuristic(name: str) -> bool:
return '\\' in name or '$' in name

for lang, files in statements.by_language.items():
if len(files) > 1:
diag.error(f'Found multiple statements in the same language {lang}: {", ".join(file.name for file in files)}')

if lang not in metadata.name:
diag.error(f'No problem name given in language {lang}')
elif not metadata.name[lang]:
diag.error(f'Problem name in language {lang} is empty')
elif not metadata.name[lang].strip():
diag.error(f'Problem name in language {lang} contains only whitespace')
elif format is FormatVersion.LEGACY and _latex_heuristic(metadata.name[lang]):
diag.warning(f'Problem name in language {lang} looks like LaTeX. Consider using plainproblemname.')

for file in files:
try:
options = problem2pdf.get_parser().parse_args([''])
options.problem = probdir
options.language = lang
options.nopdf = True
options.quiet = True
if not problem2pdf.convert(options, file):
diag.error(
f'Could not compile problem statement for language "{lang}". Run problem2pdf --language {lang} on the problem to diagnose.'
)
except Exception as e:
diag.error(f'Error raised when checking problem statement for language {lang}:\n{e}\n{traceback.format_exc()}')

try:
options = problem2html.get_parser().parse_args([''])
options.problem = probdir
options.destdir = os.path.join(work_dir, 'html')
options.language = lang
options.quiet = True
problem2html.convert(options, file)
except Exception as e:
diag.error(
f'Could not convert problem statement to html for language "{lang}". Run problem2html --language {lang} on the problem to diagnose.\n{e}\n{traceback.format_exc()}'
)
6 changes: 6 additions & 0 deletions problemtools/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
#: A submission's (or testcase's) verdict, e.g. as expected by policy or produced by judging.
Verdict = Literal['AC', 'TLE', 'OLE', 'MLE', 'RTE', 'WA', 'PAC', 'JE']

from .attachments import Attachments, load_attachments
from .graders import DEFAULT_GRADER, Graders, load_graders
from .includes import DEFAULT_LANGUAGE, IncludeFile, Includes, LanguageIncludes, load_includes
from .statements import Statements, load_statements
from .submissions import LegacyPolicy, Submission, Submissions, load_submissions
from .testdata import DEFAULT_CONFIG, SCORING_ONLY_KEYS, TestCase, TestDataGroup, load_testdata
from .validators import DEFAULT_VALIDATOR, InputValidators, OutputValidators, load_input_validators, load_output_validators
Expand All @@ -15,22 +17,26 @@
'DEFAULT_LANGUAGE',
'DEFAULT_VALIDATOR',
'SCORING_ONLY_KEYS',
'Attachments',
'Graders',
'IncludeFile',
'Includes',
'InputValidators',
'LanguageIncludes',
'LegacyPolicy',
'OutputValidators',
'Statements',
'Submission',
'Submissions',
'TestCase',
'TestDataGroup',
'Verdict',
'load_attachments',
'load_graders',
'load_includes',
'load_input_validators',
'load_output_validators',
'load_statements',
'load_submissions',
'load_testdata',
]
17 changes: 17 additions & 0 deletions problemtools/model/attachments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path


@dataclass(frozen=True)
class Attachments:
"""A problem's attachments: files found under the attachments/ directory."""

paths: list[Path] = field(default_factory=list)


def load_attachments(probdir: Path) -> Attachments:
attachments_dir = probdir / 'attachments'
paths = list(attachments_dir.iterdir()) if attachments_dir.is_dir() else []
return Attachments(paths=paths)
21 changes: 21 additions & 0 deletions problemtools/model/statements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path

from .. import statement_util
from ..formatversion import FormatVersion


@dataclass(frozen=True)
class Statements:
"""A problem's statements, keyed by language code.

Well-formed packages have exactly one statement per language; this keeps every
file found (rather than just the first) so checks can report duplicates."""

by_language: dict[str, list[Path]] = field(default_factory=dict)


def load_statements(probdir: Path, format: FormatVersion) -> Statements:
return Statements(by_language=statement_util.find_statements(probdir, format))
Loading