Skip to content

Commit 97610ba

Browse files
Improved logging, fix unicode error, include_dir buildrun
1 parent 563f0d1 commit 97610ba

File tree

6 files changed

+100
-40
lines changed

6 files changed

+100
-40
lines changed

problemtools/problem2html.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
import logging
88
import subprocess
99

10-
import plasTeX.TeX
11-
import plasTeX.Logging
12-
13-
from .ProblemPlasTeX import ProblemRenderer
14-
from .ProblemPlasTeX import ProblemsetMacros
15-
from . import template
1610

1711
def convert(options: argparse.Namespace) -> None:
12+
import plasTeX.TeX
13+
import plasTeX.Logging
14+
15+
from .ProblemPlasTeX import ProblemRenderer
16+
from .ProblemPlasTeX import ProblemsetMacros
17+
from . import template
18+
1819
problem = os.path.realpath(options.problem)
1920

2021
problembase = os.path.splitext(os.path.basename(problem))[0]

problemtools/run/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,18 @@ def get_program(path, language_config=None, work_dir=None, include_dir=None,
102102
files = [path]
103103
else:
104104
build = os.path.join(path, 'build')
105-
if os.path.isfile(build) and os.access(path, os.X_OK):
105+
if os.path.isfile(build) and os.access(build, os.X_OK):
106106
return BuildRun(path, work_dir)
107107
files = rutil.list_files_recursive(path)
108108

109109
if language_config is not None:
110110
lang = language_config.detect_language(files)
111111
if lang is not None:
112-
return SourceCode(path, lang,
113-
work_dir=work_dir, include_dir=include_dir)
112+
if include_dir is not None:
113+
lang_dir = os.path.join(include_dir, lang.lang_id)
114+
build = os.path.join(lang_dir, 'build')
115+
if os.path.isfile(build) and os.access(build, os.X_OK):
116+
return BuildRun(path, work_dir=work_dir, include_dir=lang_dir)
117+
118+
return SourceCode(path, lang, work_dir=work_dir, include_dir=include_dir)
114119
return None

problemtools/run/buildrun.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
from .program import Program
1313
from . import rutil
1414

15+
log = logging.getLogger(__file__)
16+
1517

1618
class BuildRun(Program):
1719
"""Class for build/run-script program.
1820
"""
1921

20-
def __init__(self, path, work_dir=None):
22+
def __init__(self, path, work_dir=None, include_dir=None):
2123
"""Instantiate BuildRun object.
2224
2325
Args:
@@ -28,12 +30,6 @@ def __init__(self, path, work_dir=None):
2830
if not os.path.isdir(path):
2931
raise ProgramError('%s is not a directory' % path)
3032

31-
build = os.path.join(path, 'build')
32-
if not os.path.isfile(build):
33-
raise ProgramError('%s does not have a build script' % path)
34-
if not os.access(build, os.X_OK):
35-
raise ProgramError('%s/build is not executable' % path)
36-
3733
if work_dir is None:
3834
work_dir = tempfile.mkdtemp()
3935

@@ -47,7 +43,14 @@ def __init__(self, path, work_dir=None):
4743
os.makedirs(self.path)
4844

4945
rutil.add_files(path, self.path)
46+
if include_dir is not None and os.path.isdir(include_dir):
47+
rutil.add_files(include_dir, self.path)
5048

49+
build = os.path.join(self.path, 'build')
50+
if not os.path.isfile(build):
51+
raise ProgramError('%s does not have a build script' % path)
52+
if not os.access(build, os.X_OK):
53+
raise ProgramError('%s/build is not executable' % path)
5154

5255
def __str__(self):
5356
"""String representation"""
@@ -65,8 +68,8 @@ def compile(self):
6568
run = os.path.join(self.path, 'run')
6669

6770
if status:
68-
logging.debug('Build script failed (status %d) when compiling %s\n', status, self.name)
69-
self._compile_result = (False, 'build script failed with exit code %d' % (status))
71+
log.debug('Build script failed (status %d) when compiling %s', status, self.name)
72+
self._compile_result = (False, f'build script failed with exit code {status:d}')
7073
elif not os.path.isfile(run) or not os.access(run, os.X_OK):
7174
self._compile_result = (False, 'build script did not produce an executable called "run"')
7275
else:

problemtools/run/program.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
from .errors import ProgramError
1010

11+
log = logging.getLogger(__name__)
12+
13+
1114
class Program(object):
1215
"""Abstract base class for programs.
1316
"""
@@ -70,7 +73,7 @@ def should_skip_memory_rlimit(self):
7073

7174
@staticmethod
7275
def __run_wait(argv, infile, outfile, errfile, timelim, memlim, working_directory=None):
73-
logging.debug('run "%s < %s > %s 2> %s"',
76+
log.debug('run "%s < %s > %s 2> %s"',
7477
' '.join(argv), infile, outfile, errfile)
7578
pid = os.fork()
7679
if pid == 0: # child
@@ -111,7 +114,7 @@ def __run_wait(argv, infile, outfile, errfile, timelim, memlim, working_director
111114
print(exc)
112115
os.kill(os.getpid(), signal.SIGTERM)
113116
# Unreachable
114-
logging.error("Unreachable part of run_wait reached")
117+
log.error("Unreachable part of run_wait reached")
115118
os.kill(os.getpid(), signal.SIGTERM)
116119
(pid, status, rusage) = os.wait4(pid, 0)
117120
return status, rusage.ru_utime + rusage.ru_stime

problemtools/run/source.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
from .program import Program
1313
from . import rutil
1414

15+
log = logging.getLogger(__name__)
16+
17+
1518
class SourceCode(Program):
1619
"""Class representing a program provided by source code.
1720
"""
@@ -103,7 +106,7 @@ def compile(self):
103106
if not os.path.isfile(compiler) or not os.access(compiler, os.X_OK):
104107
return (False, '%s does not seem to be installed, expected to find compiler at %s' % (self.language.name, compiler))
105108

106-
logging.debug('compile command: %s', command)
109+
log.debug('compile command: %s', command)
107110

108111
try:
109112
subprocess.check_output(command, stderr=subprocess.STDOUT)

problemtools/verifyproblem.py

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131

3232
from typing import Callable, Literal, Pattern, Match
3333

34+
log = logging.getLogger(__name__)
35+
3436
Verdict = Literal['AC', 'TLE', 'OLE', 'MLE', 'RTE', 'WA', 'PAC', 'JE']
3537

3638
def is_TLE(status: int, may_signal_with_usr1: bool=False) -> bool:
@@ -91,6 +93,7 @@ class ProblemAspect:
9193
warnings = 0
9294
bail_on_error = False
9395
_check_res: bool|None = None
96+
consider_warnings_errors = False
9497
basename_regex = re.compile('^[a-zA-Z0-9][a-zA-Z0-9_.-]*[a-zA-Z0-9]$')
9598
consider_warnings_errors: bool
9699

@@ -110,28 +113,28 @@ def __append_additional_info(msg: str, additional_info: str|None) -> str:
110113

111114
return f'{msg}:\n' + '\n'.join(' '*8 + line for line in lines)
112115

113-
def error(self, msg: str, additional_info: str|None=None) -> None:
116+
def __init__(self, name):
117+
self.log = log.getChild(name)
118+
119+
def error(self, msg: str, additional_info: str|None=None, *args) -> None:
114120
self._check_res = False
115121
ProblemAspect.errors += 1
116-
logging.error('in %s: %s', self, ProblemAspect.__append_additional_info(msg, additional_info))
122+
self.log.error(ProblemAspect.__append_additional_info(msg, additional_info), *args)
117123
if ProblemAspect.bail_on_error:
118124
raise VerifyError(msg)
119125

120-
def warning(self, msg: str, additional_info: str|None=None) -> None:
126+
def warning(self, msg: str, additional_info: str|None=None, *args) -> None:
121127
if ProblemAspect.consider_warnings_errors:
122-
self.error(msg)
128+
self.error(msg, additional_info, *args)
123129
return
124130
ProblemAspect.warnings += 1
125-
logging.warning('in %s: %s', self, ProblemAspect.__append_additional_info(msg, additional_info))
126-
127-
def msg(self, msg: str) -> None:
128-
print(msg)
131+
self.log.warning(ProblemAspect.__append_additional_info(msg, additional_info), *args)
129132

130-
def info(self, msg: str) -> None:
131-
logging.info(': %s', msg)
133+
def info(self, msg: str, *args) -> None:
134+
self.log.info(msg, *args)
132135

133-
def debug(self, msg: str) -> None:
134-
logging.debug(': %s', msg)
136+
def debug(self, msg: str, *args) -> None:
137+
self.log.debug(msg, *args)
135138

136139
def check_basename(self, path: str) -> None:
137140
basename = os.path.basename(path)
@@ -140,6 +143,7 @@ def check_basename(self, path: str) -> None:
140143

141144
class TestCase(ProblemAspect):
142145
def __init__(self, problem: Problem, base: str, testcasegroup: TestCaseGroup):
146+
super().__init__(f"{problem.shortname}.test.{testcasegroup.name}.{os.path.basename(base)}")
143147
self._base = base
144148
self.infile = f'{base}.in'
145149
self.ansfile = f'{base}.ans'
@@ -248,6 +252,8 @@ def _run_submission_real(self, sub, args: argparse.Namespace, timelim: int, time
248252
return (res, res_low, res_high, True)
249253

250254
outfile = os.path.join(self._problem.tmpdir, 'output')
255+
errfile = os.path.join(self._problem.tmpdir, 'error')
256+
251257
if sys.stdout.isatty():
252258
msg = f'Running {sub} on {self}...'
253259
sys.stdout.write(msg)
@@ -256,13 +262,22 @@ def _run_submission_real(self, sub, args: argparse.Namespace, timelim: int, time
256262
if self._problem.is_interactive:
257263
res_high = self._problem.output_validators.validate_interactive(self, sub, timelim_high, self._problem.submissions)
258264
else:
259-
status, runtime = sub.run(self.infile, outfile,
265+
status, runtime = sub.run(infile=self.infile, outfile=outfile, errfile=errfile,
260266
timelim=timelim_high+1,
261267
memlim=self._problem.config.get('limits')['memory'], set_work_dir=True)
262268
if is_TLE(status) or runtime > timelim_high:
263269
res_high = SubmissionResult('TLE')
264270
elif is_RTE(status):
265-
res_high = SubmissionResult('RTE')
271+
if os.path.isfile(errfile):
272+
try:
273+
with open(errfile, mode="rt") as f:
274+
info = f.read()
275+
except IOError:
276+
self.info("Failed to read error file %s", errfile)
277+
info = None
278+
else:
279+
info = None
280+
res_high = SubmissionResult('RTE', additional_info=info)
266281
else:
267282
res_high = self._problem.output_validators.validate(self, outfile)
268283
res_high.runtime = runtime
@@ -318,8 +333,13 @@ def __init__(self, problem: Problem, datadir: str, parent: TestCaseGroup|None=No
318333
self._parent = parent
319334
self._problem = problem
320335
self._datadir = datadir
336+
self.name = os.path.relpath(os.path.abspath(self._datadir),
337+
os.path.abspath(self._problem.probdir)).replace("/", ".")
338+
339+
super().__init__(f"{problem.shortname}.test.{self.name}")
340+
321341
self._seen_oob_scores = False
322-
self.debug(f' Loading test data group {datadir}')
342+
self.debug('Loading test data group %s', datadir)
323343
configfile = os.path.join(self._datadir, 'testdata.yaml')
324344
self.config = {}
325345
if os.path.isfile(configfile):
@@ -374,7 +394,7 @@ def __init__(self, problem: Problem, datadir: str, parent: TestCaseGroup|None=No
374394

375395

376396
def __str__(self) -> str:
377-
return f'test case group {os.path.relpath(self._datadir, os.path.join(self._problem.probdir))}'
397+
return f'test case group {self.name}'
378398

379399
def set_symlinks(self) -> None:
380400
for sub in self._items:
@@ -627,6 +647,7 @@ class ProblemConfig(ProblemAspect):
627647
_VALID_LICENSES = ['unknown', 'public domain', 'cc0', 'cc by', 'cc by-sa', 'educational', 'permission']
628648

629649
def __init__(self, problem: Problem):
650+
super().__init__(f"{problem.shortname}.config")
630651
self.debug(' Loading problem config')
631652
self._problem = problem
632653
self.configfile = os.path.join(problem.probdir, 'problem.yaml')
@@ -1061,6 +1082,7 @@ def check(self, args: argparse.Namespace) -> bool:
10611082

10621083
class ProblemStatement(ProblemAspect):
10631084
def __init__(self, problem: Problem):
1085+
super().__init__(f"{problem.shortname}.statement")
10641086
self.debug(' Loading problem statement')
10651087
self._problem = problem
10661088
self.languages = []
@@ -1136,6 +1158,7 @@ class Attachments(ProblemAspect):
11361158
"""
11371159

11381160
def __init__(self, problem: Problem):
1161+
super().__init__(f"{problem.shortname}.attachments")
11391162
attachments_path = os.path.join(problem.probdir, 'attachments')
11401163
self.attachments: list[str] = []
11411164
if os.path.isdir(attachments_path):
@@ -1165,7 +1188,7 @@ def __str__(self) -> str:
11651188

11661189
_JUNK_CASES = [
11671190
('an empty file', b''),
1168-
('a binary file with byte values 0 up to 256', bytearray(x for x in range(256))),
1191+
('a binary file with byte values 0 up to 127', bytearray(x for x in range(127))),
11691192
('a text file with the ASCII characters 32 up to 127', bytearray(x for x in range(32, 127))),
11701193
('a random text file with printable ASCII characters', bytearray(random.choice(string.printable.encode('utf8')) for _ in range(200))),
11711194
]
@@ -1185,6 +1208,7 @@ def _build_junk_modifier(desc: str, pattern: str, repl: str|Callable[[Match], st
11851208
class InputFormatValidators(ProblemAspect):
11861209

11871210
def __init__(self, problem: Problem):
1211+
super().__init__(f"{problem.shortname}.input_validator")
11881212
self._problem = problem
11891213
input_validators_path = os.path.join(problem.probdir, 'input_format_validators')
11901214
if os.path.isdir(input_validators_path):
@@ -1304,6 +1328,7 @@ class Graders(ProblemAspect):
13041328
_default_grader = run.get_tool('default_grader')
13051329

13061330
def __init__(self, problem: Problem):
1331+
super().__init__(f"{problem.shortname}.grader")
13071332
self._problem = problem
13081333
self._graders: list = run.find_programs(os.path.join(problem.probdir, 'graders'),
13091334
language_config=problem.language_config,
@@ -1382,7 +1407,7 @@ def grade(self, sub_results: list[SubmissionResult], testcasegroup: TestCaseGrou
13821407
# TODO: check that all graders give same result
13831408

13841409
if not shadow_result:
1385-
self.info(f'Grade on {testcasegroup} is {verdict} ({score})')
1410+
self.debug(f'Grade on {testcasegroup} is {verdict} ({score})')
13861411

13871412
return (verdict, score)
13881413

@@ -1392,6 +1417,7 @@ class OutputValidators(ProblemAspect):
13921417

13931418

13941419
def __init__(self, problem: Problem):
1420+
super().__init__(f"{problem.shortname}.output_validator")
13951421
self._problem = problem
13961422
self._validators = run.find_programs(os.path.join(problem.probdir,
13971423
'output_validators'),
@@ -1585,11 +1611,28 @@ def validate(self, testcase: TestCase, submission_output: str) -> SubmissionResu
15851611
for val in self._actual_validators():
15861612
if val is not None and val.compile()[0]:
15871613
feedbackdir = tempfile.mkdtemp(prefix='feedback', dir=self._problem.tmpdir)
1614+
validator_output = tempfile.mkdtemp(prefix='checker_out', dir=self._problem.tmpdir)
1615+
outfile = validator_output + "/out.txt"
1616+
errfile = validator_output + "/err.txt"
15881617
status, runtime = val.run(submission_output,
15891618
args=[testcase.infile, testcase.ansfile, feedbackdir] + flags,
1590-
timelim=val_timelim, memlim=val_memlim)
1619+
timelim=val_timelim, memlim=val_memlim,
1620+
outfile=outfile, errfile=errfile)
1621+
if self.log.isEnabledFor(logging.DEBUG):
1622+
try:
1623+
with open(outfile, mode="rt") as f:
1624+
output = f.read()
1625+
if output:
1626+
self.log.debug("Validator output:\n%s", output)
1627+
with open(errfile, mode="rt") as f:
1628+
error = f.read()
1629+
if error:
1630+
self.log.debug("Validator stderr:\n%s", error)
1631+
except IOError as e:
1632+
self.info("Failed to read validator output: %s", e)
15911633
res = self._parse_validator_results(val, status, feedbackdir, testcase)
15921634
shutil.rmtree(feedbackdir)
1635+
shutil.rmtree(validator_output)
15931636
if res.verdict != 'AC':
15941637
return res
15951638

@@ -1609,6 +1652,7 @@ class Submissions(ProblemAspect):
16091652
]
16101653

16111654
def __init__(self, problem: Problem):
1655+
super().__init__(f"{problem.shortname}.submission")
16121656
self._submissions = {}
16131657
self._problem = problem
16141658
srcdir = os.path.join(problem.probdir, 'submissions')
@@ -1742,6 +1786,7 @@ class Problem(ProblemAspect):
17421786
def __init__(self, probdir: str):
17431787
self.probdir = os.path.realpath(probdir)
17441788
self.shortname: str|None = os.path.basename(self.probdir)
1789+
super().__init__(self.shortname)
17451790
self.language_config = languages.load_language_config()
17461791

17471792
def __enter__(self) -> Problem:

0 commit comments

Comments
 (0)