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,429 @@
1
+ import os
2
+ import pathlib
3
+ import sys
4
+
5
+ from rbx.grading import steps_with_caching
6
+ from rbx.grading.caching import DependencyCache
7
+ from rbx.grading.judge.cacher import FileCacher
8
+ from rbx.grading.judge.sandbox import SandboxBase, SandboxParams
9
+ from rbx.grading.steps import (
10
+ DigestOrSource,
11
+ GradingArtifacts,
12
+ GradingFileInput,
13
+ GradingFileOutput,
14
+ RunLogMetadata,
15
+ )
16
+
17
+
18
+ def test_run_from_digest(
19
+ cleandir: pathlib.Path,
20
+ dependency_cache: DependencyCache,
21
+ sandbox: SandboxBase,
22
+ file_cacher: FileCacher,
23
+ ):
24
+ executable = DigestOrSource.create(file_cacher.put_file_text('print(5)'))
25
+ artifacts = GradingArtifacts()
26
+ artifacts.inputs.append(
27
+ GradingFileInput(**executable.expand(), dest=pathlib.Path('executable.py'))
28
+ )
29
+ artifacts.outputs.append(
30
+ GradingFileOutput(src=pathlib.Path('box-out.txt'), dest=pathlib.Path('out.txt'))
31
+ )
32
+ steps_with_caching.run(
33
+ f'{sys.executable} executable.py',
34
+ params=SandboxParams(stdout_file=pathlib.Path('box-out.txt')),
35
+ sandbox=sandbox,
36
+ artifacts=artifacts,
37
+ dependency_cache=dependency_cache,
38
+ metadata=RunLogMetadata(),
39
+ )
40
+
41
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
42
+ assert artifacts.logs is not None
43
+ assert artifacts.logs.run is not None
44
+ assert artifacts.logs.run.metadata is not None
45
+ assert not artifacts.logs.cached
46
+
47
+
48
+ def test_run_from_disk(
49
+ cleandir: pathlib.Path,
50
+ dependency_cache: DependencyCache,
51
+ sandbox: SandboxBase,
52
+ ):
53
+ pathlib.Path('executable.py').write_text('print(42)')
54
+
55
+ executable = DigestOrSource.create(pathlib.Path('executable.py'))
56
+ artifacts = GradingArtifacts()
57
+ artifacts.inputs.append(
58
+ GradingFileInput(**executable.expand(), dest=pathlib.Path('executable.py'))
59
+ )
60
+ artifacts.outputs.append(
61
+ GradingFileOutput(src=pathlib.Path('box-out.txt'), dest=pathlib.Path('out.txt'))
62
+ )
63
+ steps_with_caching.run(
64
+ f'{sys.executable} executable.py',
65
+ params=SandboxParams(stdout_file=pathlib.Path('box-out.txt')),
66
+ sandbox=sandbox,
67
+ artifacts=artifacts,
68
+ dependency_cache=dependency_cache,
69
+ )
70
+
71
+ assert (cleandir / 'out.txt').read_text().strip() == '42'
72
+ assert artifacts.logs is not None
73
+ assert artifacts.logs.run is not None
74
+ assert not artifacts.logs.cached
75
+
76
+
77
+ def test_run_caches_intermediate_digest_if_dest_changes(
78
+ cleandir: pathlib.Path,
79
+ dependency_cache: DependencyCache,
80
+ sandbox: SandboxBase,
81
+ file_cacher: FileCacher,
82
+ ):
83
+ def configure_and_run_with_dest(dest: pathlib.Path) -> GradingArtifacts:
84
+ executable = DigestOrSource.create(file_cacher.put_file_text('print(5)'))
85
+ artifacts = GradingArtifacts()
86
+ artifacts.inputs.append(
87
+ GradingFileInput(**executable.expand(), dest=pathlib.Path('executable.py'))
88
+ )
89
+ artifacts.outputs.append(
90
+ GradingFileOutput(src=pathlib.Path('box-out.txt'), dest=dest)
91
+ )
92
+ steps_with_caching.run(
93
+ f'{sys.executable} executable.py',
94
+ params=SandboxParams(stdout_file=pathlib.Path('box-out.txt')),
95
+ sandbox=sandbox,
96
+ artifacts=artifacts,
97
+ dependency_cache=dependency_cache,
98
+ )
99
+ return artifacts
100
+
101
+ artifacts = configure_and_run_with_dest(pathlib.Path('out.txt'))
102
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
103
+ assert artifacts.logs is not None
104
+ assert not artifacts.logs.cached
105
+
106
+ another_artifacts = configure_and_run_with_dest(pathlib.Path('another-out.txt'))
107
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
108
+ assert another_artifacts.logs is not None
109
+ assert another_artifacts.logs.cached
110
+
111
+
112
+ def test_run_overwrite_changed_file_with_storage_value(
113
+ cleandir: pathlib.Path,
114
+ dependency_cache: DependencyCache,
115
+ sandbox: SandboxBase,
116
+ file_cacher: FileCacher,
117
+ ):
118
+ def configure_and_run_with_dest(dest: pathlib.Path) -> GradingArtifacts:
119
+ executable = DigestOrSource.create(file_cacher.put_file_text('print(5)'))
120
+ artifacts = GradingArtifacts()
121
+ artifacts.inputs.append(
122
+ GradingFileInput(**executable.expand(), dest=pathlib.Path('executable.py'))
123
+ )
124
+ artifacts.outputs.append(
125
+ GradingFileOutput(src=pathlib.Path('box-out.txt'), dest=dest)
126
+ )
127
+ steps_with_caching.run(
128
+ f'{sys.executable} executable.py',
129
+ params=SandboxParams(stdout_file=pathlib.Path('box-out.txt')),
130
+ sandbox=sandbox,
131
+ artifacts=artifacts,
132
+ dependency_cache=dependency_cache,
133
+ )
134
+ return artifacts
135
+
136
+ artifacts = configure_and_run_with_dest(pathlib.Path('out.txt'))
137
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
138
+ assert artifacts.logs is not None
139
+ assert not artifacts.logs.cached
140
+
141
+ pathlib.Path('out.txt').write_text('42')
142
+
143
+ another_artifacts = configure_and_run_with_dest(pathlib.Path('out.txt'))
144
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
145
+ assert another_artifacts.logs is not None
146
+ assert another_artifacts.logs.cached
147
+
148
+
149
+ def test_run_recreates_deleted_file_with_storage_value(
150
+ cleandir: pathlib.Path,
151
+ dependency_cache: DependencyCache,
152
+ sandbox: SandboxBase,
153
+ file_cacher: FileCacher,
154
+ ):
155
+ def configure_and_run_with_dest(dest: pathlib.Path) -> GradingArtifacts:
156
+ executable = DigestOrSource.create(file_cacher.put_file_text('print(5)'))
157
+ artifacts = GradingArtifacts()
158
+ artifacts.inputs.append(
159
+ GradingFileInput(**executable.expand(), dest=pathlib.Path('executable.py'))
160
+ )
161
+ artifacts.outputs.append(
162
+ GradingFileOutput(src=pathlib.Path('box-out.txt'), dest=dest)
163
+ )
164
+ steps_with_caching.run(
165
+ f'{sys.executable} executable.py',
166
+ params=SandboxParams(stdout_file=pathlib.Path('box-out.txt')),
167
+ sandbox=sandbox,
168
+ artifacts=artifacts,
169
+ dependency_cache=dependency_cache,
170
+ )
171
+ return artifacts
172
+
173
+ artifacts = configure_and_run_with_dest(pathlib.Path('out.txt'))
174
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
175
+ assert artifacts.logs is not None
176
+ assert not artifacts.logs.cached
177
+
178
+ pathlib.Path('out.txt').unlink()
179
+
180
+ another_artifacts = configure_and_run_with_dest(pathlib.Path('out.txt'))
181
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
182
+ assert another_artifacts.logs is not None
183
+ assert another_artifacts.logs.cached
184
+
185
+
186
+ def test_run_overwrite_exec_bit_when_changed(
187
+ cleandir: pathlib.Path,
188
+ dependency_cache: DependencyCache,
189
+ sandbox: SandboxBase,
190
+ file_cacher: FileCacher,
191
+ ):
192
+ def configure_and_run_with_dest(dest: pathlib.Path) -> GradingArtifacts:
193
+ executable = DigestOrSource.create(file_cacher.put_file_text('print(5)'))
194
+ artifacts = GradingArtifacts()
195
+ artifacts.inputs.append(
196
+ GradingFileInput(
197
+ **executable.expand(),
198
+ dest=pathlib.Path('executable.py'),
199
+ )
200
+ )
201
+ artifacts.outputs.append(
202
+ GradingFileOutput(
203
+ src=pathlib.Path('box-out.txt'), dest=dest, executable=True
204
+ )
205
+ )
206
+ steps_with_caching.run(
207
+ f'{sys.executable} executable.py',
208
+ params=SandboxParams(stdout_file=pathlib.Path('box-out.txt')),
209
+ sandbox=sandbox,
210
+ artifacts=artifacts,
211
+ dependency_cache=dependency_cache,
212
+ )
213
+ return artifacts
214
+
215
+ artifacts = configure_and_run_with_dest(pathlib.Path('out.txt'))
216
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
217
+ assert artifacts.logs is not None
218
+ assert not artifacts.logs.cached
219
+ assert os.access('out.txt', os.X_OK)
220
+
221
+ pathlib.Path('out.txt').chmod(0o644)
222
+
223
+ another_artifacts = configure_and_run_with_dest(pathlib.Path('out.txt'))
224
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
225
+ assert another_artifacts.logs is not None
226
+ assert another_artifacts.logs.cached
227
+ assert os.access('out.txt', os.X_OK)
228
+
229
+
230
+ def test_run_evicts_when_changed_file_and_no_hash(
231
+ cleandir: pathlib.Path,
232
+ dependency_cache: DependencyCache,
233
+ sandbox: SandboxBase,
234
+ file_cacher: FileCacher,
235
+ ):
236
+ def configure_and_run_with_dest(dest: pathlib.Path) -> GradingArtifacts:
237
+ executable = DigestOrSource.create(file_cacher.put_file_text('print(5)'))
238
+ artifacts = GradingArtifacts()
239
+ artifacts.inputs.append(
240
+ GradingFileInput(**executable.expand(), dest=pathlib.Path('executable.py'))
241
+ )
242
+ artifacts.outputs.append(
243
+ GradingFileOutput(src=pathlib.Path('box-out.txt'), dest=dest, hash=False)
244
+ )
245
+ steps_with_caching.run(
246
+ f'{sys.executable} executable.py',
247
+ params=SandboxParams(stdout_file=pathlib.Path('box-out.txt')),
248
+ sandbox=sandbox,
249
+ artifacts=artifacts,
250
+ dependency_cache=dependency_cache,
251
+ )
252
+ return artifacts
253
+
254
+ artifacts = configure_and_run_with_dest(pathlib.Path('out.txt'))
255
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
256
+ assert artifacts.logs is not None
257
+ assert not artifacts.logs.cached
258
+
259
+ pathlib.Path('out.txt').write_text('42')
260
+
261
+ another_artifacts = configure_and_run_with_dest(pathlib.Path('out.txt'))
262
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
263
+ assert another_artifacts.logs is not None
264
+ assert not another_artifacts.logs.cached
265
+
266
+
267
+ def test_run_evicts_when_exec_bit_different_and_no_hash(
268
+ cleandir: pathlib.Path,
269
+ dependency_cache: DependencyCache,
270
+ sandbox: SandboxBase,
271
+ file_cacher: FileCacher,
272
+ ):
273
+ def configure_and_run_with_dest(dest: pathlib.Path) -> GradingArtifacts:
274
+ executable = DigestOrSource.create(file_cacher.put_file_text('print(5)'))
275
+ artifacts = GradingArtifacts()
276
+ artifacts.inputs.append(
277
+ GradingFileInput(**executable.expand(), dest=pathlib.Path('executable.py'))
278
+ )
279
+ artifacts.outputs.append(
280
+ GradingFileOutput(
281
+ src=pathlib.Path('box-out.txt'), dest=dest, hash=False, executable=True
282
+ )
283
+ )
284
+ steps_with_caching.run(
285
+ f'{sys.executable} executable.py',
286
+ params=SandboxParams(stdout_file=pathlib.Path('box-out.txt')),
287
+ sandbox=sandbox,
288
+ artifacts=artifacts,
289
+ dependency_cache=dependency_cache,
290
+ )
291
+ return artifacts
292
+
293
+ artifacts = configure_and_run_with_dest(pathlib.Path('out.txt'))
294
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
295
+ assert artifacts.logs is not None
296
+ assert not artifacts.logs.cached
297
+
298
+ pathlib.Path('out.txt').chmod(0o0644)
299
+
300
+ another_artifacts = configure_and_run_with_dest(pathlib.Path('out.txt'))
301
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
302
+ assert another_artifacts.logs is not None
303
+ assert not another_artifacts.logs.cached
304
+
305
+
306
+ def test_run_evicts_when_input_fingerprint_changes(
307
+ cleandir: pathlib.Path,
308
+ dependency_cache: DependencyCache,
309
+ sandbox: SandboxBase,
310
+ ):
311
+ def configure_and_run() -> GradingArtifacts:
312
+ executable = DigestOrSource.create(pathlib.Path('executable.py'))
313
+ artifacts = GradingArtifacts()
314
+ artifacts.inputs.append(
315
+ GradingFileInput(**executable.expand(), dest=pathlib.Path('executable.py'))
316
+ )
317
+ artifacts.outputs.append(
318
+ GradingFileOutput(
319
+ src=pathlib.Path('box-out.txt'),
320
+ dest=pathlib.Path('out.txt'),
321
+ )
322
+ )
323
+ steps_with_caching.run(
324
+ f'{sys.executable} executable.py',
325
+ params=SandboxParams(stdout_file=pathlib.Path('box-out.txt')),
326
+ sandbox=sandbox,
327
+ artifacts=artifacts,
328
+ dependency_cache=dependency_cache,
329
+ )
330
+ return artifacts
331
+
332
+ pathlib.Path('executable.py').write_text('print(5)')
333
+
334
+ artifacts = configure_and_run()
335
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
336
+ assert artifacts.logs is not None
337
+ assert not artifacts.logs.cached
338
+
339
+ pathlib.Path('executable.py').write_text('print(42)')
340
+
341
+ another_artifacts = configure_and_run()
342
+ assert (cleandir / 'out.txt').read_text().strip() == '42'
343
+ assert another_artifacts.logs is not None
344
+ assert not another_artifacts.logs.cached
345
+
346
+
347
+ def test_run_evicts_when_output_is_deleted_and_no_hash(
348
+ cleandir: pathlib.Path,
349
+ dependency_cache: DependencyCache,
350
+ sandbox: SandboxBase,
351
+ ):
352
+ def configure_and_run() -> GradingArtifacts:
353
+ executable = DigestOrSource.create(pathlib.Path('executable.py'))
354
+ artifacts = GradingArtifacts()
355
+ artifacts.inputs.append(
356
+ GradingFileInput(**executable.expand(), dest=pathlib.Path('executable.py'))
357
+ )
358
+ artifacts.outputs.append(
359
+ GradingFileOutput(
360
+ src=pathlib.Path('box-out.txt'),
361
+ dest=pathlib.Path('out.txt'),
362
+ hash=False,
363
+ )
364
+ )
365
+ steps_with_caching.run(
366
+ f'{sys.executable} executable.py',
367
+ params=SandboxParams(stdout_file=pathlib.Path('box-out.txt')),
368
+ sandbox=sandbox,
369
+ artifacts=artifacts,
370
+ dependency_cache=dependency_cache,
371
+ )
372
+ return artifacts
373
+
374
+ pathlib.Path('executable.py').write_text('print(5)')
375
+
376
+ artifacts = configure_and_run()
377
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
378
+ assert artifacts.logs is not None
379
+ assert not artifacts.logs.cached
380
+
381
+ pathlib.Path('out.txt').unlink()
382
+
383
+ another_artifacts = configure_and_run()
384
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
385
+ assert another_artifacts.logs is not None
386
+ assert not another_artifacts.logs.cached
387
+
388
+
389
+ def test_run_misses_when_input_file_changes(
390
+ cleandir: pathlib.Path,
391
+ dependency_cache: DependencyCache,
392
+ sandbox: SandboxBase,
393
+ file_cacher: FileCacher,
394
+ ):
395
+ def configure_and_run(number: int) -> GradingArtifacts:
396
+ executable = DigestOrSource.create(
397
+ file_cacher.put_file_text(f'print({number})')
398
+ )
399
+ artifacts = GradingArtifacts()
400
+ artifacts.inputs.append(
401
+ GradingFileInput(**executable.expand(), dest=pathlib.Path('executable.py'))
402
+ )
403
+ artifacts.outputs.append(
404
+ GradingFileOutput(
405
+ src=pathlib.Path('box-out.txt'),
406
+ dest=pathlib.Path('out.txt'),
407
+ hash=False,
408
+ )
409
+ )
410
+ steps_with_caching.run(
411
+ f'{sys.executable} executable.py',
412
+ params=SandboxParams(stdout_file=pathlib.Path('box-out.txt')),
413
+ sandbox=sandbox,
414
+ artifacts=artifacts,
415
+ dependency_cache=dependency_cache,
416
+ )
417
+ return artifacts
418
+
419
+ artifacts = configure_and_run(5)
420
+ assert (cleandir / 'out.txt').read_text().strip() == '5'
421
+ assert artifacts.logs is not None
422
+ assert not artifacts.logs.cached
423
+
424
+ pathlib.Path('out.txt').write_text('42')
425
+
426
+ another_artifacts = configure_and_run(42)
427
+ assert (cleandir / 'out.txt').read_text().strip() == '42'
428
+ assert another_artifacts.logs is not None
429
+ assert not another_artifacts.logs.cached
rbx/grading_utils.py ADDED
@@ -0,0 +1,148 @@
1
+ import pathlib
2
+ from pathlib import PosixPath
3
+ from typing import List
4
+
5
+ from rbx import config
6
+ from rbx.config import Artifact, Language, format_vars
7
+ from rbx.grading import steps
8
+ from rbx.grading.judge.sandbox import MERGE_STDERR, SandboxParams
9
+ from rbx.grading.steps import (
10
+ GradingArtifacts,
11
+ GradingFileInput,
12
+ GradingFileOutput,
13
+ TestcaseIO,
14
+ )
15
+ from rbx.schema import DumpedProblem, Problem
16
+
17
+
18
+ def build_formatted_command(
19
+ command: str, problem: DumpedProblem, lang: Language
20
+ ) -> str:
21
+ return format_vars(
22
+ command,
23
+ **problem.get_vars(),
24
+ file=lang.get_file(problem.code),
25
+ submit_file=lang.get_submit_file(problem.code),
26
+ )
27
+
28
+
29
+ def build_preprocess_commands(problem: DumpedProblem, lang: Language) -> List[str]:
30
+ return [
31
+ build_formatted_command(cmd, problem, lang) for cmd in (lang.preprocess or [])
32
+ ]
33
+
34
+
35
+ def build_preprocess_sandbox_params() -> SandboxParams:
36
+ params = SandboxParams(
37
+ max_processes=None,
38
+ preserve_env=True,
39
+ )
40
+ params.add_mapped_directory(pathlib.Path('/usr'))
41
+ params.add_mapped_directory(pathlib.Path('/etc'))
42
+ return params
43
+
44
+
45
+ def build_compile_grading_artifacts(
46
+ problem: DumpedProblem, lang: Language
47
+ ) -> GradingArtifacts:
48
+ res = GradingArtifacts(root=PosixPath('.'))
49
+ file = lang.get_file(problem.code)
50
+ submit_file = lang.get_submit_file(problem.code)
51
+ # Copy input file.
52
+ res.inputs.append(GradingFileInput(src=PosixPath(file), dest=PosixPath(file)))
53
+ # Copy output file.
54
+ if lang.has_submit_file():
55
+ res.outputs.append(
56
+ GradingFileOutput(
57
+ src=PosixPath(submit_file),
58
+ dest=PosixPath(submit_file),
59
+ )
60
+ )
61
+ # Copy other artifacts.
62
+ for artifact_name, artifact_cfg in lang.artifacts.items():
63
+ artifact_cfg = artifact_cfg or Artifact()
64
+ artifact_path = format_vars(
65
+ artifact_name, **problem.get_vars(), file=file, submit_file=submit_file
66
+ )
67
+ res.outputs.append(
68
+ GradingFileOutput(
69
+ src=PosixPath(artifact_path),
70
+ dest=PosixPath(artifact_cfg.filename or artifact_path),
71
+ optional=artifact_cfg.optional,
72
+ executable=artifact_cfg.executable,
73
+ )
74
+ )
75
+
76
+ return res
77
+
78
+
79
+ def build_run_sandbox_params(problem: Problem, has_input: bool) -> SandboxParams:
80
+ params = SandboxParams()
81
+ params.timeout = problem.timeLimit * 2
82
+ params.wallclock_timeout = problem.timeLimit * 5
83
+ params.address_space = problem.memoryLimit or 1024 # 1 GB
84
+ params.set_stdall(
85
+ stdin=PosixPath('stdin.txt') if has_input else None,
86
+ stdout=PosixPath('stdout.txt'),
87
+ stderr=MERGE_STDERR,
88
+ )
89
+ return params
90
+
91
+
92
+ def build_run_grading_artifacts(
93
+ testcase: TestcaseIO, persist_root: pathlib.Path
94
+ ) -> GradingArtifacts:
95
+ res = GradingArtifacts(root=PosixPath('.'))
96
+ res.inputs.append(
97
+ GradingFileInput(
98
+ src=testcase.input,
99
+ dest=PosixPath('stdin.txt'),
100
+ )
101
+ )
102
+ res.outputs.append(
103
+ GradingFileOutput(
104
+ src=PosixPath('stdout.txt'),
105
+ dest=persist_root / f'stdout-{testcase.index}.txt',
106
+ maxlen=steps.MAX_STDOUT_LEN,
107
+ )
108
+ )
109
+ return res
110
+
111
+
112
+ def build_checker_compile_grading_artifacts(
113
+ problem: DumpedProblem, persist_root: pathlib.Path
114
+ ) -> GradingArtifacts:
115
+ res = GradingArtifacts(root=PosixPath('.'))
116
+ if not problem.checker:
117
+ return res
118
+
119
+ checker_path = PosixPath(problem.checker)
120
+ if not checker_path.is_file():
121
+ checker_path = config.get_builtin_checker(problem.checker)
122
+ if not checker_path:
123
+ return res
124
+
125
+ res.inputs.append(GradingFileInput(src=checker_path, dest=PosixPath('checker.cpp')))
126
+ testlib = config.get_testlib()
127
+ if testlib.is_file():
128
+ res.inputs.append(GradingFileInput(src=testlib, dest=PosixPath('testlib.h')))
129
+ res.outputs.append(
130
+ GradingFileOutput(
131
+ src=PosixPath('checker'), dest=persist_root / 'checker', executable=True
132
+ )
133
+ )
134
+ return res
135
+
136
+
137
+ def build_checker_run_grading_artifacts(
138
+ problem: DumpedProblem, persist_root: pathlib.Path
139
+ ) -> GradingArtifacts:
140
+ res = GradingArtifacts(root=PosixPath('.'))
141
+ if not problem.checker:
142
+ return res
143
+ res.inputs.append(
144
+ GradingFileInput(
145
+ src=persist_root / 'checker', dest=PosixPath('checker'), executable=True
146
+ )
147
+ )
148
+ return res
rbx/hydration.py ADDED
@@ -0,0 +1,101 @@
1
+ import pathlib
2
+ from typing import List, Optional, Tuple
3
+
4
+ from rbx import config, hydration, metadata
5
+ from rbx.console import console
6
+ from rbx.schema import DumpedProblem, Testcase
7
+ from rbx.test import get_testcases_io
8
+
9
+
10
+ def get_testcase_paths(
11
+ root: pathlib.Path, problem: DumpedProblem, i: int
12
+ ) -> Tuple[pathlib.Path, pathlib.Path]:
13
+ return (root / f'{problem.code}.{i}.in', root / f'{problem.code}.{i}.out')
14
+
15
+
16
+ def hydrate_problem(root: pathlib.Path, problem: DumpedProblem):
17
+ for i, testcase in enumerate(problem.tests or []):
18
+ in_path, out_path = get_testcase_paths(root, problem, i)
19
+ in_path.write_text(testcase.input)
20
+ out_path.write_text(testcase.output)
21
+
22
+
23
+ def add_testcase(root: pathlib.Path, problem: DumpedProblem, testcase: Testcase):
24
+ problem_path = metadata.find_problem_path_by_code(problem.code, root)
25
+ if not problem_path or not problem_path.is_file():
26
+ console.print(
27
+ f'[error]Problem [item]{problem.pretty_name()}[/item] not found.[/error]'
28
+ )
29
+ return
30
+
31
+ # Pick next number.
32
+ i = max([tc.index for tc in get_testcases_io(problem, root)] + [-1]) + 1
33
+ in_path, out_path = get_testcase_paths(root, problem, i)
34
+ in_path.write_text(testcase.input)
35
+ out_path.write_text(testcase.output)
36
+
37
+ console.print(
38
+ f'Added testcase [item]{i}[/item] to problem [item]{problem.pretty_name()}[/item].'
39
+ )
40
+
41
+
42
+ def remove_testcase(root: pathlib.Path, problem: DumpedProblem, i: int):
43
+ problem_path = metadata.find_problem_path_by_code(problem.code, root)
44
+ if not problem_path or not problem_path.is_file():
45
+ console.print(
46
+ f'[error]Problem [item]{problem.pretty_name()}[/item] not found.[/error]'
47
+ )
48
+ return
49
+
50
+ testcases = get_testcases_io(problem, root)
51
+ testcases = [testcase for testcase in testcases if testcase.index == i]
52
+ if not testcases:
53
+ console.print(
54
+ f'[error]Testcase [item]{i}[/item] not found in problem [item]{problem.pretty_name()}[/item].[/error]'
55
+ )
56
+ return
57
+ if testcases[0].input:
58
+ testcases[0].input.unlink(missing_ok=True)
59
+ if testcases[0].output:
60
+ testcases[0].output.unlink(missing_ok=True)
61
+
62
+ console.print(
63
+ f'Removed testcase [item]{i}[/item] from problem [item]{problem.pretty_name()}[/item].'
64
+ )
65
+
66
+
67
+ def edit_testcase(root: pathlib.Path, problem: DumpedProblem, i: int):
68
+ problem_path = metadata.find_problem_path_by_code(problem.code, root)
69
+ if not problem_path or not problem_path.is_file():
70
+ console.print(
71
+ f'[error]Problem [item]{problem.pretty_name()}[/item] not found.[/error]'
72
+ )
73
+ return
74
+
75
+ testcases = get_testcases_io(problem, root)
76
+ testcases = [testcase for testcase in testcases if testcase.index == i]
77
+ if not testcases:
78
+ console.print(
79
+ f'[error]Testcase [item]{i}[/item] not found in problem [item]{problem.pretty_name()}[/item].[/error]'
80
+ )
81
+ return
82
+
83
+ paths: List[Optional[pathlib.Path]] = [testcases[0].input, testcases[0].output]
84
+ config.open_editor(*[path for path in paths if path is not None and path.is_file()])
85
+
86
+
87
+ def main(problem: Optional[str] = None):
88
+ problems_to_hydrate = []
89
+ if not problem:
90
+ problems_to_hydrate = metadata.find_problems()
91
+ else:
92
+ dumped_problem = metadata.find_problem_by_anything(problem)
93
+ problems_to_hydrate.append(dumped_problem)
94
+
95
+ root = pathlib.Path()
96
+
97
+ for dumped_problem in problems_to_hydrate:
98
+ console.print(
99
+ f'Hydrating problem [item]{dumped_problem.pretty_name()}[/item]...'
100
+ )
101
+ hydration.hydrate_problem(root, dumped_problem)