rbx.cp 0.13.4__py3-none-any.whl → 0.13.6__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.
Files changed (42) hide show
  1. rbx/box/checkers.py +2 -9
  2. rbx/box/cli.py +0 -1
  3. rbx/box/code.py +27 -80
  4. rbx/box/environment.py +16 -6
  5. rbx/box/generators.py +26 -3
  6. rbx/box/global_package.py +1 -1
  7. rbx/box/header.py +26 -8
  8. rbx/box/package.py +0 -14
  9. rbx/box/setter_config.py +11 -0
  10. rbx/box/solutions.py +12 -4
  11. rbx/box/tasks.py +9 -4
  12. rbx/box/testing/testing_package.py +69 -2
  13. rbx/box/ui/screens/run_explorer.py +0 -8
  14. rbx/box/ui/utils/run_ui.py +7 -3
  15. rbx/box/ui/widgets/test_output_box.py +1 -1
  16. rbx/box/unit.py +4 -4
  17. rbx/box/validators.py +3 -1
  18. rbx/grading/caching.py +65 -15
  19. rbx/grading/judge/cacher.py +5 -3
  20. rbx/grading/judge/program.py +300 -0
  21. rbx/grading/judge/sandbox.py +30 -200
  22. rbx/grading/judge/sandboxes/stupid_sandbox.py +234 -240
  23. rbx/grading/judge/sandboxes/tee.py +31 -0
  24. rbx/grading/judge/storage.py +7 -1
  25. rbx/grading/steps.py +89 -201
  26. rbx/grading/steps_with_caching.py +15 -6
  27. rbx/resources/presets/default/problem/problem.rbx.yml +0 -2
  28. rbx/resources/templates/rbx.h +43 -2
  29. rbx/testing_utils.py +7 -0
  30. rbx/utils.py +104 -6
  31. {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/METADATA +1 -1
  32. {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/RECORD +35 -40
  33. rbx/grading/judge/sandboxes/isolate.py +0 -695
  34. rbx/grading/judge/sandboxes/timeit.py +0 -358
  35. rbx/grading/judge/test.py +0 -38
  36. rbx/grading/judge/testiso.py +0 -54
  37. rbx/grading/processing_context.py +0 -71
  38. rbx/resources/envs/isolate.rbx.yml +0 -36
  39. rbx/resources/presets/default/problem/sols/slow.cpp +0 -15
  40. {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/LICENSE +0 -0
  41. {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/WHEEL +0 -0
  42. {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/entry_points.txt +0 -0
@@ -1,358 +0,0 @@
1
- import dataclasses
2
- import os
3
- import pathlib
4
- import resource
5
- import signal
6
- import stat
7
- import sys
8
- import threading
9
- from time import monotonic
10
- from typing import Any, Dict, List, Optional, Set, Union
11
-
12
-
13
- @dataclasses.dataclass
14
- class Options:
15
- output_file: str
16
- argv: List[str]
17
- chdir: Optional[str] = None
18
- stdin_file: Optional[str] = None
19
- stdout_file: Optional[str] = None
20
- stderr_file: Optional[str] = None
21
- time_limit: Optional[float] = None
22
- wall_time_limit: Optional[float] = None # seconds
23
- memory_limit: Optional[int] = None # kb, but passed in args as mb
24
- fs_limit: Optional[int] = None # kb
25
- files_to_open: List[int] = dataclasses.field(default_factory=list)
26
- file_duplicates: Dict[int, List[str]] = dataclasses.field(default_factory=dict)
27
- prefixed: Set[str] = dataclasses.field(default_factory=set)
28
- prefix: str = ''
29
- process_group: Optional[int] = None
30
-
31
-
32
- def exit_with(code: int):
33
- sys.exit(code)
34
-
35
-
36
- def get_tee_command(files: List[str]) -> str:
37
- path = (
38
- os.path.join(os.path.dirname(os.path.realpath(__file__)), 'tee.py')
39
- + ' '
40
- + ' '.join(files)
41
- )
42
- return sys.executable + ' ' + path
43
-
44
-
45
- valid_modes = ['a', 'w']
46
-
47
-
48
- @dataclasses.dataclass
49
- class Tee:
50
- file: Any
51
- prefix: Union[str, bytes] = ''
52
-
53
-
54
- def create_tee(files, mode, buffer_size=4096, prefix=''):
55
- """Get a file object that will mirror writes across multiple files objs
56
-
57
- Options:
58
- files A list of files and/or file objects. All strings will be
59
- treated as file paths and opened for writing. Everything
60
- else is assumed to be a file-like object that implements
61
- both the write() and flush() methods.
62
-
63
- mode Which mode to use when opening new files. Valid values
64
- are 'a' (append) and 'w' (overwrite).
65
-
66
- buffer_size
67
- Control the size of the buffer between writes to the
68
- resulting file object and the list of files.
69
- """
70
- if mode not in valid_modes:
71
- raise IOError(
72
- 'Only valid modes to create_tee() are: %s' % ', '.join(valid_modes)
73
- )
74
-
75
- tee_list = []
76
- for file in files:
77
- if isinstance(file, Tee):
78
- tee_list.append(file)
79
- else:
80
- tee_list.append(Tee(file))
81
- for tee in tee_list:
82
- if isinstance(tee.file, str):
83
- tee.file = open(tee.file, f'{mode}b')
84
- if isinstance(tee.prefix, str):
85
- tee.prefix = tee.prefix.encode()
86
-
87
- pipe_read, pipe_write = os.pipe()
88
- pid = os.fork()
89
- if pid == 0:
90
- # Child -- Read bytes from the pipe and write them to the specified
91
- # files.
92
- try:
93
- # Close parent's end of the pipe
94
- os.close(pipe_write)
95
-
96
- new = True
97
- while bytes := os.read(pipe_read, 1):
98
- for tee in tee_list:
99
- if tee.prefix and new:
100
- tee.file.write(tee.prefix)
101
- tee.file.write(bytes)
102
- tee.file.flush()
103
- # TODO maybe add in fsync() here if the fileno() method
104
- # exists on file
105
- new = bytes == b'\n'
106
- except Exception:
107
- pass
108
- finally:
109
- os._exit(255)
110
- else:
111
- # Parent -- Return a file object wrapper around the pipe to the
112
- # child.
113
- # Preserve line buffering (buffering=1).
114
- return os.fdopen(pipe_write, 'w', buffering=1, closefd=False)
115
-
116
-
117
- def parse_opts() -> Options:
118
- options = Options(output_file=sys.argv[1], argv=[])
119
- options.files_to_open = []
120
- num_opts = 0
121
- while num_opts + 2 < len(sys.argv) and sys.argv[num_opts + 2].startswith('-'):
122
- # Process option
123
- opt = sys.argv[num_opts + 2]
124
- if opt.startswith('-t'):
125
- options.time_limit = float(opt[2:])
126
- elif opt.startswith('-w'):
127
- options.wall_time_limit = float(opt[2:])
128
- elif opt.startswith('-m'):
129
- options.memory_limit = int(opt[2:]) * 1024
130
- elif opt.startswith('-i'):
131
- options.stdin_file = opt[2:]
132
- options.files_to_open.append(0)
133
- elif opt.startswith('-o'):
134
- options.stdout_file = opt[2:]
135
- options.files_to_open.append(1)
136
- elif opt.startswith('-e'):
137
- options.stderr_file = opt[2:]
138
- options.files_to_open.append(2)
139
- elif opt.startswith('-d') or opt.startswith('-D'):
140
- is_prefixed = opt.startswith('-D')
141
- possibilities = [None, 'o', 'e']
142
- index = possibilities.index(opt[2])
143
- if index not in options.file_duplicates:
144
- options.file_duplicates[index] = []
145
- options.file_duplicates[index].append(opt[3:])
146
- if is_prefixed:
147
- options.prefixed.add(opt[3:])
148
- elif opt.startswith('-c'):
149
- options.chdir = opt[2:]
150
- elif opt.startswith('-f'):
151
- options.fs_limit = int(opt[2:])
152
- elif opt.startswith('-P'):
153
- options.prefix = opt[2:]
154
- elif opt.startswith('-g'):
155
- options.process_group = int(opt[2:])
156
- else:
157
- raise Exception(f'Invalid option {opt}')
158
- num_opts += 1
159
- options.argv = sys.argv[num_opts + 2 :]
160
- return options
161
-
162
-
163
- def get_memory_usage(ru: resource.struct_rusage) -> int:
164
- if sys.platform == 'darwin':
165
- return ru.ru_maxrss // 1024 + ru.ru_ixrss
166
- return ru.ru_maxrss + ru.ru_ixrss + ru.ru_idrss + ru.ru_isrss
167
-
168
-
169
- def get_cpu_time(ru: resource.struct_rusage) -> float:
170
- return ru.ru_utime + ru.ru_stime
171
-
172
-
173
- def _get_file_size(filename: Optional[str]) -> int:
174
- if filename is None:
175
- return 0
176
- path = pathlib.Path(filename)
177
- if not path.is_file():
178
- return 0
179
- return path.stat().st_size
180
-
181
-
182
- def get_file_sizes(options: Options):
183
- return _get_file_size(options.stdout_file) + _get_file_size(options.stderr_file)
184
-
185
-
186
- def set_rlimits(options: Options):
187
- if options.time_limit is not None:
188
- time_limit_in_ms = int(options.time_limit * 1000)
189
- rlimit_cpu = int((time_limit_in_ms + 999) // 1000)
190
- resource.setrlimit(resource.RLIMIT_CPU, (rlimit_cpu, rlimit_cpu + 1))
191
- if options.fs_limit is not None:
192
- fs_limit = options.fs_limit * 1024 # in bytes
193
- resource.setrlimit(resource.RLIMIT_FSIZE, (fs_limit + 1, fs_limit * 2))
194
-
195
-
196
- def redirect_fds(options: Options):
197
- files = [options.stdin_file, options.stdout_file, options.stderr_file]
198
-
199
- for i in options.files_to_open:
200
- file = files[i]
201
- if file is None:
202
- continue
203
- open_args = [
204
- os.O_WRONLY | os.O_TRUNC | os.O_CREAT,
205
- stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IWUSR,
206
- ]
207
- if i == 0:
208
- # stdin
209
- open_args = [os.O_RDONLY]
210
- if i in options.file_duplicates:
211
- dups = [
212
- Tee(f, prefix=options.prefix if f in options.prefixed else '')
213
- for f in options.file_duplicates[i]
214
- ]
215
- tee = create_tee(dups + [file], 'a', prefix=options.prefix)
216
- fd = tee.fileno()
217
- else:
218
- fd = os.open(
219
- file,
220
- *open_args,
221
- )
222
- os.dup2(fd, i)
223
- os.close(fd)
224
-
225
-
226
- def wait_and_finish(
227
- pid: int,
228
- options: Options,
229
- start_time: float,
230
- status_holder: Set[str],
231
- alarm_msg: Optional[List[Optional[str]]] = None,
232
- ):
233
- _, exitstatus, ru = os.wait4(pid, 0)
234
- wall_time = monotonic() - start_time
235
- cpu_time = get_cpu_time(ru)
236
- memory_used = get_memory_usage(ru)
237
- file_sizes = get_file_sizes(options)
238
-
239
- entries = []
240
- exitcode = os.waitstatus_to_exitcode(exitstatus)
241
- entries.append(f'exit-code: {exitcode}')
242
- if exitcode < 0:
243
- entries.append(f'exit-sig: {-exitcode}')
244
-
245
- status = status_holder
246
- if exitcode > 0:
247
- status.add('RE')
248
- if exitcode < 0:
249
- status.add('SG')
250
- if options.time_limit is not None and (
251
- cpu_time > options.time_limit or -exitcode == 24
252
- ):
253
- status.add('TO')
254
- cpu_time = max(cpu_time, options.time_limit)
255
- if options.wall_time_limit is not None and wall_time > options.wall_time_limit:
256
- status.add('WT')
257
- status.add('TO')
258
- if options.memory_limit is not None and memory_used > options.memory_limit:
259
- status.add('ML')
260
- if options.fs_limit is not None and file_sizes > options.fs_limit * 1024:
261
- status.add('OL')
262
-
263
- if status:
264
- status_str = ','.join(status)
265
- entries.append(f'status: {status_str}')
266
-
267
- if alarm_msg:
268
- alarm_str = ','.join(msg for msg in alarm_msg if msg is not None)
269
- if alarm_str:
270
- entries.append(f'alarm-msg: {alarm_str}')
271
-
272
- entries.append(f'time: {cpu_time:.3f}')
273
- entries.append(f'time-wall: {wall_time:.3f}')
274
- entries.append(f'mem: {memory_used}')
275
- entries.append(f'file: {file_sizes}')
276
- entries.append(f'pid: {pid}')
277
-
278
- output_file = pathlib.Path(sys.argv[1])
279
- output_file.parent.mkdir(parents=True, exist_ok=True)
280
- output_file.write_text('\n'.join(entries) + '\n')
281
-
282
-
283
- def main():
284
- options = parse_opts()
285
-
286
- if options.process_group is not None:
287
- os.setpgid(0, options.process_group)
288
-
289
- start_time = monotonic()
290
- sub_pid = os.fork()
291
- if sub_pid == 0:
292
- if options.chdir is not None:
293
- os.chdir(options.chdir)
294
- set_rlimits(options)
295
- redirect_fds(options)
296
- signal.signal(signal.SIGPIPE, signal.SIG_DFL)
297
- os.execvp(options.argv[0], options.argv)
298
-
299
- alarm_msg: List[Optional[str]] = [None]
300
- status_holder: Set[str] = set()
301
-
302
- stop_wall_handler = threading.Event()
303
- stop_alarm_handler = threading.Event()
304
-
305
- def handle_wall():
306
- if stop_wall_handler.wait(options.wall_time_limit):
307
- return
308
- stop_alarm_handler.set()
309
- nonlocal alarm_msg
310
- alarm_msg[0] = 'wall timelimit'
311
- os.kill(sub_pid, 9)
312
-
313
- def handle_alarm():
314
- if stop_alarm_handler.wait(0.3):
315
- return
316
- nonlocal alarm_msg
317
- ru = resource.getrusage(resource.RUSAGE_CHILDREN)
318
- if options.time_limit is not None:
319
- cpu_time = get_cpu_time(ru)
320
- if cpu_time > options.time_limit:
321
- alarm_msg[0] = 'timelimit'
322
- os.kill(sub_pid, 9)
323
- return
324
- if options.memory_limit is not None:
325
- memory_used = get_memory_usage(ru)
326
- if memory_used > options.memory_limit:
327
- alarm_msg[0] = 'memorylimit'
328
- os.kill(sub_pid, 9)
329
- return
330
-
331
- stop_alarm_handler.clear()
332
- handle_alarm()
333
-
334
- alarm_handler = threading.Thread(target=handle_alarm, daemon=True)
335
- wall_handler = threading.Thread(target=handle_wall, daemon=True)
336
- alarm_handler.start()
337
- wall_handler.start()
338
-
339
- def handle_sub_term(*args, **kwargs):
340
- nonlocal status_holder
341
- status_holder.add('TE')
342
- os.kill(sub_pid, 9)
343
-
344
- signal.signal(signal.SIGTERM, handle_sub_term)
345
-
346
- wait_and_finish(sub_pid, options, start_time, status_holder, alarm_msg=alarm_msg)
347
-
348
- # Process finished, stop the handlers.
349
- stop_wall_handler.set()
350
- stop_alarm_handler.set()
351
-
352
- # Exit gracefully.
353
- sys.exit(0)
354
-
355
-
356
- if __name__ == '__main__':
357
- main()
358
- # type: ignore
rbx/grading/judge/test.py DELETED
@@ -1,38 +0,0 @@
1
- import atexit
2
- import pathlib
3
-
4
- from rich.console import Console
5
-
6
- from rbx.grading.judge import cacher, storage
7
- from rbx.grading.judge.sandboxes import stupid_sandbox
8
-
9
- console = Console()
10
-
11
-
12
- def main():
13
- fs = storage.FilesystemStorage(pathlib.PosixPath('/tmp/rbx-storage'))
14
- cache = cacher.FileCacher(fs)
15
-
16
- python_file = cache.put_file_text("print('hello')")
17
-
18
- sandbox = stupid_sandbox.StupidSandbox(cache)
19
- atexit.register(sandbox.cleanup)
20
- sandbox.create_file_from_storage(pathlib.PosixPath('run.py'), python_file)
21
-
22
- sandbox.params.stdout_file = pathlib.PosixPath('run.out')
23
-
24
- sandbox.execute_without_std(['ls'])
25
- try:
26
- sandbox.hydrate_logs()
27
- except Exception:
28
- console.print_exception()
29
-
30
- print(sandbox.get_human_exit_description())
31
- print(sandbox.get_stats())
32
- print(sandbox.log)
33
-
34
- print(sandbox.get_file_to_string(pathlib.PosixPath('run.out')))
35
-
36
-
37
- if __name__ == '__main__':
38
- main()
@@ -1,54 +0,0 @@
1
- import atexit
2
- import pathlib
3
-
4
- from rich.console import Console
5
-
6
- from rbx import grading_utils
7
- from rbx.grading.judge import cacher, storage
8
- from rbx.grading.judge.sandboxes.isolate import IsolateSandbox
9
-
10
- console = Console()
11
-
12
-
13
- def main():
14
- fs = storage.FilesystemStorage(pathlib.PosixPath('/tmp/rbx-storage'))
15
- cache = cacher.FileCacher(fs)
16
-
17
- python_file = cache.put_file_text(
18
- """
19
- #include <bits/stdc++.h>
20
-
21
- int main() {
22
- std::cout << "Hello, World!" << std::endl;
23
- return 0;
24
- }
25
- """
26
- )
27
-
28
- sandbox = IsolateSandbox(
29
- cache, params=grading_utils.build_preprocess_sandbox_params(), debug=True
30
- )
31
- atexit.register(sandbox.cleanup)
32
- sandbox.create_file_from_storage(pathlib.PosixPath('run.cpp'), python_file)
33
-
34
- sandbox.params.stdout_file = pathlib.PosixPath('run.out')
35
- sandbox.params.stderr_file = pathlib.PosixPath('run.err')
36
-
37
- sandbox.execute_without_std(
38
- ['/usr/bin/g++', '-std=c++17', '-o', 'executable', 'run.cpp'],
39
- )
40
- try:
41
- sandbox.hydrate_logs()
42
- except Exception:
43
- console.print_exception()
44
-
45
- print(sandbox.log)
46
- print(sandbox.get_human_exit_description())
47
- print(sandbox.get_stats())
48
-
49
- print(sandbox.get_file_to_string(pathlib.PosixPath('run.out')))
50
- print(sandbox.get_file_to_string(pathlib.PosixPath('run.err')))
51
-
52
-
53
- if __name__ == '__main__':
54
- main()
@@ -1,71 +0,0 @@
1
- import contextlib
2
- import os
3
- import signal
4
- import subprocess
5
- from typing import List, Optional
6
-
7
- from rbx.grading.judge.sandbox import SandboxBase
8
-
9
-
10
- @contextlib.contextmanager
11
- def new_process_group():
12
- p = subprocess.Popen(['/bin/bash', '-c', 'exec sleep infinity'])
13
- try:
14
- yield p.pid
15
- finally:
16
- p.terminate()
17
- p.wait()
18
-
19
-
20
- def should_use_group(sandboxes: List[SandboxBase]) -> bool:
21
- if not sandboxes:
22
- return False
23
- uses_pgid = all(sandbox.use_pgid() for sandbox in sandboxes)
24
- all_pgids = set(
25
- sandbox.params.pgid for sandbox in sandboxes if sandbox.params.pgid is not None
26
- )
27
- return uses_pgid and len(all_pgids) == 1
28
-
29
-
30
- async def _fetch_pids(sandboxes: List[SandboxBase]) -> List[int]:
31
- return [await sandbox.get_pid() for sandbox in sandboxes]
32
-
33
-
34
- def _find_sandbox_idx(pids: List[int], pid: int) -> Optional[int]:
35
- try:
36
- return pids.index(pid)
37
- except ValueError:
38
- return None
39
-
40
-
41
- async def _wait_for_group(sandboxes: List[SandboxBase]) -> List[int]:
42
- pgid = [
43
- sandbox.params.pgid for sandbox in sandboxes if sandbox.params.pgid is not None
44
- ][0]
45
- assert pgid is not None
46
-
47
- sandbox_pids = await _fetch_pids(sandboxes)
48
-
49
- finished = []
50
- while len(finished) < len(sandboxes):
51
- try:
52
- pid, status = os.waitpid(-pgid, 0)
53
- except ChildProcessError:
54
- break
55
-
56
- if os.waitstatus_to_exitcode(status) != 0:
57
- os.kill(pgid, signal.SIGKILL)
58
-
59
- sandbox_idx = _find_sandbox_idx(sandbox_pids, pid)
60
- if sandbox_idx is not None:
61
- finished.append(sandbox_idx)
62
- continue
63
-
64
- return finished
65
-
66
-
67
- async def wait_all(sandboxes: List[SandboxBase]):
68
- if not should_use_group(sandboxes):
69
- raise RuntimeError('Sandboxes are not using a process group')
70
-
71
- await _wait_for_group(sandboxes)
@@ -1,36 +0,0 @@
1
- ---
2
- sandbox: "isolate"
3
- defaultCompilation:
4
- sandbox:
5
- maxProcesses: null
6
- timeLimit: null # 10 seconds
7
- wallTimeLimit: null # 10 seconds
8
- memoryLimit: null # 1gb
9
- preserveEnv: true
10
- mirrorDirs:
11
- - "/etc"
12
- - "/usr"
13
- defaultExecution:
14
- sandbox:
15
- # Useful for checkers, validators, etc.
16
- timeLimit: 10000 # 10 seconds
17
- wallTimeLimit: 10000 # 10 seconds
18
- memoryLimit: 1024 # 1gb
19
- languages:
20
- - name: "cpp"
21
- readableName: "C++17"
22
- extension: "cpp"
23
- compilation:
24
- commands:
25
- - "/usr/bin/g++ -std=c++17 -O2 -o {executable} {compilable}"
26
- execution:
27
- command: "./{executable}"
28
- fileMapping:
29
- compilable: "compilable.cpp"
30
- - name: "py"
31
- readableName: "Python3"
32
- extension: "py"
33
- execution:
34
- command: "/usr/bin/python3 {executable}"
35
- fileMapping:
36
- executable: "executable.py"
@@ -1,15 +0,0 @@
1
- #include <bits/stdc++.h>
2
-
3
- using namespace std;
4
-
5
- int32_t main() {
6
- int64_t a, b;
7
- cin >> a >> b;
8
-
9
- int64_t i;
10
- // Unncessarily slow.
11
- for (int k = 0; k < 10; k++)
12
- for (i = 0; i < a + b; i++) {}
13
-
14
- cout << i << endl;
15
- }