rbx.cp 0.5.16__py3-none-any.whl → 0.5.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rbx/box/solutions_test.py CHANGED
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import pathlib
2
3
 
3
4
  import pytest
@@ -20,7 +21,7 @@ def test_solutions(pkg_from_testdata: pathlib.Path):
20
21
  generate_outputs_for_testcases()
21
22
 
22
23
  result = run_solutions(verification=VerificationLevel.FULL)
23
- res = convert_list_of_solution_evaluations_to_dict(result.items)
24
+ res = asyncio.run(convert_list_of_solution_evaluations_to_dict(result.items))
24
25
 
25
26
  # First solution should pass all tests.
26
27
  assert all(chk.result.outcome == Outcome.ACCEPTED for chk in res[0]['gen1'])
rbx/box/stresses.py CHANGED
@@ -8,7 +8,7 @@ from pydantic import BaseModel
8
8
 
9
9
  from rbx import console
10
10
  from rbx.box import checkers, package, validators
11
- from rbx.box.code import compile_item, run_item
11
+ from rbx.box.code import SanitizationLevel, compile_item, run_item
12
12
  from rbx.box.generators import generate_standalone
13
13
  from rbx.box.schema import CodeItem, GeneratorCall, Stress, Testcase
14
14
  from rbx.box.solutions import compile_solutions, get_outcome_style_verdict
@@ -49,6 +49,7 @@ def run_stress(
49
49
  findingsLimit: int = 1,
50
50
  verbose: bool = False,
51
51
  progress: Optional[StatusProgress] = None,
52
+ sanitized: bool = False,
52
53
  ) -> StressReport:
53
54
  if finder:
