rbx.cp 0.5.28__tar.gz → 0.5.29__tar.gz

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 (170) hide show
  1. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/PKG-INFO +1 -4
  2. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/README.md +0 -3
  3. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/pyproject.toml +1 -1
  4. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/builder.py +5 -5
  5. rbx_cp-0.5.29/rbx/box/generators.py +610 -0
  6. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/main.py +10 -7
  7. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/schema.py +15 -6
  8. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/solutions.py +150 -50
  9. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/stresses.py +11 -4
  10. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/testcases.py +17 -1
  11. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/utils.py +8 -2
  12. rbx_cp-0.5.28/rbx/box/generators.py +0 -483
  13. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/LICENSE +0 -0
  14. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/__init__.py +0 -0
  15. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/annotations.py +0 -0
  16. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/autoenum.py +0 -0
  17. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/__init__.py +0 -0
  18. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/cd.py +0 -0
  19. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/checkers.py +0 -0
  20. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/code.py +0 -0
  21. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/compile.py +0 -0
  22. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/conftest.py +0 -0
  23. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/contest/__init__.py +0 -0
  24. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/contest/build_contest_statements.py +0 -0
  25. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/contest/contest_package.py +0 -0
  26. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/contest/contest_utils.py +0 -0
  27. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/contest/main.py +0 -0
  28. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/contest/schema.py +0 -0
  29. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/contest/statements.py +0 -0
  30. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/creation.py +0 -0
  31. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/deferred.py +0 -0
  32. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/download.py +0 -0
  33. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/environment.py +0 -0
  34. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/extensions.py +0 -0
  35. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/generators_test.py +0 -0
  36. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/package.py +0 -0
  37. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/packaging/boca/extension.py +0 -0
  38. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/packaging/boca/packager.py +0 -0
  39. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/packaging/contest_main.py +0 -0
  40. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/packaging/main.py +0 -0
  41. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/packaging/packager.py +0 -0
  42. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/packaging/polygon/packager.py +0 -0
  43. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/packaging/polygon/test.py +0 -0
  44. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/packaging/polygon/xml_schema.py +0 -0
  45. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/presets/__init__.py +0 -0
  46. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/presets/fetch.py +0 -0
  47. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/presets/lock_schema.py +0 -0
  48. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/presets/schema.py +0 -0
  49. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/retries.py +0 -0
  50. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/sanitizers/warning_stack.py +0 -0
  51. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/setter_config.py +0 -0
  52. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/solutions_test.py +0 -0
  53. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/statements/__init__.py +0 -0
  54. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/statements/build_statements.py +0 -0
  55. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/statements/builders.py +0 -0
  56. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/statements/joiners.py +0 -0
  57. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/statements/latex.py +0 -0
  58. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/statements/latex_jinja.py +0 -0
  59. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/statements/schema.py +0 -0
  60. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/stressing/__init__.py +0 -0
  61. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/stressing/finder_parser.py +0 -0
  62. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/stressing/generator_parser.py +0 -0
  63. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/ui/__init__.py +0 -0
  64. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/ui/captured_log.py +0 -0
  65. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/ui/css/app.tcss +0 -0
  66. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/ui/main.py +0 -0
  67. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/ui/run.py +0 -0
  68. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/validators.py +0 -0
  69. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/box/validators_test.py +0 -0
  70. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/checker.py +0 -0
  71. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/clone.py +0 -0
  72. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/config.py +0 -0
  73. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/conftest.py +0 -0
  74. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/console.py +0 -0
  75. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/create.py +0 -0
  76. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/edit.py +0 -0
  77. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/__init__.py +0 -0
  78. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/caching.py +0 -0
  79. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/conftest.py +0 -0
  80. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/judge/__init__.py +0 -0
  81. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/judge/cacher.py +0 -0
  82. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/judge/digester.py +0 -0
  83. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/judge/sandbox.py +0 -0
  84. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/judge/sandboxes/__init__.py +0 -0
  85. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/judge/sandboxes/isolate.py +0 -0
  86. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/judge/sandboxes/stupid_sandbox.py +0 -0
  87. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/judge/sandboxes/timeit.py +0 -0
  88. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/judge/storage.py +0 -0
  89. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/judge/test.py +0 -0
  90. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/judge/testiso.py +0 -0
  91. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/steps.py +0 -0
  92. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/steps_with_caching.py +0 -0
  93. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading/steps_with_caching_run_test.py +0 -0
  94. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/grading_utils.py +0 -0
  95. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/hydration.py +0 -0
  96. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/main.py +0 -0
  97. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/metadata.py +0 -0
  98. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/providers/__init__.py +0 -0
  99. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/providers/codeforces.py +0 -0
  100. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/providers/provider.py +0 -0
  101. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/checkers/boilerplate.cpp +0 -0
  102. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/default_config.json +0 -0
  103. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/default_setter_config.mac.yml +0 -0
  104. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/default_setter_config.yml +0 -0
  105. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/envs/default.rbx.yml +0 -0
  106. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/envs/isolate.rbx.yml +0 -0
  107. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/checker.sh +0 -0
  108. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/compare +0 -0
  109. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/compile/c +0 -0
  110. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/compile/cc +0 -0
  111. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/compile/cpp +0 -0
  112. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/compile/java +0 -0
  113. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/compile/kt +0 -0
  114. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/compile/pas +0 -0
  115. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/compile/py2 +0 -0
  116. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/compile/py3 +0 -0
  117. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/run/c +0 -0
  118. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/run/cc +0 -0
  119. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/run/cpp +0 -0
  120. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/run/java +0 -0
  121. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/run/kt +0 -0
  122. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/run/py2 +0 -0
  123. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/packagers/boca/run/py3 +0 -0
  124. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/contest/contest.rbx.yml +0 -0
  125. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/contest/statement/contest.rbx.tex +0 -0
  126. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/contest/statement/olymp.sty +0 -0
  127. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/contest/statement/template.rbx.tex +0 -0
  128. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/preset.rbx.yml +0 -0
  129. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/.gitignore +0 -0
  130. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/gen.cpp +0 -0
  131. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/problem.rbx.yml +0 -0
  132. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/random.py +0 -0
  133. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/random.txt +0 -0
  134. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/sols/main.cpp +0 -0
  135. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/sols/slow.cpp +0 -0
  136. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/sols/wa.cpp +0 -0
  137. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/statement/olymp.sty +0 -0
  138. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/statement/projecao.png +0 -0
  139. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/statement/statement.rbx.tex +0 -0
  140. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/statement/template.rbx.tex +0 -0
  141. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/tests/samples/000.in +0 -0
  142. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/tests/samples/001.in +0 -0
  143. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/validator.cpp +0 -0
  144. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/presets/default/problem/wcmp.cpp +0 -0
  145. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/resources/templates/template.cpp +0 -0
  146. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/run.py +0 -0
  147. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/schema.py +0 -0
  148. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/submit.py +0 -0
  149. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/submitors/__init__.py +0 -0
  150. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/submitors/codeforces.py +0 -0
  151. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/submitors/submitor.py +0 -0
  152. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/test.py +0 -0
  153. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testcase.py +0 -0
  154. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testcase_rendering.py +0 -0
  155. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/gen1.cpp +0 -0
  156. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/gen2.cpp +0 -0
  157. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/genScript.py +0 -0
  158. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/hard-tle.sol.cpp +0 -0
  159. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/ole.cpp +0 -0
  160. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/problem.rbx.yml +0 -0
  161. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/re.sol.cpp +0 -0
  162. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/sol.cpp +0 -0
  163. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/tests/1.in +0 -0
  164. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/tle-and-incorrect.sol.cpp +0 -0
  165. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/tle.sol.cpp +0 -0
  166. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/validator.cpp +0 -0
  167. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/box1/wa.sol.cpp +0 -0
  168. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/caching/executable.py +0 -0
  169. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testdata/compatible +0 -0
  170. {rbx_cp-0.5.28 → rbx_cp-0.5.29}/rbx/testing_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rbx.cp
