rbx.cp 0.5.0__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 (164) hide show
  1. rbx/__init__.py +0 -0
  2. rbx/annotations.py +127 -0
  3. rbx/autoenum.py +333 -0
  4. rbx/box/__init__.py +0 -0
  5. rbx/box/builder.py +77 -0
  6. rbx/box/cd.py +37 -0
  7. rbx/box/checkers.py +134 -0
  8. rbx/box/code.py +185 -0
  9. rbx/box/compile.py +56 -0
  10. rbx/box/conftest.py +42 -0
  11. rbx/box/contest/__init__.py +0 -0
  12. rbx/box/contest/build_contest_statements.py +347 -0
  13. rbx/box/contest/contest_package.py +76 -0
  14. rbx/box/contest/contest_utils.py +20 -0
  15. rbx/box/contest/main.py +179 -0
  16. rbx/box/contest/schema.py +155 -0
  17. rbx/box/contest/statements.py +82 -0
  18. rbx/box/creation.py +72 -0
  19. rbx/box/download.py +64 -0
  20. rbx/box/environment.py +345 -0
  21. rbx/box/extensions.py +26 -0
  22. rbx/box/generators.py +478 -0
  23. rbx/box/generators_test.py +63 -0
  24. rbx/box/main.py +449 -0
  25. rbx/box/package.py +316 -0
  26. rbx/box/packaging/boca/extension.py +27 -0
  27. rbx/box/packaging/boca/packager.py +245 -0
  28. rbx/box/packaging/contest_main.py +82 -0
  29. rbx/box/packaging/main.py +68 -0
  30. rbx/box/packaging/packager.py +117 -0
  31. rbx/box/packaging/polygon/packager.py +320 -0
  32. rbx/box/packaging/polygon/test.py +81 -0
  33. rbx/box/packaging/polygon/xml_schema.py +106 -0
  34. rbx/box/presets/__init__.py +503 -0
  35. rbx/box/presets/fetch.py +70 -0
  36. rbx/box/presets/lock_schema.py +20 -0
  37. rbx/box/presets/schema.py +59 -0
  38. rbx/box/schema.py +394 -0
  39. rbx/box/solutions.py +792 -0
  40. rbx/box/solutions_test.py +41 -0
  41. rbx/box/statements/__init__.py +0 -0
  42. rbx/box/statements/build_statements.py +359 -0
  43. rbx/box/statements/builders.py +375 -0
  44. rbx/box/statements/joiners.py +113 -0
  45. rbx/box/statements/latex.py +47 -0
  46. rbx/box/statements/latex_jinja.py +214 -0
  47. rbx/box/statements/schema.py +138 -0
  48. rbx/box/stresses.py +292 -0
  49. rbx/box/stressing/__init__.py +0 -0
  50. rbx/box/stressing/finder_parser.py +359 -0
  51. rbx/box/stressing/generator_parser.py +258 -0
  52. rbx/box/testcases.py +54 -0
  53. rbx/box/ui/__init__.py +0 -0
  54. rbx/box/ui/captured_log.py +372 -0
  55. rbx/box/ui/css/app.tcss +48 -0
  56. rbx/box/ui/main.py +38 -0
  57. rbx/box/ui/run.py +209 -0
  58. rbx/box/validators.py +245 -0
  59. rbx/box/validators_test.py +15 -0
  60. rbx/checker.py +128 -0
  61. rbx/clone.py +197 -0
  62. rbx/config.py +271 -0
  63. rbx/conftest.py +38 -0
  64. rbx/console.py +27 -0
  65. rbx/create.py +37 -0
  66. rbx/edit.py +24 -0
  67. rbx/grading/__init__.py +0 -0
  68. rbx/grading/caching.py +356 -0
  69. rbx/grading/conftest.py +33 -0
  70. rbx/grading/judge/__init__.py +0 -0
  71. rbx/grading/judge/cacher.py +503 -0
  72. rbx/grading/judge/digester.py +35 -0
  73. rbx/grading/judge/sandbox.py +748 -0
  74. rbx/grading/judge/sandboxes/__init__.py +0 -0
  75. rbx/grading/judge/sandboxes/isolate.py +683 -0
  76. rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
  77. rbx/grading/judge/sandboxes/timeit.py +217 -0
  78. rbx/grading/judge/storage.py +284 -0
  79. rbx/grading/judge/test.py +38 -0
  80. rbx/grading/judge/testiso.py +54 -0
  81. rbx/grading/steps.py +522 -0
  82. rbx/grading/steps_with_caching.py +59 -0
  83. rbx/grading/steps_with_caching_run_test.py +429 -0
  84. rbx/grading_utils.py +148 -0
  85. rbx/hydration.py +101 -0
  86. rbx/main.py +122 -0
  87. rbx/metadata.py +105 -0
  88. rbx/providers/__init__.py +43 -0
  89. rbx/providers/codeforces.py +73 -0
  90. rbx/providers/provider.py +26 -0
  91. rbx/resources/checkers/boilerplate.cpp +20 -0
  92. rbx/resources/default_config.json +48 -0
  93. rbx/resources/envs/default.rbx.yml +37 -0
  94. rbx/resources/envs/isolate.rbx.yml +37 -0
  95. rbx/resources/packagers/boca/checker.sh +43 -0
  96. rbx/resources/packagers/boca/compare +53 -0
  97. rbx/resources/packagers/boca/compile/c +172 -0
  98. rbx/resources/packagers/boca/compile/cc +173 -0
  99. rbx/resources/packagers/boca/compile/cpp +172 -0
  100. rbx/resources/packagers/boca/compile/java +194 -0
  101. rbx/resources/packagers/boca/compile/kt +155 -0
  102. rbx/resources/packagers/boca/compile/pas +172 -0
  103. rbx/resources/packagers/boca/compile/py2 +173 -0
  104. rbx/resources/packagers/boca/compile/py3 +173 -0
  105. rbx/resources/packagers/boca/run/c +128 -0
  106. rbx/resources/packagers/boca/run/cc +128 -0
  107. rbx/resources/packagers/boca/run/cpp +128 -0
  108. rbx/resources/packagers/boca/run/java +194 -0
  109. rbx/resources/packagers/boca/run/kt +159 -0
  110. rbx/resources/packagers/boca/run/py2 +166 -0
  111. rbx/resources/packagers/boca/run/py3 +166 -0
  112. rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
  113. rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
  114. rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
  115. rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
  116. rbx/resources/presets/default/preset.rbx.yml +12 -0
  117. rbx/resources/presets/default/problem/.gitignore +6 -0
  118. rbx/resources/presets/default/problem/gen.cpp +9 -0
  119. rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
  120. rbx/resources/presets/default/problem/random.py +3 -0
  121. rbx/resources/presets/default/problem/random.txt +2 -0
  122. rbx/resources/presets/default/problem/sols/main.cpp +9 -0
  123. rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
  124. rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
  125. rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
  126. rbx/resources/presets/default/problem/statement/projecao.png +0 -0
  127. rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
  128. rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
  129. rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
  130. rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
  131. rbx/resources/presets/default/problem/validator.cpp +16 -0
  132. rbx/resources/presets/default/problem/wcmp.cpp +34 -0
  133. rbx/resources/templates/template.cpp +19 -0
  134. rbx/run.py +45 -0
  135. rbx/schema.py +64 -0
  136. rbx/submit.py +61 -0
  137. rbx/submitors/__init__.py +18 -0
  138. rbx/submitors/codeforces.py +120 -0
  139. rbx/submitors/submitor.py +25 -0
  140. rbx/test.py +347 -0
  141. rbx/testcase.py +70 -0
  142. rbx/testcase_rendering.py +79 -0
  143. rbx/testdata/box1/gen1.cpp +7 -0
  144. rbx/testdata/box1/gen2.cpp +9 -0
  145. rbx/testdata/box1/genScript.py +2 -0
  146. rbx/testdata/box1/hard-tle.sol.cpp +26 -0
  147. rbx/testdata/box1/ole.cpp +17 -0
  148. rbx/testdata/box1/problem.rbx.yml +39 -0
  149. rbx/testdata/box1/re.sol.cpp +23 -0
  150. rbx/testdata/box1/sol.cpp +22 -0
  151. rbx/testdata/box1/tests/1.in +1 -0
  152. rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
  153. rbx/testdata/box1/tle.sol.cpp +35 -0
  154. rbx/testdata/box1/validator.cpp +11 -0
  155. rbx/testdata/box1/wa.sol.cpp +22 -0
  156. rbx/testdata/caching/executable.py +1 -0
  157. rbx/testdata/compatible +0 -0
  158. rbx/testing_utils.py +65 -0
  159. rbx/utils.py +162 -0
  160. rbx_cp-0.5.0.dist-info/LICENSE +201 -0
  161. rbx_cp-0.5.0.dist-info/METADATA +89 -0
  162. rbx_cp-0.5.0.dist-info/RECORD +164 -0
  163. rbx_cp-0.5.0.dist-info/WHEEL +4 -0
  164. rbx_cp-0.5.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,748 @@