54
55
  stress = Stress(
@@ -63,7 +64,7 @@ def run_stress(
63
64
  generator = package.get_generator(call.name)
64
65
 
65
66
  try:
66
- generator_digest = compile_item(generator)
67
+ generator_digest = compile_item(generator, sanitized=SanitizationLevel.PREFER)
67
68
  except:
68
69
  console.console.print(
69
70
  f'[error]Failed compiling generator [item]{generator.name}[/item].[/error]'
@@ -79,7 +80,8 @@ def run_stress(
79
80
 
80
81
  solution_indices = {str(solution.path): i for i, solution in enumerate(solutions)}
81
82
  solutions_digest = compile_solutions(
82
- tracked_solutions=set(str(solution.path) for solution in solutions)
83
+ tracked_solutions=set(str(solution.path) for solution in solutions),
84
+ sanitized=sanitized,
83
85
  )
84
86
  if progress:
85
87
  progress.update('Compiling finders...')
rbx/box/validators.py CHANGED
@@ -1,13 +1,13 @@
1
1
  import pathlib
2
2
  import shlex
3
- from typing import Dict, List, Optional, Set, Tuple
3
+ from typing import Dict, Iterable, List, Optional, Set, Tuple
4
4
 
5
5
  import typer
6
6
  from pydantic import BaseModel
7
7
 
8
8
  from rbx import console
9
9
  from rbx.box import package
10
- from rbx.box.code import compile_item, run_item
10
+ from rbx.box.code import SanitizationLevel, compile_item, run_item
11
11
  from rbx.box.schema import CodeItem, Primitive
12
12
  from rbx.box.testcases import find_built_testcase_inputs
13
13
  from rbx.grading.judge.sandbox import SandboxBase
@@ -32,7 +32,7 @@ class TestcaseValidationInfo(BaseModel):
32
32
 
33
33
  def _compile_validator(validator: CodeItem) -> str:
34
34
  try:
35
- digest = compile_item(validator)
35
+ digest = compile_item(validator, sanitized=SanitizationLevel.PREFER)
36
36
  except:
37
37
  console.console.print(
38
38
  f'[error]Failed compiling validator [item]{validator.path}[/item][/error]'
@@ -65,7 +65,7 @@ def _process_bounds(log: str) -> HitBounds:
65
65
  return bounds
66
66
 
67
67
 
68
- def _merge_hit_bounds(hit_bounds: List[HitBounds]) -> HitBounds:
68
+ def _merge_hit_bounds(hit_bounds: Iterable[HitBounds]) -> HitBounds:
69
69
  res: HitBounds = {}
70
70
  for hb in hit_bounds:
71
71
  for k, hit in hb.items():
@@ -241,7 +241,7 @@ def has_validation_errors(infos: List[TestcaseValidationInfo]) -> bool:
241
241
 
242
242
  def print_validation_report(infos: List[TestcaseValidationInfo]):
243
243
  console.console.rule('Validation report', style='status')
244
- hit_bounds_per_group: Dict[str, HitBounds] = {}
244
+ hit_bounds_per_group: Dict[Optional[str], HitBounds] = {}
245
245
  for info in infos:
246
246
  if not info.ok:
247
247
  console.console.print(
rbx/config.py CHANGED
@@ -268,4 +268,7 @@ def edit():
268
268
  """
269
269
  Open the config in an editor.
270
270
  """
271
+ # Ensure config is created before calling the editor.
272
+ get_config()
273
+
271
274
  open_editor(get_config_path())
rbx/grading/caching.py CHANGED
@@ -334,6 +334,10 @@ class DependencyCache:
334
334
  for logs, reference_logs in zip(fingerprint.logs, reference_fingerprint.logs):
335
335
  if logs.run is not None:
336
336
  reference_logs.run = logs.run.model_copy(deep=True)
337
+ if logs.preprocess is not None:
338
+ reference_logs.preprocess = [
339
+ log.model_copy(deep=True) for log in logs.preprocess
340
+ ]
337
341
  reference_logs.cached = True
338
342
 
339
343
  return True
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import importlib
4
4
  import importlib.resources
5
5
  import logging
6
+ import os
6
7
  import pathlib
7
8
  import shutil
8
9
  import signal
@@ -291,6 +292,7 @@ class StupidSandbox(SandboxBase):
291
292
  stdin=subprocess.PIPE,
292
293
  stdout=subprocess.PIPE,
293
294
  stderr=subprocess.STDOUT,
295
+ env={**os.environ, **self.params.set_env},
294
296
  )
295
297
  self.hydrate_logs()
296
298
  return self.translate_box_exitcode(self.returncode)
@@ -200,14 +200,14 @@ def main():
200
200
  os.kill(sub_pid, 9)
201
201
  return
202
202
 
203
- signal.alarm(1)
203
+ signal.setitimer(signal.ITIMER_REAL, 0.3)
204
204
 
205
- signal.alarm(1)
205
+ signal.setitimer(signal.ITIMER_REAL, 0.3)
206
206
  signal.signal(signal.SIGALRM, handle_alarm)
207
207
  wait_and_finish(sub_pid, options, start_time, alarm_msg=alarm_msg)
208
208
 
209
209
  # Cancel alarm before exiting to avoid surprises.
210
- signal.alarm(0)
210
+ signal.setitimer(signal.ITIMER_REAL, 0)
211
211
 
212
212
  # Exit gracefully.
213
213
  sys.exit()
rbx/grading/steps.py CHANGED
@@ -1,12 +1,14 @@
1
1
  import functools
2
- import os
3
2
  import pathlib
3
+ import re
4
4
  import shlex
5
5
  import shutil
6
6
  import subprocess
7
+ import sys
7
8
  from enum import Enum
8
- from typing import Any, Dict, List, Optional, Tuple, Union
9
+ from typing import IO, Any, Dict, List, Optional, Tuple, Union
9
10
 
11
+ import typer
10
12
  from pydantic import BaseModel
11
13
  from rich.text import Text
12
14
 
@@ -14,7 +16,7 @@ from rbx import utils
14
16
  from rbx.config import get_bits_stdcpp, get_jngen, get_testlib
15
17
  from rbx.console import console
16
18
  from rbx.grading.judge.sandbox import SandboxBase, SandboxParams
17
- from rbx.grading.judge.storage import copyfileobj
19
+ from rbx.grading.judge.storage import Storage, copyfileobj
18
20
 
19
21
  MAX_STDOUT_LEN = 1024 * 1024 * 128 # 128 MB
20
22
 
@@ -36,6 +38,7 @@ class DigestHolder(BaseModel):
36
38
 
37
39
  class GradingLogsHolder(BaseModel):
38
40
  run: Optional['RunLog'] = None
41
+ preprocess: Optional[List['PreprocessLog']] = None
39
42
  cached: bool = False
40
43
 
41
44
 
@@ -114,6 +117,17 @@ class GradingFileOutput(BaseModel):
114
117
  # Whether to track file through its hash (disable for optimization).
115
118
  hash: bool = True
116
119
 
120
+ def get_file(self, storage: Storage) -> Optional[IO[bytes]]:
121
+ if self.dest is not None:
122
+ if self.optional and not self.dest.exists():
123
+ return None
124
+ return self.dest.open('rb')
125
+ if self.digest is not None and self.digest.value is not None:
126
+ if self.optional and not storage.exists(self.digest.value):
127
+ return None
128
+ return storage.get_file(self.digest.value)
129
+ raise ValueError('No file to get')
130
+
117
131
 
118
132
  class GradingArtifacts(BaseModel):
119
133
  # Root directory for the produced artifacts.
@@ -125,6 +139,18 @@ class GradingArtifacts(BaseModel):
125
139
  # Capture certain logs of the execution.
126
140
  logs: Optional[GradingLogsHolder] = None
127
141
 
142
+ def get_input_file_for_dest(self, dest: pathlib.Path) -> Optional[GradingFileInput]:
143
+ for input in self.inputs:
144
+ if input.dest == dest:
145
+ return input
146
+ return None
147
+
148
+ def get_output_file_for_src(self, src: pathlib.Path) -> Optional[GradingFileOutput]:
149
+ for output in self.outputs:
150
+ if output.src == src:
151
+ return output
152
+ return None
153
+
128
154
 
129
155
  class TestcaseIO(BaseModel):
130
156
  index: int
@@ -134,6 +160,7 @@ class TestcaseIO(BaseModel):
134
160
 
135
161
  class RunLogMetadata(BaseModel):
136
162
  language: Optional[str] = None
163
+ is_sanitized: bool = False
137
164
 
138
165
 
139
166
  class RunLog(BaseModel):
@@ -141,6 +168,7 @@ class RunLog(BaseModel):
141
168
  exitstatus: str = SandboxBase.EXIT_SANDBOX_ERROR
142
169
  time: Optional[float] = 0.0
143
170
  memory: Optional[int] = 0
171
+ warnings: bool = False
144
172
  metadata: Optional[RunLogMetadata] = None
145
173
 
146
174
  def get_run_language(self) -> Optional[str]:
@@ -160,6 +188,9 @@ class PreprocessLog(RunLog):
160
188
  cmd: List[str]
161
189
  log: str
162
190
 
191
+ def get_command(self) -> str:
192
+ return ' '.join(self.cmd)
193
+
163
194
 
164
195
  class TestcaseLog(RunLog):
165
196
  stdout_absolute_path: Optional[pathlib.Path] = None
@@ -171,6 +202,7 @@ class CheckerResult(BaseModel):
171
202
  outcome: Outcome
172
203
  message: str = ''
173
204
  no_tle_outcome: Optional[Outcome] = None
205
+ sanitizer_warnings: bool = False
174
206
 
175
207
 
176
208
  class Evaluation(BaseModel):
@@ -260,11 +292,25 @@ def _split_and_expand(command: str, sandbox: SandboxBase) -> List[str]:
260
292
 
261
293
 
262
294
  def _is_c_command(exe_command: str) -> bool:
263
- return exe_command.endswith('gcc') or exe_command.endswith('clang')
295
+ return 'gcc' in exe_command or 'clang' in exe_command
264
296
 
265
297
 
266
298
  def _is_cpp_command(exe_command: str) -> bool:
267
- return exe_command.endswith('g++') or exe_command.endswith('clang++')
299
+ return 'g++' in exe_command or 'clang++' in exe_command
300
+
301
+
302
+ def is_cxx_command(exe_command: str) -> bool:
303
+ return _is_cpp_command(exe_command) or _is_c_command(exe_command)
304
+
305
+
306
+ def is_cxx_sanitizer_command(command: str) -> bool:
307
+ cmds = shlex.split(command)
308
+ if not cmds:
309
+ return False
310
+ exe = cmds[0]
311
+ if not is_cxx_command(exe):
312
+ return False
313
+ return 'fsanitize' in command
268
314
 
269
315
 
270
316
  @functools.cache
@@ -279,27 +325,35 @@ def _complain_about_clang() -> None:
279
325
  )
280
326
 
281
327
 
282
- def _maybe_get_bits_stdcpp_for_clang(command: str) -> Optional[GradingFileInput]:
328
+ def _get_cxx_version_output(command: str) -> Optional[str]:
283
329
  cmds = shlex.split(command)
284
330
  if not cmds:
285
331
  return None
286
332
  exe = cmds[0]
287
-
288
- if not _is_cpp_command(exe):
333
+ if not is_cxx_command(exe):
289
334
  return None
290
335
 
336
+ exe = cmds[0]
291
337
  output = subprocess.run([exe, '-v'], capture_output=True)
292
338
  if output.returncode != 0:
293
- console.print('[error]Failed to get g++/clang compiler version.[/error]')
339
+ console.print('[error]Failed to get C/C++ compiler version.[/error]')
294
340
  return None
295
- lines = output.stderr.decode().splitlines()
341
+ return output.stderr.decode()
342
+
343
+
344
+ def _maybe_get_bits_stdcpp_for_clang(command: str) -> Optional[GradingFileInput]:
345
+ version_output = _get_cxx_version_output(command)
346
+ if version_output is None:
347
+ return None
348
+ lines = version_output.splitlines()
296
349
  if not lines:
297
350
  return None
298
351
  # Check the first line for `clang`.
299
352
  if 'clang' not in lines[0]:
300
353
  return None
301
354
 
302
- _complain_about_clang()
355
+ if not is_cxx_sanitizer_command(command):
356
+ _complain_about_clang()
303
357
  bits = get_bits_stdcpp()
304
358
  return GradingFileInput(src=bits, dest=pathlib.Path('bits/stdc++.h'))
305
359
 
@@ -316,10 +370,6 @@ def _maybe_get_bits_stdcpp_for_commands(
316
370
 
317
371
  @functools.cache
318
372
  def _try_following_alias_for_exe(exe: str) -> Optional[str]:
319
- if _is_c_command(exe) and os.environ.get('RBX_C_PATH'):
320
- return os.environ['RBX_C_PATH']
321
- if _is_cpp_command(exe) and os.environ.get('RBX_CXX_PATH'):
322
- return os.environ['RBX_CXX_PATH']
323
373
  output = subprocess.run(
324
374
  f'which {exe}', shell=True, executable=shutil.which('bash'), capture_output=True
325
375
  )
@@ -346,6 +396,74 @@ def _try_following_alias_for_commands(commands: List[str]) -> List[str]:
346
396
  return res
347
397
 
348
398
 
399
+ @functools.cache
400
+ def _maybe_complain_about_sanitization(command: str) -> None:
401
+ if not is_cxx_sanitizer_command(command):
402
+ return
403
+ if sys.platform != 'darwin':
404
+ return
405
+
406
+ version_output = _get_cxx_version_output(command)
407
+ if version_output is None:
408
+ return
409
+ lines = version_output.splitlines()
410
+ if not lines:
411
+ return
412
+ if 'gcc' in lines[-1]:
413
+ console.print(
414
+ '[error]Notice you are using sanitizers in [item]MacOS[/item], but your C/C++ compiler is [item]gcc[/item].[/error]'
415
+ )
416
+ console.print('[error]GCC does not support sanitization in MacOS.[/error]')
417
+ console.print(
418
+ '[warning]See [item]https://rsalesc.github.io/rbx/cpp-on-macos[/item] for instructions on how to use C/C++ sanitizers on MacOS.[/warning]'
419
+ )
420
+ raise typer.Exit(1)
421
+
422
+
423
+ def _check_for_sanitizer_warnings_in_line(line: str) -> bool:
424
+ line = line.lower()
425
+ return 'runtime error:' in line or '==error' in line
426
+
427
+
428
+ def _check_for_sanitizer_warnings(
429
+ sandbox: SandboxBase, stderr_file: Optional[pathlib.Path]
430
+ ) -> bool:
431
+ if stderr_file is None:
432
+ return False
433
+ if not sandbox.file_exists(stderr_file):
434
+ return False
435
+ with sandbox.get_file(stderr_file) as f:
436
+ return any(_check_for_sanitizer_warnings_in_line(line.decode()) for line in f)
437
+
438
+
439
+ _WARNING_RE = re.compile(r'[^:]+:\d+:\d+:[ ]+warning:.*')
440
+
441
+
442
+ def _check_for_compilation_warnings_in_line(line: str) -> bool:
443
+ if line.startswith('./'):
444
+ return False
445
+ matched = _WARNING_RE.match(line) is not None
446
+ # if matched:
447
+ # console.print(
448
+ # '[warning]Compilation warning:[/warning]',
449
+ # utils.highlight_json_obj(line),
450
+ # )
451
+ return matched
452
+
453
+
454
+ def _check_for_compilation_warnings(
455
+ sandbox: SandboxBase, stderr_file: Optional[pathlib.Path]
456
+ ) -> bool:
457
+ if stderr_file is None:
458
+ return False
459
+ if not sandbox.file_exists(stderr_file):
460
+ return False
461
+ with sandbox.get_file(stderr_file) as f:
462
+ return any(
463
+ _check_for_compilation_warnings_in_line(line.strip().decode()) for line in f
464
+ )
465
+
466
+
349
467
  def compile(
350
468
  commands: List[str],
351
469
  params: SandboxParams,
@@ -366,6 +484,7 @@ def compile(
366
484
  sandbox.set_params(params)
367
485
 
368
486
  for i, command in enumerate(commands):
487
+ _maybe_complain_about_sanitization(command)
369
488
  cmd = _split_and_expand(command, sandbox)
370
489
  stdout_file = pathlib.PosixPath(f'compile-{i}.stdout')
371
490
  stderr_file = pathlib.PosixPath(f'compile-{i}.stderr')
@@ -399,6 +518,7 @@ def compile(
399
518
  exitstatus=sandbox.get_exit_status(),
400
519
  time=sandbox.get_execution_time(),
401
520
  memory=sandbox.get_memory_used(),
521
+ warnings=_check_for_compilation_warnings(sandbox, stderr_file),
402
522
  log='\n'.join(std_outputs),
403
523
  )
404
524
  logs.append(log)
@@ -406,6 +526,9 @@ def compile(
406
526
  if log.exitcode != 0:
407
527
  break
408
528
 
529
+ if artifacts.logs is not None:
530
+ artifacts.logs.preprocess = logs
531
+
409
532
  if logs and logs[-1].exitcode != 0:
410
533
  console.print(
411
534
  '[error]FAILED[/error] Preprocessing failed with command',
@@ -455,6 +578,11 @@ def run(
455
578
  memory=sandbox.get_memory_used(),
456
579
  metadata=metadata,
457
580
  )
581
+ if metadata is not None and metadata.is_sanitized:
582
+ run_log.warnings = _check_for_sanitizer_warnings(
583
+ sandbox,
584
+ params.stderr_file,
585
+ )
458
586
  if artifacts.logs is not None:
459
587
  artifacts.logs.run = run_log.model_copy()
460
588
  return run_log
@@ -18,6 +18,8 @@ def compile(
18
18
  artifacts: GradingArtifacts,
19
19
  dependency_cache: DependencyCache,
20
20
  ):
21
+ artifacts.logs = GradingLogsHolder()
22
+
21
23
  ok = True
22
24
  with dependency_cache(
23
25
  commands, [artifacts], params.get_cacheable_params()
@@ -0,0 +1,31 @@
1
+ # Whether to enable warnings when running solutions.
2
+ warnings:
3
+ enabled: true
4
+
5
+ # A list of command substitutions to apply to rbx.
6
+ # Useful when replacing compilers in OS such as Mac.
7
+ command_substitutions:
8
+ g++: g++
9
+ gcc: gcc
10
+ java: java
11
+ javac: javac
12
+ jar: jar
13
+ python: python
14
+ python2: python2
15
+ python3: python3
16
+
17
+ # Whether sanitizers will be enabled by default
18
+ # when running testlib components.
19
+ # This flag has no effect on running solutions with
20
+ # sanitizers. For this, you have to use the `-s` flag in `rbx run`.
21
+ sanitizers:
22
+ enabled: false
23
+
24
+ # A list of command substitutions to apply to rbx when
25
+ # sanitizers are enabled.
26
+ #
27
+ # This is useful when replacing compilers in OS such as Mac,
28
+ # since GCC on Mac does not support sanitizers.
29
+ command_substitutions:
30
+ g++: clang++
31
+ gcc: clang
@@ -0,0 +1,29 @@
1
+ # Whether to enable warnings when running solutions.
2
+ warnings:
3
+ enabled: true
4
+
5
+ # A list of command substitutions to apply to rbx.
6
+ # Useful when replacing compilers in OS such as Mac.
7
+ command_substitutions:
8
+ g++: g++
9
+ gcc: gcc
10
+ java: java
11
+ javac: javac
12
+ jar: jar
13
+ python: python
14
+ python2: python2
15
+ python3: python3
16
+
17
+ # Whether sanitizers will be enabled by default
18
+ # when running testlib components.
19
+ # This flag has no effect on running solutions with
20
+ # sanitizers. For this, you have to use the `-s` flag in `rbx run`.
21
+ sanitizers:
22
+ enabled: false
23
+
24
+ # A list of command substitutions to apply to rbx when
25
+ # sanitizers are enabled.
26
+ #
27
+ # This is useful when replacing compilers in OS such as Mac,
28
+ # since GCC on Mac does not support sanitizers.
29
+ command_substitutions: {}
rbx/utils.py CHANGED
@@ -9,6 +9,7 @@ from typing import Any, Optional, Type, TypeVar
9
9
  import rich
10
10
  import rich.prompt
11
11
  import rich.status
12
+ import ruyaml
12
13
  import typer
13
14
  import yaml
14
15
  from fastapi.encoders import jsonable_encoder
@@ -91,6 +92,12 @@ def validate_field(model: Type[T], field: str, value: Any):
91
92
  )
92
93
 
93
94
 
95
+ def save_ruyaml(path: pathlib.Path, yml: ruyaml.YAML, data: ruyaml.Any):
96
+ path.parent.mkdir(parents=True, exist_ok=True)
97
+ with path.open('w') as f:
98
+ yml.dump(data, f)
99
+
100
+
94
101
  def confirm_on_status(status: Optional[rich.status.Status], *args, **kwargs) -> bool:
95
102
  if status:
96
103
  status.stop()
@@ -113,7 +120,7 @@ def get_open_fds():
113
120
 
114
121
 
115
122
  @contextlib.contextmanager
116
- def new_cd(x):
123
+ def new_cd(x: pathlib.Path):
117
124
  d = os.getcwd()
118
125
 
119
126
  # This could raise an exception, but it's probably
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rbx.cp
3
- Version: 0.5.16
3
+ Version: 0.5.18
4
4
  Summary:
5
5
  Author: Roberto Sales
6
6
  Requires-Python: >=3.9,<4.0
@@ -27,6 +27,7 @@ Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
27
27
  Requires-Dist: questionary (>=2.1.0,<3.0.0)
28
28
  Requires-Dist: requests (>=2.32.3,<3.0.0)
29
29
  Requires-Dist: rich (>=13.9.4,<14.0.0)
30
+ Requires-Dist: ruyaml (>=0.91.0,<0.92.0)
30
31
  Requires-Dist: textual (>=0.79.1,<0.80.0)
31
32
  Requires-Dist: typer (>=0.15.1,<0.16.0)
32
33
  Description-Content-Type: text/markdown