3
- Version: 0.5.28
3
+ Version: 0.5.29
4
4
  Summary:
5
5
  Author: Roberto Sales
6
6
  Requires-Python: >=3.9,<4.0
@@ -32,9 +32,6 @@ Requires-Dist: textual (>=0.79.1,<0.80.0)
32
32
  Requires-Dist: typer (>=0.15.1,<0.16.0)
33
33
  Description-Content-Type: text/markdown
34
34
 
35
- <p align="center">
36
- <img src="docs/rbx_transparent.png" width="240px">
37
- </p>
38
35
  <p align="center">
39
36
  <em>The go-to CLI tool for competitive programmers and setters.</em>
40
37
  </p>
@@ -1,6 +1,3 @@
1
- <p align="center">
2
- <img src="docs/rbx_transparent.png" width="240px">
3
- </p>
4
1
  <p align="center">
5
2
  <em>The go-to CLI tool for competitive programmers and setters.</em>
6
3
  </p>
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "rbx.cp"
3
- version = "0.5.28"
3
+ version = "0.5.29"
4
4
  description = ""
5
5
  packages = [
6
6
  {include = "rbx"}
@@ -49,11 +49,11 @@ def build(
49
49
  infos = validate_testcases(s, groups=groups)
50
50
  print_validation_report(infos)
51
51
 
52
- if has_validation_errors(infos):
53
- console.console.print(
54
- '[error]Validation failed, check the report above.[/error]'
55
- )
56
- return False
52
+ if has_validation_errors(infos):
53
+ console.console.print(
54
+ '[error]Validation failed, check the report above.[/error]'
55
+ )
56
+ return False
57
57
 
58
58
  with utils.StatusProgress(
59
59
  'Building outputs for testcases...',
@@ -0,0 +1,610 @@
1
+ import abc
2
+ import pathlib
3
+ import shlex
4
+ import shutil
5
+ from pathlib import PosixPath
6
+ from typing import Dict, List, Optional, Set
7
+
8
+ import typer
9
+ from pydantic import BaseModel
10
+
11
+ from rbx import console
12
+ from rbx.box import checkers, package, testcases, validators
13
+ from rbx.box.code import SanitizationLevel, compile_item, run_item
14
+ from rbx.box.environment import (
15
+ EnvironmentSandbox,
16
+ ExecutionConfig,
17
+ )
18
+ from rbx.box.schema import (
19
+ CodeItem,
20
+ GeneratorCall,
21
+ Testcase,
22
+ TestcaseSubgroup,
23
+ )
24
+ from rbx.box.stressing import generator_parser
25
+ from rbx.box.testcases import TestcaseEntry, find_built_testcases
26
+ from rbx.grading.steps import (
27
+ DigestHolder,
28
+ DigestOrDest,
29
+ DigestOrSource,
30
+ )
31
+ from rbx.utils import StatusProgress
32
+
33
+
34
+ def _compile_generator(generator: CodeItem) -> str:
35
+ return compile_item(generator, sanitized=SanitizationLevel.PREFER)
36
+
37
+
38
+ def _get_group_input(
39
+ group_path: pathlib.Path, subgroup_prefix: str, i: int
40
+ ) -> pathlib.Path:
41
+ return group_path / f'{subgroup_prefix}{i:03d}.in'
42
+
43
+
44
+ def _get_group_output(
45
+ group_path: pathlib.Path, subgroup_prefix: str, i: int
46
+ ) -> pathlib.Path:
47
+ return group_path / f'{subgroup_prefix}{i:03d}.out'
48
+
49
+
50
+ def _fill_output_for_testcase(testcase: Testcase) -> Testcase:
51
+ res = testcase.model_copy()
52
+ if res.outputPath is not None:
53
+ return res
54
+ output_path = res.inputPath.with_suffix('.out')
55
+ if output_path.is_file():
56
+ res.outputPath = output_path
57
+ return res
58
+
59
+
60
+ def _copy_testcase_over(
61
+ testcase: Testcase,
62
+ dest: Testcase,
63
+ ):
64
+ testcase = _fill_output_for_testcase(testcase)
65
+ dest.inputPath.parent.mkdir(parents=True, exist_ok=True)
66
+ shutil.copy(
67
+ str(testcase.inputPath),
68
+ str(dest.inputPath),
69
+ )
70
+ if (
71
+ testcase.outputPath is not None
72
+ and testcase.outputPath.is_file()
73
+ and dest.outputPath is not None
74
+ ):
75
+ dest.outputPath.parent.mkdir(parents=True, exist_ok=True)
76
+ shutil.copy(
77
+ str(testcase.outputPath),
78
+ str(dest.outputPath),
79
+ )
80
+
81
+
82
+ def get_all_built_testcases() -> Dict[str, List[Testcase]]:
83
+ pkg = package.find_problem_package_or_die()
84
+ res = {group.name: find_built_testcases(group) for group in pkg.testcases}
85
+ return res
86
+
87
+
88
+ def get_call_from_string(call_str: str) -> GeneratorCall:
89
+ name, args = call_str.split(None, 1)
90
+ return GeneratorCall(name=name, args=args)
91
+
92
+
93
+ def _run_generator_script(testcase: TestcaseSubgroup) -> str:
94
+ assert testcase.generatorScript is not None
95
+
96
+ cacher = package.get_file_cacher()
97
+
98
+ if not testcase.generatorScript.path.is_file():
99
+ console.console.print(
100
+ f'[error]Generator script not found: [item]{testcase.generatorScript.path}[/item][/error]'
101
+ )
102
+ raise typer.Exit(1)
103
+
104
+ script_digest = DigestHolder()
105
+ if testcase.generatorScript.path.suffix == '.txt':
106
+ script_digest.value = cacher.put_file_from_path(testcase.generatorScript.path)
107
+ else:
108
+ try:
109
+ compiled_digest = compile_item(testcase.generatorScript)
110
+ except:
111
+ console.console.print(
112
+ f'[error]Failed compiling generator script for group [item]{testcase.name}[/item].[/error]'
113
+ )
114
+ raise
115
+
116
+ run_stderr = DigestHolder()
117
+ run_log = run_item(
118
+ testcase.generatorScript,
119
+ DigestOrSource.create(compiled_digest),
120
+ stdout=DigestOrDest.create(script_digest),
121
+ stderr=DigestOrDest.create(run_stderr),
122
+ )
123
+
124
+ if run_log is None or run_log.exitcode != 0:
125
+ console.console.print(
126
+ f'Could not run generator script for group {testcase.name}'
127
+ )
128
+ if run_log is not None:
129
+ console.console.print(
130
+ f'[error]Summary:[/error] {run_log.get_summary()}'
131
+ )
132
+ if run_stderr.value is not None:
133
+ console.console.print('[error]Stderr:[/error]')
134
+ console.console.print(
135
+ package.get_digest_as_string(run_stderr.value) or ''
136
+ )
137
+ raise typer.Exit(1)
138
+
139
+ assert script_digest.value
140
+ script = cacher.get_file_content(script_digest.value).decode()
141
+ return script
142
+
143
+
144
+ def _extract_script_lines(script: str):
145
+ lines = script.splitlines()
146
+ for line in lines:
147
+ line = line.strip()
148
+ if not line:
149
+ continue
150
+ if line.startswith('#'):
151
+ continue
152
+ yield shlex.split(line)[0], shlex.join(shlex.split(line)[1:])
153
+
154
+
155
+ class GenerationMetadata(BaseModel):
156
+ copied_to: Testcase
157
+
158
+ copied_from: Optional[Testcase] = None
159
+ generator_call: Optional[GeneratorCall] = None
160
+
161
+
162
+ class GenerationTestcaseEntry(BaseModel):
163
+ group_entry: TestcaseEntry
164
+ subgroup_entry: TestcaseEntry
165
+
166
+ metadata: GenerationMetadata
167
+
168
+
169
+ class TestcaseVisitor(abc.ABC):
170
+ @abc.abstractmethod
171
+ def visit(self, entry: GenerationTestcaseEntry):
172
+ pass
173
+
174
+ def should_visit_group(self, group_name: str) -> bool:
175
+ return True
176
+
177
+ def should_visit_subgroup(self, subgroup_path: str) -> bool:
178
+ return True
179
+
180
+ def should_visit_generator_scripts(
181
+ self, group_name: str, subgroup_path: str
182
+ ) -> bool:
183
+ return True
184
+
185
+
186
+ class TestcaseGroupVisitor(TestcaseVisitor):
187
+ def __init__(self, groups: Optional[Set[str]] = None):
188
+ self.groups = groups
189
+
190
+ def should_visit_group(self, group_name: str) -> bool:
191
+ return self.groups is None or group_name in self.groups
192
+
193
+
194
+ def run_testcase_visitor(visitor: TestcaseVisitor):
195
+ pkg = package.find_problem_package_or_die()
196
+
197
+ def _explore_subgroup(
198
+ subgroup: TestcaseSubgroup, subgroup_index: int, prefix: List[str]
199
+ ):
200
+ assert prefix and len(prefix) >= 1 and len(prefix) <= 2
201
+ group_path = prefix[0]
202
+ subgroup_path = '/'.join(prefix)
203
+ if not visitor.should_visit_subgroup(subgroup_path):
204
+ return
205
+
206
+ def _entry(i: int) -> TestcaseEntry:
207
+ return TestcaseEntry(group=group_path, index=i)
208
+
209
+ def _sub_entry(i: int) -> TestcaseEntry:
210
+ return TestcaseEntry(group=subgroup_path, index=i)
211
+
212
+ def _copied_to(i: int) -> Testcase:
213
+ group_fs_path = package.get_build_testgroup_path(group_path)
214
+ group_prefix = ''
215
+ if len(prefix) == 2:
216
+ group_prefix = f'{subgroup_index}-{prefix[1]}-'
217
+ return Testcase(
218
+ inputPath=_get_group_input(group_fs_path, group_prefix, i),
219
+ outputPath=_get_group_output(group_fs_path, group_prefix, i),
220
+ )
221
+
222
+ # Go through testcases.
223
+ i = 0
224
+ # Individual testcases.
225
+ for tc in subgroup.testcases or []:
226
+ visitor.visit(
227
+ GenerationTestcaseEntry(
228
+ group_entry=_entry(i),
229
+ subgroup_entry=_sub_entry(i),
230
+ metadata=GenerationMetadata(
231
+ copied_from=_fill_output_for_testcase(tc),
232
+ copied_to=_copied_to(i),
233
+ ),
234
+ )
235
+ )
236
+ i += 1
237
+
238
+ # Glob testcases.
239
+ if subgroup.testcaseGlob:
240
+ matched_inputs = sorted(PosixPath().glob(subgroup.testcaseGlob))
241
+
242
+ for input_path in matched_inputs:
243
+ if not input_path.is_file() or input_path.suffix != '.in':
244
+ continue
245
+
246
+ tc = Testcase(inputPath=input_path)
247
+ visitor.visit(
248
+ GenerationTestcaseEntry(
249
+ group_entry=_entry(i),
250
+ subgroup_entry=_sub_entry(i),
251
+ metadata=GenerationMetadata(
252
+ copied_from=_fill_output_for_testcase(tc),
253
+ copied_to=_copied_to(i),
254
+ ),
255
+ )
256
+ )
257
+ i += 1
258
+
259
+ # Single generators.
260
+ for generator_call in subgroup.generators:
261
+ visitor.visit(
262
+ GenerationTestcaseEntry(
263
+ group_entry=_entry(i),
264
+ subgroup_entry=_sub_entry(i),
265
+ metadata=GenerationMetadata(
266
+ generator_call=generator_call,
267
+ copied_to=_copied_to(i),
268
+ ),
269
+ )
270
+ )
271
+ i += 1
272
+
273
+ if not visitor.should_visit_generator_scripts(group_path, subgroup_path):
274
+ return
275
+
276
+ # Run generator script.
277
+ if subgroup.generatorScript is not None:
278
+ script = _run_generator_script(subgroup)
279
+
280
+ # Run each line from generator script.
281
+ for generator_name, args in _extract_script_lines(script):
282
+ call = GeneratorCall(name=generator_name, args=args)
283
+ visitor.visit(
284
+ GenerationTestcaseEntry(
285
+ group_entry=_entry(i),
286
+ subgroup_entry=_sub_entry(i),
287
+ metadata=GenerationMetadata(
288
+ generator_call=call,
289
+ copied_to=_copied_to(i),
290
+ ),
291
+ )
292
+ )
293
+ i += 1
294
+
295
+ for group in pkg.testcases:
296
+ if not visitor.should_visit_group(group.name):
297
+ continue
298
+
299
+ _explore_subgroup(group, 0, [group.name])
300
+
301
+ for i, subgroup in enumerate(group.subgroups):
302
+ _explore_subgroup(subgroup, i, [group.name, subgroup.name])
303
+
304
+
305
+ def _get_necessary_generators_for_groups(
306
+ groups: Optional[Set[str]] = None,
307
+ ) -> Set[str]:
308
+ pkg = package.find_problem_package_or_die()
309
+ existing_generators = set(generator.name for generator in pkg.generators)
310
+ necessary_generators = set()
311
+
312
+ class NecessaryGeneratorsVisitor(TestcaseGroupVisitor):
313
+ def visit(self, entry: GenerationTestcaseEntry):
314
+ if entry.metadata.generator_call is not None:
315
+ necessary_generators.add(entry.metadata.generator_call.name)
316
+
317
+ run_testcase_visitor(NecessaryGeneratorsVisitor(groups))
318
+
319
+ return existing_generators.intersection(necessary_generators)
320
+
321
+
322
+ def compile_generators(
323
+ progress: Optional[StatusProgress] = None,
324
+ tracked_generators: Optional[Set[str]] = None,
325
+ ) -> Dict[str, str]:
326
+ def update_status(text: str):
327
+ if progress is not None:
328
+ progress.update(text)
329
+
330
+ pkg = package.find_problem_package_or_die()
331
+
332
+ generator_to_compiled_digest = {}
333
+
334
+ for generator in pkg.generators:
335
+ if tracked_generators is not None and generator.name not in tracked_generators:
336
+ continue
337
+ update_status(f'Compiling generator [item]{generator.name}[/item]')
338
+ try:
339
+ generator_to_compiled_digest[generator.name] = _compile_generator(generator)
340
+ except:
341
+ console.console.print(
342
+ f'[error]Failed compiling generator [item]{generator.name}[/item].[/error]'
343
+ )
344
+ raise
345
+
346
+ return generator_to_compiled_digest
347
+
348
+
349
+ def expand_generator_call(call: GeneratorCall) -> GeneratorCall:
350
+ vars = package.find_problem_package_or_die().expanded_vars
351
+ generator_for_args = generator_parser.Generator(vars)
352
+ parsed_args = generator_parser.parse(call.args or '')
353
+ return call.model_copy(update={'args': generator_for_args.generate(parsed_args)})
354
+
355
+
356
+ def generate_standalone(
357
+ spec: GenerationMetadata,
358
+ validate: bool = True,
359
+ group_entry: Optional[TestcaseEntry] = None,
360
+ generator_digest: Optional[str] = None,
361
+ validator_digest: Optional[str] = None,
362
+ progress: Optional[StatusProgress] = None,
363
+ ):
364
+ def _print_error_header(text: Optional[str] = None):
365
+ prefix = 'Failed generating test'
366
+ if group_entry is not None:
367
+ prefix += (
368
+ f' [item]{group_entry.group}[/item]/[item]{group_entry.index}[/item]'
369
+ )
370
+ suffix = '.'
371
+ if text:
372
+ suffix = f': {text}'
373
+ if spec.generator_call is not None:
374
+ console.console.print(
375
+ f'[error]{prefix} using generator call [info]{spec.generator_call.name} {spec.generator_call.args}[/info]{suffix}[/error]'
376
+ )
377
+ else:
378
+ console.console.print(f'[error]{prefix}{suffix}[/error]')
379
+
380
+ if spec.generator_call is not None:
381
+ call = spec.generator_call
382
+
383
+ generation_stderr = DigestHolder()
384
+
385
+ # Get generator item
386
+ generator = package.get_generator(call.name)
387
+ if generator_digest is None:
388
+ if progress:
389
+ progress.update(f'Compiling generator {generator.name}...')
390
+ generator_digest = _compile_generator(generator)
391
+
392
+ if progress:
393
+ progress.update(
394
+ f'Generating testcase [status]{generator.name} {call.args}[/status]...'
395
+ )
396
+ generation_log = run_item(
397
+ generator,
398
+ DigestOrSource.create(generator_digest),
399
+ stdout=DigestOrDest.create(spec.copied_to.inputPath),
400
+ stderr=DigestOrDest.create(generation_stderr),
401
+ extra_args=call.args or None,
402
+ )
403
+ if not generation_log or generation_log.exitcode != 0:
404
+ _print_error_header()
405
+ if generation_log is not None:
406
+ console.console.print(
407
+ f'[error]Summary:[/error] {generation_log.get_summary()}'
408
+ )
409
+ if generation_stderr.value is not None:
410
+ console.console.print('[error]Stderr:[/error]')
411
+ console.console.print(
412
+ package.get_digest_as_string(generation_stderr.value) or ''
413
+ )
414
+
415
+ raise typer.Exit(1)
416
+ elif spec.copied_from is not None:
417
+ _copy_testcase_over(spec.copied_from, spec.copied_to)
418
+
419
+ validator = package.get_validator_or_nil()
420
+ # Run validator, if it is available.
421
+ if validator is not None and validate:
422
+ if validator_digest is None:
423
+ if progress:
424
+ progress.update('Compiling validator...')
425
+ validator_tp = validators.compile_main_validator()
426
+ assert validator_tp is not None
427
+ _, validator_digest = validator_tp
428
+ if progress:
429
+ progress.update('Validating test...')
430
+ ok, message, *_ = validators.validate_test(
431
+ spec.copied_to.inputPath,
432
+ validator,
433
+ validator_digest,
434
+ )
435
+ if not ok:
436
+ _print_error_header('Failed validating testcase.')
437
+ console.console.print(f'[error]Message:[/error] {message}')
438
+ console.console.print(
439
+ f'Testcase written at [item]{spec.copied_to.inputPath}[/item]'
440
+ )
441
+ raise typer.Exit(1)
442
+
443
+
444
+ def generate_testcases(
445
+ progress: Optional[StatusProgress] = None, groups: Optional[Set[str]] = None
446
+ ):
447
+ def step():
448
+ if progress is not None:
449
+ progress.step()
450
+
451
+ compiled_generators = compile_generators(
452
+ progress=progress,
453
+ tracked_generators=_get_necessary_generators_for_groups(groups)
454
+ if groups is not None
455
+ else None,
456
+ )
457
+
458
+ testcases.clear_built_testcases()
459
+
460
+ class BuildTestcaseVisitor(TestcaseGroupVisitor):
461
+ def visit(self, entry: GenerationTestcaseEntry):
462
+ if entry.metadata.copied_from is not None:
463
+ _copy_testcase_over(
464
+ entry.metadata.copied_from,
465
+ entry.metadata.copied_to,
466
+ )
467
+
468
+ if entry.metadata.generator_call is not None:
469
+ generate_standalone(
470
+ entry.metadata,
471
+ group_entry=entry.group_entry,
472
+ validate=False,
473
+ generator_digest=compiled_generators[
474
+ entry.metadata.generator_call.name
475
+ ],
476
+ )
477
+ step()
478
+
479
+ run_testcase_visitor(BuildTestcaseVisitor(groups))
480
+
481
+
482
+ def generate_output_for_testcase(
483
+ main_solution_digest: str,
484
+ testcase: Testcase,
485
+ stderr_path: Optional[pathlib.Path] = None,
486
+ ):
487
+ assert testcase.outputPath is not None
488
+
489
+ if testcase.outputPath.is_file():
490
+ # Output file was already copied over from manual tests.
491
+ return
492
+
493
+ pkg = package.find_problem_package_or_die()
494
+ main_solution = package.get_main_solution()
495
+ if main_solution is None:
496
+ return
497
+
498
+ # Obey no limits when generating testcases.
499
+ sandbox = EnvironmentSandbox()
500
+ sandbox.fileSizeLimit = pkg.outputLimit
501
+ extra_config = ExecutionConfig(sandbox=sandbox)
502
+
503
+ try:
504
+ run_log = run_item(
505
+ main_solution,
506
+ DigestOrSource.create(main_solution_digest),
507
+ stdin=DigestOrSource.create(testcase.inputPath),
508
+ stdout=DigestOrDest.create(testcase.outputPath),
509
+ stderr=DigestOrDest.create(stderr_path)
510
+ if stderr_path is not None
511
+ else None,
512
+ extra_config=extra_config,
513
+ )
514
+ except:
515
+ console.console.print(
516
+ '[error]Failed running main solution to generate testcase.[/error]'
517
+ )
518
+ raise
519
+
520
+ if run_log is None or run_log.exitcode != 0:
521
+ console.console.print(
522
+ f'[error]Failed generating output for [item]{testcase.inputPath}[/item][/error]',
523
+ )
524
+ if run_log is not None:
525
+ console.console.print(f'[error]Summary:[/error] {run_log.get_summary()}')
526
+ checker_result = checkers.check_with_no_output(run_log)
527
+ console.console.print(
528
+ f'[warning]Verdict: [item]{checker_result.outcome.value}[/item][/warning]',
529
+ )
530
+ console.console.print(
531
+ f'[warning]Message: [info]{checker_result.message}[/info][/warning]',
532
+ )
533
+ console.console.print(f'Input written at [item]{testcase.inputPath}[/item]')
534
+ console.console.print(
535
+ f'Output written at [item]{testcase.outputPath}[/item]'
536
+ )
537
+ console.console.print(f'Stderr written at [item]{stderr_path}[/item]')
538
+ raise typer.Exit(1)
539
+
540
+
541
+ def generate_outputs_for_testcases(
542
+ progress: Optional[StatusProgress] = None, groups: Optional[Set[str]] = None
543
+ ):
544
+ def step():
545
+ if progress is not None:
546
+ progress.step()
547
+
548
+ main_solution = package.get_main_solution()
549
+ solution_digest: Optional[str] = None
550
+
551
+ if main_solution is not None:
552
+ if progress:
553
+ progress.update('Compiling main solution...')
554
+ try:
555
+ solution_digest = compile_item(main_solution)
556
+ except:
557
+ console.console.print('[error]Failed compiling main solution.[/error]')
558
+ raise
559
+
560
+ gen_runs_dir = package.get_problem_runs_dir() / '.gen'
561
+ shutil.rmtree(str(gen_runs_dir), ignore_errors=True)
562
+ gen_runs_dir.mkdir(parents=True, exist_ok=True)
563
+
564
+ class GenerateOutputsVisitor(TestcaseGroupVisitor):
565
+ def visit(self, entry: GenerationTestcaseEntry):
566
+ tc = entry.metadata.copied_to
567
+ if not tc.inputPath.is_file():
568
+ return
569
+ assert tc.outputPath is not None
570
+
571
+ if (
572
+ main_solution is None or solution_digest is None
573
+ ) and not tc.outputPath.is_file():
574
+ console.console.print(
575
+ '[error]No main solution found to generate outputs for testcases.[/error]',
576
+ )
577
+ raise typer.Exit(1)
578
+
579
+ assert solution_digest is not None
580
+ generate_output_for_testcase(
581
+ solution_digest,
582
+ tc,
583
+ gen_runs_dir / 'main.stderr',
584
+ )
585
+ step()
586
+
587
+ run_testcase_visitor(GenerateOutputsVisitor(groups))
588
+
589
+
590
+ def extract_generation_testcases(
591
+ entries: List[TestcaseEntry],
592
+ ) -> List[GenerationTestcaseEntry]:
593
+ # TODO: support subgroups.
594
+ groups = set(entry.group for entry in entries)
595
+ entry_keys = set(entry.key() for entry in entries)
596
+
597
+ res: List[GenerationTestcaseEntry] = []
598
+
599
+ class ExtractGenerationTestcasesVisitor(TestcaseVisitor):
600
+ def should_visit_group(self, group_name: str) -> bool:
601
+ return group_name in groups
602
+
603
+ def visit(self, entry: GenerationTestcaseEntry):
604
+ # TODO: support subgroups.
605
+ if entry.group_entry.key() not in entry_keys:
606
+ return
607
+ res.append(entry)
608
+
609
+ run_testcase_visitor(ExtractGenerationTestcasesVisitor())
610
+ return res
@@ -48,6 +48,7 @@ from rbx.box.solutions import (
48
48
  run_solutions,
49
49
  )
50
50
  from rbx.box.statements import build_statements
51
+ from rbx.box.testcases import TestcaseEntry
51
52
 
52
53
  app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
53
54
  app.add_typer(
@@ -343,6 +344,14 @@ def irun(
343
344
  '-g',
344
345
  help='Generator call to use to generate a single test for execution.',
345
346
  ),
347
+ testcase: Optional[str] = typer.Option(
348
+ None,
349
+ '--testcase',
350
+ '--test',
351
+ '-tc',
352
+ '-t',
353
+ help='Testcase to run, in the format "[group]/[index]". If not specified, will run interactively.',
354
+ ),
346
355
  print: bool = typer.Option(
347
356
  False, '--print', '-p', help='Whether to print outputs to terminal.'
348
357
  ),
@@ -370,13 +379,6 @@ def irun(
370
379
  )
371
380
  return
372
381
 
373
- main_solution = package.get_main_solution()
374
- if check and main_solution is None:
375
- console.console.print(
376
- '[warning]No main solution found, running without checkers.[/warning]'
377
- )
378
- check = False
379
-
380
382
  tracked_solutions = None
381
383
  if outcome is not None:
382
384
  tracked_solutions = {
@@ -411,6 +413,7 @@ def irun(
411
413
  generator=generators.get_call_from_string(generator)
412
414
  if generator is not None
413
415
  else None,
416
+ testcase_entry=TestcaseEntry.parse(testcase) if testcase else None,
414
417
  print=print,
415
418
  sanitized=sanitized,
416
419
  )