1
+ import abc
2
+ import dataclasses
3
+ import io
4
+ import logging
5
+ import os
6
+ import pathlib
7
+ import select
8
+ import stat
9
+ import subprocess
10
+ import sys
11
+ import typing
12
+ from typing import IO, Any, Dict, List, Optional
13
+
14
+ import pydantic
15
+
16
+ from rbx.grading.judge import cacher, storage
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ MERGE_STDERR = pathlib.PosixPath('/dev/stdout')
21
+
22
+
23
+ def wait_without_std(
24
+ procs: List[subprocess.Popen], actually_pipe_to_stdout: bool = False
25
+ ) -> List[int]:
26
+ """Wait for the conclusion of the processes in the list, avoiding
27
+ starving for input and output.
28
+
29
+ procs (list): a list of processes as returned by Popen.
30
+
31
+ return (list): a list of return codes.
32
+
33
+ """
34
+
35
+ def get_to_consume():
36
+ """Amongst stdout and stderr of list of processes, find the
37
+ ones that are alive and not closed (i.e., that may still want
38
+ to write to).
39
+
40
+ return (list): a list of open streams.
41
+
42
+ """
43
+ to_consume = []
44
+ for process in procs:
45
+ if process.poll() is None: # If the process is alive.
46
+ if process.stdout and not process.stdout.closed:
47
+ to_consume.append(process.stdout)
48
+ if process.stderr and not process.stderr.closed:
49
+ to_consume.append(process.stderr)
50
+ return to_consume
51
+
52
+ # Close stdin; just saying stdin=None isn't ok, because the
53
+ # standard input would be obtained from the application stdin,
54
+ # that could interfere with the child process behaviour
55
+ for process in procs:
56
+ if process.stdin:
57
+ process.stdin.close()
58
+
59
+ # Read stdout and stderr to the end without having to block
60
+ # because of insufficient buffering (and without allocating too
61
+ # much memory). Unix specific.
62
+ to_consume = get_to_consume()
63
+
64
+ while len(to_consume) > 0:
65
+ to_read = select.select(to_consume, [], [], 1.0)[0]
66
+ for file_ in to_read:
67
+ consumed = file_.read(8 * 1024)
68
+ if actually_pipe_to_stdout:
69
+ sys.stdout.buffer.write(consumed)
70
+ sys.stdout.buffer.flush()
71
+ to_consume = get_to_consume()
72
+
73
+ return [process.wait() for process in procs]
74
+
75
+
76
+ @dataclasses.dataclass
77
+ class DirectoryMount:
78
+ src: pathlib.Path
79
+ dst: pathlib.Path
80
+ options: Optional[str] = None
81
+
82
+
83
+ class SandboxParams(pydantic.BaseModel):
84
+ """Parameters for the sandbox.
85
+
86
+ box_id (int): the id of the sandbox.
87
+ fsize (int|None): maximum file size.
88
+ cgroup (bool): whether to use cgroups.
89
+ dirs ([string]): directories to mount.
90
+ preserve_env (bool): whether to preserve the environment.
91
+ inherit_env ([string]): environment variables to inherit.
92
+ set_env (Dict[string, string]): environment variables to set.
93
+ verbosity (int): verbosity level.
94
+ max_processes (int): maximum number of processes.
95
+
96
+ """
97
+
98
+ fsize: Optional[int] = None # KiB
99
+ cgroup: bool = False
100
+ dirs: List[DirectoryMount] = []
101
+ preserve_env: bool = False
102
+ inherit_env: List[str] = []
103
+ set_env: Dict[str, str] = {}
104
+ verbosity: int = 0
105
+ max_processes: Optional[int] = 1
106
+
107
+ stdin_file: Optional[pathlib.Path] = None
108
+ stdout_file: Optional[pathlib.Path] = None
109
+ stderr_file: Optional[pathlib.Path] = None
110
+ stack_space: Optional[int] = None # MiB
111
+ address_space: Optional[int] = None # MiB
112
+ timeout: Optional[int] = None # ms
113
+ wallclock_timeout: Optional[int] = None # ms
114
+ extra_timeout: Optional[int] = None # ms
115
+
116
+ def get_cacheable_params(self) -> Dict[str, Any]:
117
+ return self.model_dump(mode='json', exclude_unset=True, exclude_none=True)
118
+
119
+ def set_stdio(
120
+ self,
121
+ stdin: Optional[pathlib.Path] = None,
122
+ stdout: Optional[pathlib.Path] = None,
123
+ ):
124
+ """Set the standard input/output files.
125
+
126
+ stdin (Path): standard input file.
127
+ stdout (Path): standard output file.
128
+
129
+ """
130
+ self.stdin_file = stdin
131
+ self.stdout_file = stdout
132
+
133
+ def set_stdall(
134
+ self,
135
+ stdin: Optional[pathlib.Path] = None,
136
+ stdout: Optional[pathlib.Path] = None,
137
+ stderr: Optional[pathlib.Path] = None,
138
+ ):
139
+ """Set the standard input/output/error files.
140
+
141
+ stdin (Path): standard input file.
142
+ stdout (Path): standard output file.
143
+ stderr (Path): standard error file.
144
+
145
+ """
146
+ self.stdin_file = stdin
147
+ self.stdout_file = stdout
148
+ self.stderr_file = stderr
149
+
150
+ def add_mapped_directory(
151
+ self,
152
+ src: pathlib.Path,
153
+ dest: Optional[pathlib.Path] = None,
154
+ options: Optional[str] = None,
155
+ ignore_if_not_existing: bool = False,
156
+ ):
157
+ """Add src to the directory to be mapped inside the sandbox.
158
+
159
+ src (Path): directory to make visible.
160
+ dest (Path|None): if not None, the path where to bind src.
161
+ options (str|None): if not None, isolate's directory rule options.
162
+ ignore_if_not_existing (bool): if True, ignore the mapping when src
163
+ does not exist (instead of having isolate terminate with an
164
+ error).
165
+
166
+ """
167
+ if dest is None:
168
+ dest = src
169
+ if ignore_if_not_existing and not src.exists():
170
+ return
171
+ self.dirs.append(DirectoryMount(src, dest, options))
172
+
173
+
174
+ class SandboxBase(abc.ABC):
175
+ """A base class for all sandboxes, meant to contain common
176
+ resources.
177
+
178
+ """
179
+
180
+ EXIT_SANDBOX_ERROR = 'sandbox error'
181
+ EXIT_OK = 'ok'
182
+ EXIT_SIGNAL = 'signal'
183
+ EXIT_TIMEOUT = 'timeout'
184
+ EXIT_TIMEOUT_WALL = 'wall timeout'
185
+ EXIT_NONZERO_RETURN = 'nonzero return'
186
+ EXIT_MEMORY_LIMIT_EXCEEDED = 'memory limit exceeded'
187
+ EXIT_OUTPUT_LIMIT_EXCEEDED = 'output limit exceeded'
188
+
189
+ file_cacher: cacher.FileCacher
190
+ name: str
191
+ temp_dir: Optional[pathlib.Path]
192
+ cmd_file: pathlib.Path
193
+
194
+ params: SandboxParams
195
+
196
+ def __init__(
197
+ self,
198
+ file_cacher: Optional[cacher.FileCacher] = None,
199
+ name: Optional[str] = None,
200
+ temp_dir: Optional[pathlib.Path] = None,
201
+ params: Optional[SandboxParams] = None,
202
+ ):
203
+ """Initialization.
204
+
205
+ file_cacher (FileCacher): an instance of the FileCacher class
206
+ (to interact with FS), if the sandbox needs it.
207
+ name (string|None): name of the sandbox, which might appear in the
208
+ path and in system logs.
209
+ temp_dir (Path|None): temporary directory to use; if None, use the
210
+ default temporary directory.
211
+
212
+ """
213
+ self.file_cacher = file_cacher or cacher.FileCacher(storage.NullStorage())
214
+ self.name = name if name is not None else 'unnamed'
215
+ self.temp_dir = temp_dir
216
+
217
+ self.cmd_file = pathlib.PosixPath('commands.log')
218
+
219
+ self.params = params or SandboxParams()
220
+
221
+ # Set common environment variables.
222
+ # Specifically needed by Python, that searches the home for
223
+ # packages.
224
+ self.params.set_env['HOME'] = './'
225
+
226
+ def set_params(self, params: SandboxParams):
227
+ """Set the parameters of the sandbox.
228
+
229
+ params (SandboxParams): the parameters to set.
230
+
231
+ """
232
+ self.params = params
233
+
234
+ def set_multiprocess(self, multiprocess: bool):
235
+ """Set the sandbox to (dis-)allow multiple threads and processes.
236
+
237
+ multiprocess (bool): whether to allow multiple thread/processes or not.
238
+
239
+ """
240
+ if multiprocess:
241
+ # Max processes is set to 1000 to limit the effect of fork bombs.
242
+ self.params.max_processes = 1000
243
+ else:
244
+ self.params.max_processes = 1
245
+
246
+ def get_stats(self) -> str:
247
+ """Return a human-readable string representing execution time
248
+ and memory usage.
249
+
250
+ return (string): human-readable stats.
251
+
252
+ """
253
+ execution_time = self.get_execution_time()
254
+ if execution_time is not None:
255
+ time_str = f'{execution_time:.3f} sec'
256
+ else:
257
+ time_str = '(time unknown)'
258
+ memory_used = self.get_memory_used()
259
+ if memory_used is not None:
260
+ mem_str = f'{memory_used / (1024 * 1024):.2f} MB'
261
+ else:
262
+ mem_str = '(memory usage unknown)'
263
+ return f'[{time_str} - {mem_str}]'
264
+
265
+ @abc.abstractmethod
266
+ def get_root_path(self) -> pathlib.Path:
267
+ """Return the toplevel path of the sandbox.
268
+
269
+ return (Path): the root path.
270
+
271
+ """
272
+ pass
273
+
274
+ @abc.abstractmethod
275
+ def get_execution_time(self) -> Optional[float]:
276
+ """Return the time spent in the sandbox.
277
+
278
+ return (float): time spent in the sandbox.
279
+
280
+ """
281
+ pass
282
+
283
+ @abc.abstractmethod
284
+ def get_memory_used(self) -> Optional[int]:
285
+ """Return the memory used by the sandbox.
286
+
287
+ return (int): memory used by the sandbox (in bytes).
288
+
289
+ """
290
+ pass
291
+
292
+ @abc.abstractmethod
293
+ def get_killing_signal(self) -> int:
294
+ """Return the signal that killed the sandboxed process.
295
+
296
+ return (int): offending signal, or 0.
297
+
298
+ """
299
+ pass
300
+
301
+ @abc.abstractmethod
302
+ def get_exit_status(self) -> str:
303
+ """Get information about how the sandbox terminated.
304
+
305
+ return (string): the main reason why the sandbox terminated.
306
+
307
+ """
308
+ pass
309
+
310
+ @abc.abstractmethod
311
+ def get_exit_code(self) -> int:
312
+ """Return the exit code of the sandboxed process.
313
+
314
+ return (int): exitcode, or 0.
315
+
316
+ """
317
+ pass
318
+
319
+ @abc.abstractmethod
320
+ def get_human_exit_description(self) -> str:
321
+ """Get the status of the sandbox and return a human-readable
322
+ string describing it.
323
+
324
+ return (string): human-readable explaination of why the
325
+ sandbox terminated.
326
+
327
+ """
328
+ pass
329
+
330
+ def use_soft_timeout(self) -> bool:
331
+ return False
332
+
333
+ def relative_path(self, path: pathlib.Path) -> pathlib.Path:
334
+ """Translate from a relative path inside the sandbox to a
335
+ system path.
336
+
337
+ path (Path): relative path of the file inside the sandbox.
338
+
339
+ return (string): the absolute path.
340
+
341
+ """
342
+ return self.get_root_path() / path
343
+
344
+ def create_file(
345
+ self, path: pathlib.Path, executable: bool = False, override: bool = False
346
+ ) -> IO[bytes]:
347
+ """Create an empty file in the sandbox and open it in write
348
+ binary mode.
349
+
350
+ path (Path): relative path of the file inside the sandbox.
351
+ executable (bool): to set permissions.
352
+
353
+ return (file): the file opened in write binary mode.
354
+
355
+ """
356
+ if executable:
357
+ logger.debug('Creating executable file %s in sandbox.', path)
358
+ else:
359
+ logger.debug('Creating plain file %s in sandbox.', path)
360
+ real_path = self.relative_path(path)
361
+ if override:
362
+ real_path.unlink(missing_ok=True)
363
+ # Ensure directory exists.
364
+ real_path.parent.mkdir(parents=True, exist_ok=True)
365
+ try:
366
+ file_fd = os.open(str(real_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
367
+ file_ = open(file_fd, 'wb')
368
+ except OSError as e:
369
+ logger.error(
370
+ 'Failed create file %s in sandbox. Unable to '
371
+ 'evalulate this submission. This may be due to '
372
+ 'cheating. %s',
373
+ real_path,
374
+ e,
375
+ exc_info=True,
376
+ )
377
+ raise
378
+ mod = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IWUSR
379
+ if executable:
380
+ mod |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
381
+ os.chmod(str(real_path), mod)
382
+ return file_
383
+
384
+ def create_symlink(
385
+ self, path: pathlib.Path, from_path: pathlib.Path, override: bool = False
386
+ ) -> Optional[pathlib.Path]:
387
+ real_path = self.relative_path(path)
388
+ if override:
389
+ real_path.unlink(missing_ok=True)
390
+ try:
391
+ real_path.symlink_to(from_path.resolve())
392
+ except NotImplementedError:
393
+ return None
394
+ return real_path
395
+
396
+ def create_file_from_storage(
397
+ self,
398
+ path: pathlib.Path,
399
+ digest: str,
400
+ executable: bool = False,
401
+ override: bool = False,
402
+ try_symlink: bool = False,
403
+ ):
404
+ """Write a file taken from FS in the sandbox.
405
+
406
+ path (Path): relative path of the file inside the sandbox.
407
+ digest (string): digest of the file in FS.
408
+ executable (bool): to set permissions.
409
+
410
+ """
411
+ if try_symlink and executable:
412
+ symlink_path = self.file_cacher.path_for_symlink(digest)
413
+ if symlink_path is not None:
414
+ created = self.create_symlink(
415
+ path,
416
+ from_path=symlink_path,
417
+ override=override,
418
+ )
419
+ if created is not None:
420
+ created.chmod(0o755)
421
+ return
422
+ with self.create_file(path, executable, override=override) as dest_fobj:
423
+ self.file_cacher.get_file_to_fobj(digest, dest_fobj)
424
+
425
+ def create_file_from_bytes(
426
+ self,
427
+ path: pathlib.Path,
428
+ content: bytes,
429
+ executable: bool = False,
430
+ override: bool = False,
431
+ ):
432
+ """Write some data to a file in the sandbox.
433
+
434
+ path (Path): relative path of the file inside the sandbox.
435
+ content (bytes): what to write in the file.
436
+ executable (bool): to set permissions.
437
+
438
+ """
439
+ with self.create_file(path, executable, override=override) as dest_fobj:
440
+ dest_fobj.write(content)
441
+
442
+ def create_file_from_other_file(
443
+ self,
444
+ path: pathlib.Path,
445
+ from_path: pathlib.Path,
446
+ executable: bool = False,
447
+ override: bool = False,
448
+ try_symlink: bool = False,
449
+ ):
450
+ """Write a file taken from FS in the sandbox.
451
+
452
+ path (Path): relative path of the file inside the sandbox.
453
+ digest (string): digest of the file in FS.
454
+ executable (bool): to set permissions.
455
+
456
+ """
457
+ if try_symlink and executable:
458
+ created = self.create_symlink(
459
+ path,
460
+ from_path,
461
+ override=override,
462
+ )
463
+ if created is not None:
464
+ created.chmod(0o755)
465
+ return
466
+ with self.create_file(path, executable, override=override) as dest_fobj:
467
+ with from_path.open('rb') as src_fobj:
468
+ storage.copyfileobj(src_fobj, dest_fobj)
469
+
470
+ def create_file_from_string(
471
+ self,
472
+ path: pathlib.Path,
473
+ content: str,
474
+ executable: bool = False,
475
+ override: bool = False,
476
+ ):
477
+ """Write some data to a file in the sandbox.
478
+
479
+ path (Path): relative path of the file inside the sandbox.
480
+ content (str): what to write in the file.
481
+ executable (bool): to set permissions.
482
+
483
+ """
484
+ return self.create_file_from_bytes(
485
+ path, content.encode('utf-8'), executable, override=override
486
+ )
487
+
488
+ def get_file(
489
+ self, path: pathlib.Path, trunc_len: Optional[int] = None
490
+ ) -> IO[bytes]:
491
+ """Open a file in the sandbox given its relative path.
492
+
493
+ path (Path): relative path of the file inside the sandbox.
494
+ trunc_len (int|None): if None, does nothing; otherwise, before
495
+ returning truncate it at the specified length.
496
+
497
+ return (file): the file opened in read binary mode.
498
+
499
+ """
500
+ logger.debug(f'Retrieving file {path} from sandbox.')
501
+ real_path = self.relative_path(path)
502
+ file_ = real_path.open('rb')
503
+ if trunc_len is not None:
504
+ file_ = Truncator(file_, trunc_len)
505
+ return typing.cast(IO[bytes], file_)
506
+
507
+ def get_file_text(
508
+ self, path: pathlib.Path, trunc_len: Optional[int] = None
509
+ ) -> IO[str]:
510
+ """Open a file in the sandbox given its relative path, in text mode.
511
+
512
+ Assumes encoding is UTF-8. The caller must handle decoding errors.
513
+
514
+ path (Path): relative path of the file inside the sandbox.
515
+ trunc_len (int|None): if None, does nothing; otherwise, before
516
+ returning truncate it at the specified length.
517
+
518
+ return (file): the file opened in read binary mode.
519
+
520
+ """
521
+ logger.debug('Retrieving text file %s from sandbox.', path)
522
+ real_path = self.relative_path(path)
523
+ file_ = real_path.open('rt', encoding='utf-8')
524
+ if trunc_len is not None:
525
+ file_ = Truncator(file_, trunc_len)
526
+ return typing.cast(IO[str], file_)
527
+
528
+ def get_file_to_bytes(
529
+ self, path: pathlib.Path, maxlen: Optional[int] = 1024
530
+ ) -> bytes:
531
+ """Return the content of a file in the sandbox given its
532
+ relative path.
533
+
534
+ path (Path): relative path of the file inside the sandbox.
535
+ maxlen (int): maximum number of bytes to read, or None if no
536
+ limit.
537
+
538
+ return (bytes): the content of the file up to maxlen bytes.
539
+
540
+ """
541
+ with self.get_file(path) as file_:
542
+ if maxlen is None:
543
+ return file_.read()
544
+ else:
545
+ return file_.read(maxlen)
546
+
547
+ def get_file_to_string(
548
+ self, path: pathlib.Path, maxlen: Optional[int] = 1024
549
+ ) -> str:
550
+ """Return the content of a file in the sandbox given its
551
+ relative path.
552
+
553
+ path (Path): relative path of the file inside the sandbox.
554
+ maxlen (int): maximum number of bytes to read, or None if no
555
+ limit.
556
+
557
+ return (string): the content of the file up to maxlen bytes.
558
+
559
+ """
560
+ return self.get_file_to_bytes(path, maxlen).decode('utf-8')
561
+
562
+ def get_file_to_storage(
563
+ self, path: pathlib.Path, description: str = '', trunc_len: Optional[int] = None
564
+ ) -> str:
565
+ """Put a sandbox file in FS and return its digest.
566
+
567
+ path (Path): relative path of the file inside the sandbox.
568
+ description (str): the description for FS.
569
+ trunc_len (int|None): if None, does nothing; otherwise, before
570
+ returning truncate it at the specified length.
571
+
572
+ return (str): the digest of the file.
573
+
574
+ """
575
+ with self.get_file(path, trunc_len=trunc_len) as file_:
576
+ return self.file_cacher.put_file_from_fobj(file_, description)
577
+
578
+ def stat_file(self, path: pathlib.Path) -> os.stat_result:
579
+ """Return the stats of a file in the sandbox.
580
+
581
+ path (Path): relative path of the file inside the sandbox.
582
+
583
+ return (stat_result): the stat results.
584
+
585
+ """
586
+ return self.relative_path(path).stat()
587
+
588
+ def file_exists(self, path: pathlib.Path) -> bool:
589
+ """Return if a file exists in the sandbox.
590
+
591
+ path (Path): relative path of the file inside the sandbox.
592
+
593
+ return (bool): if the file exists.
594
+
595
+ """
596
+ return self.relative_path(path).exists()
597
+
598
+ def remove_file(self, path: pathlib.Path):
599
+ """Delete a file in the sandbox.
600
+
601
+ path (Path): relative path of the file inside the sandbox.
602
+
603
+ """
604
+ self.relative_path(path).unlink(missing_ok=True)
605
+
606
+ def glob(self, glob_expr: str) -> List[pathlib.Path]:
607
+ return [
608
+ path.relative_to(self.get_root_path())
609
+ for path in self.get_root_path().glob(glob_expr)
610
+ ]
611
+
612
+ @abc.abstractmethod
613
+ def execute_without_std(
614
+ self,
615
+ command: List[str],
616
+ ) -> bool:
617
+ """Execute the given command in the sandbox using
618
+ subprocess.Popen and discarding standard input, output and
619
+ error. More specifically, the standard input gets closed just
620
+ after the execution has started; standard output and error are
621
+ read until the end, in a way that prevents the execution from
622
+ being blocked because of insufficient buffering.
623
+
624
+ command ([string]): executable filename and arguments of the
625
+ command.
626
+ wait (bool): True if this call is blocking, False otherwise
627
+
628
+ return (bool|Popen): if the call is blocking, then return True
629
+ if the sandbox didn't report errors (caused by the sandbox
630
+ itself), False otherwise; if the call is not blocking,
631
+ return the Popen object from subprocess.
632
+
633
+ """
634
+ pass
635
+
636
+ @abc.abstractmethod
637
+ def hydrate_logs(self):
638
+ """Fetch the results of the execution and hydrate logs.
639
+
640
+ This method should be called after the execution has
641
+ terminated, to hydrate logs and stuff.
642
+ """
643
+ pass
644
+
645
+ def translate_box_exitcode(self, exitcode: int) -> bool:
646
+ """Translate the sandbox exit code to a boolean sandbox success.
647
+
648
+ _ (int): the exit code of the sandbox.
649
+
650
+ return (bool): False if the sandbox had an error, True if it
651
+ terminated correctly (regardless of what the internal process
652
+ did).
653
+
654
+ """
655
+ return exitcode == 0
656
+
657
+ @abc.abstractmethod
658
+ def initialize(self):
659
+ """Initialize the sandbox.
660
+
661
+ To be called at the beginning of the execution.
662
+
663
+ """
664
+ pass
665
+
666
+ @abc.abstractmethod
667
+ def cleanup(self, delete: bool = False):
668
+ """Cleanup the sandbox.
669
+
670
+ To be called at the end of the execution, regardless of
671
+ whether the sandbox should be deleted or not.
672
+
673
+ delete (bool): if True, also delete get_root_path() and everything it
674
+ contains.
675
+
676
+ """
677
+ pass
678
+
679
+ def debug_message(self) -> Any:
680
+ return 'N/A'
681
+
682
+
683
+ class Truncator(io.RawIOBase):
684
+ """Wrap a file-like object to simulate truncation.
685
+
686
+ This file-like object provides read-only access to a limited prefix
687
+ of a wrapped file-like object. It provides a truncated version of
688
+ the file without ever touching the object on the filesystem.
689
+
690
+ This class is only able to wrap binary streams as it relies on the
691
+ readinto method which isn't provided by text (unicode) streams.
692
+
693
+ """
694
+
695
+ def __init__(self, fobj, size):
696
+ """Wrap fobj and give access to its first size bytes.
697
+
698
+ fobj (fileobj): a file-like object to wrap.
699
+ size (int): the number of bytes that will be accessible.
700
+
701
+ """
702
+ self.fobj = fobj
703
+ self.size = size
704
+
705
+ def close(self):
706
+ """See io.IOBase.close."""
707
+ self.fobj.close()
708
+
709
+ @property
710
+ def closed(self):
711
+ """See io.IOBase.closed."""
712
+ return self.fobj.closed
713
+
714
+ def readable(self):
715
+ """See io.IOBase.readable."""
716
+ return True
717
+
718
+ def seekable(self):
719
+ """See io.IOBase.seekable."""
720
+ return True
721
+
722
+ def readinto(self, b):
723
+ """See io.RawIOBase.readinto."""
724
+ # This is the main "trick": we clip (i.e. mask, reduce, slice)
725
+ # the given buffer so that it doesn't overflow into the area we
726
+ # want to hide (that is, out of the prefix) and then we forward
727
+ # it to the wrapped file-like object.
728
+ b = memoryview(b)[: max(0, self.size - self.fobj.tell())]
729
+ return self.fobj.readinto(b)
730
+
731
+ def seek(self, offset, whence=io.SEEK_SET):
732
+ """See io.IOBase.seek."""
733
+ # We have to catch seeks relative to the end of the file and
734
+ # adjust them to the new "imposed" size.
735
+ if whence == io.SEEK_END:
736
+ if self.fobj.seek(0, io.SEEK_END) > self.size:
737
+ self.fobj.seek(self.size, io.SEEK_SET)
738
+ return self.fobj.seek(offset, io.SEEK_CUR)
739
+ else:
740
+ return self.fobj.seek(offset, whence)
741
+
742
+ def tell(self):
743
+ """See io.IOBase.tell."""
744
+ return self.fobj.tell()
745
+
746
+ def write(self, _):
747
+ """See io.RawIOBase.write."""
748
+ raise io.UnsupportedOperation('write')