codeplain 0.3.4__py3-none-any.whl → 0.3.5__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.
- _version.py +1 -1
- {codeplain-0.3.4.dist-info → codeplain-0.3.5.dist-info}/METADATA +7 -7
- {codeplain-0.3.4.dist-info → codeplain-0.3.5.dist-info}/RECORD +25 -25
- file_utils.py +13 -2
- plain2code.py +71 -35
- plain2code_exceptions.py +5 -0
- plain2code_state.py +1 -0
- plain2code_telemetry.py +2 -0
- plain2code_utils.py +18 -0
- plain_file.py +53 -1
- plain_spec.py +9 -0
- render_machine/actions/fix_conformance_test.py +23 -1
- render_machine/actions/prepare_testing_environment.py +6 -0
- render_machine/actions/refactor_code.py +18 -0
- render_machine/actions/render_functional_requirement.py +15 -1
- render_machine/render_context.py +0 -55
- render_machine/state_machine_config.py +7 -14
- tests/test_file_utils.py +53 -2
- tests/test_plain2code.py +94 -0
- tests/test_plainfileparser.py +108 -0
- tests/test_plainspec.py +36 -0
- tests/test_telemetry.py +16 -1
- standard_template_library/__init__.py +0 -1
- {codeplain-0.3.4.dist-info → codeplain-0.3.5.dist-info}/WHEEL +0 -0
- {codeplain-0.3.4.dist-info → codeplain-0.3.5.dist-info}/entry_points.txt +0 -0
- {codeplain-0.3.4.dist-info → codeplain-0.3.5.dist-info}/licenses/LICENSE +0 -0
_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.3.
|
|
1
|
+
__version__ = "0.3.5"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeplain
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Transform plain language specifications into working code
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -12,20 +12,20 @@ Requires-Dist: gitpython==3.1.50
|
|
|
12
12
|
Requires-Dist: mistletoe==1.3.0
|
|
13
13
|
Requires-Dist: networkx==3.6.1
|
|
14
14
|
Requires-Dist: python-frontmatter==1.3.0
|
|
15
|
-
Requires-Dist: python-liquid2==0.
|
|
15
|
+
Requires-Dist: python-liquid2==0.4.0
|
|
16
16
|
Requires-Dist: pyyaml==6.0.3
|
|
17
|
-
Requires-Dist: requests==2.34.
|
|
17
|
+
Requires-Dist: requests==2.34.2
|
|
18
18
|
Requires-Dist: rich==15.0.0
|
|
19
|
-
Requires-Dist: sentry-sdk==2.
|
|
19
|
+
Requires-Dist: sentry-sdk==2.63.0
|
|
20
20
|
Requires-Dist: textual>=7.5.0
|
|
21
|
-
Requires-Dist: tiktoken==0.
|
|
21
|
+
Requires-Dist: tiktoken==0.13.0
|
|
22
22
|
Requires-Dist: transitions==0.9.3
|
|
23
23
|
Provides-Extra: dev
|
|
24
24
|
Requires-Dist: black==26.3.1; extra == 'dev'
|
|
25
25
|
Requires-Dist: flake8==7.3.0; extra == 'dev'
|
|
26
26
|
Requires-Dist: isort==5.13.2; extra == 'dev'
|
|
27
|
-
Requires-Dist: mypy==1.
|
|
28
|
-
Requires-Dist: pytest==
|
|
27
|
+
Requires-Dist: mypy==2.1.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest==9.1.1; extra == 'dev'
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
|
|
31
31
|
# Codeplain plain2code renderer
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
_version.py,sha256=
|
|
1
|
+
_version.py,sha256=ThnCuF3X7rsQSd5PAea_jfYA70ZmhLvkFcLBxBPwZnY,22
|
|
2
2
|
change_detection.py,sha256=CazORZrezeZSw2I6VBvieN-c2SEjieZPo6pNrKwmMVA,6915
|
|
3
3
|
codeplain_REST_api.py,sha256=h_XoNws8fvf7OTtoVXhHh6OfLGg77UNFuggMBwaqBwE,21533
|
|
4
4
|
concept_utils.py,sha256=D4qVHFBRvfJZwiIq1AD_SV0Ev-RnOMg2yGWo6Cal7PM,7913
|
|
5
5
|
diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
|
|
6
6
|
event_bus.py,sha256=g5XLwRfjrvTIcB6urjREuCJzZj5TA93hy8lKXLGXuCE,650
|
|
7
|
-
file_utils.py,sha256=
|
|
7
|
+
file_utils.py,sha256=q-ApwiLLJCgmmoy6-nf1QM9DaFXwzCJek6B-jQTI9ak,11665
|
|
8
8
|
git_utils.py,sha256=LQQnL5DAFpH5fDBQgUZX2WfjWY4YC5OEGBPIXryTmnY,16896
|
|
9
9
|
memory_management.py,sha256=nHNudgaOqp5UGkIMi62TRCc2nUeFOM0w-I1FfxBFaQE,6120
|
|
10
10
|
module_renderer.py,sha256=pXMXKKGMhUwLLlSxppE_hpZlwpJ3WSHSfR9FQxt6rSI,6860
|
|
11
11
|
partial_rendering.py,sha256=0ChOaGytogBUEGISpbWkbjT9Hqy8pnOkJdJXcitJDgk,12219
|
|
12
|
-
plain2code.py,sha256=
|
|
12
|
+
plain2code.py,sha256=etLa8WagRjtHrWIUt1SYO1cQNAW0sc7AqMqZQTk9gGo,15269
|
|
13
13
|
plain2code_arguments.py,sha256=J05NwaY-O04rIWVF2yIXysNB_ofZS0wJFn0u1WaCCSA,15399
|
|
14
14
|
plain2code_console.py,sha256=YdE-Kz9qRAzIIAg7aYA1bnXRcwy_aTpbFtkR2zHbd-I,4918
|
|
15
15
|
plain2code_events.py,sha256=AM8KJS68uiVXRMLt861KaGhs2H4yzujlMB_xiYPfHQc,1478
|
|
16
|
-
plain2code_exceptions.py,sha256=
|
|
16
|
+
plain2code_exceptions.py,sha256=WQeBiqKC-gGqKuj_eBbXEtCC3X7mUJAALE9zrDBBEAE,1800
|
|
17
17
|
plain2code_logger.py,sha256=kNKIfkS6Ydtut7Ptw5jmv5bFPgXlxB-_Ds5nd-j4d9o,4609
|
|
18
18
|
plain2code_nodes.py,sha256=IwjQqJxRSJKgP6eEnkYGy3VQHIyoazgkFxAEkxkjfJ8,4426
|
|
19
19
|
plain2code_read_config.py,sha256=D5CxnmTRjddupecWvstcUm0kIJWWzq5-Zmk6sU0wbIg,2235
|
|
20
|
-
plain2code_state.py,sha256=
|
|
21
|
-
plain2code_telemetry.py,sha256=
|
|
22
|
-
plain2code_utils.py,sha256=
|
|
23
|
-
plain_file.py,sha256=
|
|
20
|
+
plain2code_state.py,sha256=1VwyxgKdA13fAJNKSiPBfBb3A20TKfGCNnOm06YdcS4,2233
|
|
21
|
+
plain2code_telemetry.py,sha256=7yuTH7mQE_wGVW2H1oiVZ3UJg98ArB5HcUUm9isnv9g,4354
|
|
22
|
+
plain2code_utils.py,sha256=id04t9Iez5pYygrE3fKEyrcfWDcniKFT2iQoF2CzTJs,1409
|
|
23
|
+
plain_file.py,sha256=WMz4DqlBAGpPa1xqf9vuUj6vzXJTKeNyUtSRz1EOvso,32282
|
|
24
24
|
plain_modules.py,sha256=uvRPfxBJwstaWi5lgYKgtQdhhf0Naiy2mS41tLw_TjY,16186
|
|
25
|
-
plain_spec.py,sha256=
|
|
25
|
+
plain_spec.py,sha256=7nb5lLJKHZ3uu9THkSXyJ5brIKkbGW0ooVfRclLdlzA,16062
|
|
26
26
|
system_config.py,sha256=trgytfXKXoCn0Pgy9u00x2D0ZRHlh_tADlbkwZnF8QE,1038
|
|
27
27
|
cli_output/__init__.py,sha256=MBKl0YbVpWE-tBa3V5_aVbB7vbaqlIK2NsHTjRUZqis,284
|
|
28
28
|
cli_output/dry_run.py,sha256=6NKhNWPtD0CBK_QHSsSqIKLTzibTwxS9d8ltP9d1bgU,1473
|
|
@@ -36,10 +36,10 @@ render_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
36
36
|
render_machine/code_renderer.py,sha256=-vp_ltU7e3ssX9hbS8IY4ETSp-39JAuoJ3ZltKpdt1M,3959
|
|
37
37
|
render_machine/conformance_tests.py,sha256=MeEveEGJTClLGxWw04P4TR6U9kX9Qgz9qtxNrB9a8QI,7085
|
|
38
38
|
render_machine/implementation_code_helpers.py,sha256=LdHNLoKgVtrDaGH2APX8C9vjNjLBUhu2V7OAaJvm4rM,2009
|
|
39
|
-
render_machine/render_context.py,sha256=
|
|
39
|
+
render_machine/render_context.py,sha256=fjU_S5W5HoAX3-WlEM8hp4DKMX_NDgVlfwQUAp0yaf8,25633
|
|
40
40
|
render_machine/render_types.py,sha256=cT3D0lmxQ3enIo0jGp3LEGCY2t3X1Q1_tqwcadzSAv0,8761
|
|
41
41
|
render_machine/render_utils.py,sha256=ao0e64VSOGYfb8hwJXIZlbqer5HjNMYJWHlLDoHnJyc,9123
|
|
42
|
-
render_machine/state_machine_config.py,sha256=
|
|
42
|
+
render_machine/state_machine_config.py,sha256=NnX5sBGkc6cNG8OOQ4PrJeJBCADRGZKJYQXV7ndR4bs,26789
|
|
43
43
|
render_machine/states.py,sha256=niFXqayyA_rezCrlzo0hYeHAruErcPzQVF-BeIBGnL8,1968
|
|
44
44
|
render_machine/triggers.py,sha256=VekNhHGR-u5JXIboqc_Eyqf73LNH7wezLK7r3WWtodk,1269
|
|
45
45
|
render_machine/actions/analyze_specification_ambiguity.py,sha256=SykZAozGvr1jySxbpKCgt11nMpZhOK0sN-J66M42T5Y,2847
|
|
@@ -49,17 +49,16 @@ render_machine/actions/commit_implementation_code_changes.py,sha256=yi51oAArLCgW
|
|
|
49
49
|
render_machine/actions/create_dist.py,sha256=xMPPyws7swVkaUSQg3dwMGScCi1BuII9y2C_XSP0N80,1067
|
|
50
50
|
render_machine/actions/exit_with_error.py,sha256=-LkQsFhmV-Nr741APTx38dqu473b6OSiIvKayFFsXxI,1270
|
|
51
51
|
render_machine/actions/finish_functional_requirement.py,sha256=usQgPSg6PHpBOR1RhK7lPz9zjY-2VJJDqi3T0nlqdLw,841
|
|
52
|
-
render_machine/actions/fix_conformance_test.py,sha256=
|
|
52
|
+
render_machine/actions/fix_conformance_test.py,sha256=jtVeL11J8K-a5YvoR3uW6TVytQZkiBgXlDyFR3wXYEs,10022
|
|
53
53
|
render_machine/actions/fix_unit_tests.py,sha256=hvwvCJDk_-8fHMpkNFyeBSG1IshIcufDpymF00ulPXY,2301
|
|
54
54
|
render_machine/actions/prepare_repositories.py,sha256=eE-uU6H_K5IQMraVWSRi4TKk_XPrnA878T_JwtI9cWo,3410
|
|
55
|
-
render_machine/actions/prepare_testing_environment.py,sha256=
|
|
56
|
-
render_machine/actions/refactor_code.py,sha256=
|
|
55
|
+
render_machine/actions/prepare_testing_environment.py,sha256=kIsXgV6-jOlqp-04cimPgTQoHPrVIu0zW-H_Jj-jowE,2106
|
|
56
|
+
render_machine/actions/refactor_code.py,sha256=gyPsnqgHseL2bkENu4WeGCRyV0R18_GH3JOrPdoxKvg,2609
|
|
57
57
|
render_machine/actions/render_conformance_tests.py,sha256=5gOQn-G1T9UWEI8sDfALcVdArk0ln9DgUprOPjccS-U,9268
|
|
58
|
-
render_machine/actions/render_functional_requirement.py,sha256=
|
|
58
|
+
render_machine/actions/render_functional_requirement.py,sha256=uiEwrMdArRdPX7eWysYMTI5y2pXacUySTpIgL2O-lOI,4602
|
|
59
59
|
render_machine/actions/run_conformance_tests.py,sha256=4sJTORl6YLFXQIdLMBQHDeJd2hztGyt3tsVvrCviBko,4008
|
|
60
60
|
render_machine/actions/run_unit_tests.py,sha256=A4WXtSzpCLDHYR8fQAbfxUf_E19GYflZSHZaSreoFPk,2101
|
|
61
61
|
render_machine/actions/summarize_conformance_tests.py,sha256=Vy1kRK8vyazK_BbsKuz1MLDN_TQWVxxzkwEEm-50byc,1639
|
|
62
|
-
standard_template_library/__init__.py,sha256=LtsHl0c3VV46DUInIqyOw4KKVoxKEMOx8dAZpMXtAXc,43
|
|
63
62
|
standard_template_library/golang-console-app-template.plain,sha256=JOusEjCj7jagnXUzaI-SdIr3hj1wTo2uShK017aNtVM,949
|
|
64
63
|
standard_template_library/python-console-app-template.plain,sha256=HW-REeF8Crk0r_w4-MxnleVHeW6PdOB2C8gO8FDG61g,931
|
|
65
64
|
standard_template_library/typescript-react-app-boilerplate.plain,sha256=6LFxhEOzWnDHdVpFj-XBTq6s4_2dy7LKV3lVkCzv8i4,114
|
|
@@ -68,17 +67,18 @@ tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
|
|
|
68
67
|
tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
|
|
69
68
|
tests/test_change_detection.py,sha256=jH8SEhndX400DNqIKmcYLzxHqq9WFdgmuvY3XPq-ZFc,13534
|
|
70
69
|
tests/test_cli_output.py,sha256=owDP9pLHdErtPAr1paNKp1o044kuH-HxInHLIBLZQFI,18420
|
|
71
|
-
tests/test_file_utils.py,sha256
|
|
70
|
+
tests/test_file_utils.py,sha256=R_ZIhrTtwlhHxgmbBtA8PFEU5-QB4Fi5skJ7PS2KzdU,3614
|
|
72
71
|
tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
|
|
73
72
|
tests/test_imports.py,sha256=EWYLUxGunUUdsG9iCiY_tts6T7DIVdhdB_MZlB1LsmU,2081
|
|
74
73
|
tests/test_partial_rendering.py,sha256=r0RUUd1pI4-2ZkRrM1j8_kUqv-70hrOGjuKBhZoRmH0,22758
|
|
74
|
+
tests/test_plain2code.py,sha256=DQYFc0JVSkMI8LZCNaQ__2Nph-DcZZW17rprJA54eyw,3851
|
|
75
75
|
tests/test_plain_modules.py,sha256=S7sd4ZYNotXOgCyZzmBd714QFLih69nlZ3tV8WNtVPo,13766
|
|
76
76
|
tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
|
|
77
|
-
tests/test_plainfileparser.py,sha256=
|
|
78
|
-
tests/test_plainspec.py,sha256=
|
|
77
|
+
tests/test_plainfileparser.py,sha256=DXY7HSKDMaB4nqUwn_T5eyySEZhnfqDjHntkuTsGhY0,21442
|
|
78
|
+
tests/test_plainspec.py,sha256=ng55E8ajrH2-gPC1p_QDIe_zXplPvoqHJ-d57UWKFTw,3577
|
|
79
79
|
tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
|
|
80
80
|
tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
|
|
81
|
-
tests/test_telemetry.py,sha256=
|
|
81
|
+
tests/test_telemetry.py,sha256=hjm81aRQQZcZ0RB6aVbS3WO46_nIAii_csmbbtnUh74,7110
|
|
82
82
|
tests/e2e/conftest.py,sha256=5Dm67UTOE-M6hMb-mqZvwa6Ek_wmZgsR7fUv9ZEqCwY,4164
|
|
83
83
|
tests/e2e/test_hello_world_python.py,sha256=PQy4elzB7WvpiWLKa6_HFO0K8HA6_6hGLaAqRi0J3OU,1627
|
|
84
84
|
tests/e2e/test_hello_world_python_windows.py,sha256=kJQZIuzj8OEEtgBVxnzFvyQJ_wvP2oBu9L-E9geg8sE,2010
|
|
@@ -91,8 +91,8 @@ tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
|
|
|
91
91
|
tui/state_handlers.py,sha256=4r_6m5YMcprQtgiTP0De3XFfGAF8VzgaWXHxPweYPbQ,16325
|
|
92
92
|
tui/styles.css,sha256=c13WY1nOgmtkT_FH9Ad2PjXrfi8LYwr6zzD6rBOh9tU,6685
|
|
93
93
|
tui/widget_helpers.py,sha256=mWDPrALsxLOzJg8KD9MXXafJKRiqG-EIp797jbOT77o,5972
|
|
94
|
-
codeplain-0.3.
|
|
95
|
-
codeplain-0.3.
|
|
96
|
-
codeplain-0.3.
|
|
97
|
-
codeplain-0.3.
|
|
98
|
-
codeplain-0.3.
|
|
94
|
+
codeplain-0.3.5.dist-info/METADATA,sha256=Jr8-9-HWWeLthEkBUPqOklrg_gvyrjehDlD_GZUWiUw,5380
|
|
95
|
+
codeplain-0.3.5.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
96
|
+
codeplain-0.3.5.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
|
|
97
|
+
codeplain-0.3.5.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
|
|
98
|
+
codeplain-0.3.5.dist-info/RECORD,,
|
file_utils.py
CHANGED
|
@@ -8,8 +8,9 @@ from liquid2.exceptions import UndefinedError
|
|
|
8
8
|
|
|
9
9
|
import plain_spec
|
|
10
10
|
from plain2code_console import console
|
|
11
|
-
from plain2code_exceptions import UnsupportedResourceType
|
|
11
|
+
from plain2code_exceptions import UnsupportedBase64Content, UnsupportedResourceType
|
|
12
12
|
from plain2code_nodes import Plain2CodeIncludeTag, Plain2CodeLoaderMixin
|
|
13
|
+
from plain2code_utils import find_large_base64_blob
|
|
13
14
|
from plain_modules import CODEPLAIN_MEMORY_SUBFOLDER, CODEPLAIN_METADATA_FOLDER
|
|
14
15
|
|
|
15
16
|
BINARY_FILE_EXTENSIONS = [".pyc"]
|
|
@@ -157,7 +158,7 @@ def store_response_files(target_folder, response_files, existing_files):
|
|
|
157
158
|
|
|
158
159
|
os.makedirs(os.path.dirname(full_file_name), exist_ok=True)
|
|
159
160
|
|
|
160
|
-
with open(full_file_name, "w") as f:
|
|
161
|
+
with open(full_file_name, "w", encoding="utf-8") as f:
|
|
161
162
|
f.write(response_files[file_name])
|
|
162
163
|
|
|
163
164
|
if file_name not in existing_files:
|
|
@@ -206,6 +207,16 @@ def load_linked_resources(template_dirs: list[str], resources_list, module_name:
|
|
|
206
207
|
Please ensure that the resource exists in one of these locations, or specify the correct --template-dir if using custom templates.
|
|
207
208
|
""")
|
|
208
209
|
|
|
210
|
+
blob = find_large_base64_blob(content)
|
|
211
|
+
if blob is not None:
|
|
212
|
+
raise UnsupportedBase64Content(
|
|
213
|
+
f"Referenced resource '{file_name}' in module '{module_name}' contains a large "
|
|
214
|
+
f"base64-encoded blob ({len(blob)} characters), such as an embedded image. Inline "
|
|
215
|
+
"base64 data is not supported. Remove the data from the resource. "
|
|
216
|
+
"If the data should be used by the end software, "
|
|
217
|
+
"save the data to a separate file and include the file path in the specification without it being a reference file."
|
|
218
|
+
)
|
|
219
|
+
|
|
209
220
|
linked_resources[file_name] = content
|
|
210
221
|
|
|
211
222
|
return linked_resources
|
plain2code.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import importlib.resources
|
|
2
1
|
import logging
|
|
3
2
|
import logging.config
|
|
4
3
|
import os
|
|
@@ -37,6 +36,7 @@ from plain2code_exceptions import (
|
|
|
37
36
|
PlainSyntaxError,
|
|
38
37
|
RenderCancelledError,
|
|
39
38
|
RenderingCreditBalanceTooLow,
|
|
39
|
+
UnsupportedBase64Content,
|
|
40
40
|
UnsupportedResourceType,
|
|
41
41
|
)
|
|
42
42
|
from plain2code_logger import (
|
|
@@ -54,7 +54,7 @@ from system_config import system_config
|
|
|
54
54
|
from tui.plain2code_tui import Plain2CodeTUI
|
|
55
55
|
from tui.plain_module_render_choice_tui import PlainModuleRenderChoiceTUI
|
|
56
56
|
|
|
57
|
-
DEFAULT_TEMPLATE_DIRS =
|
|
57
|
+
DEFAULT_TEMPLATE_DIRS = "standard_template_library"
|
|
58
58
|
RENDER_THREAD_SHUTDOWN_TIMEOUT = 0.7
|
|
59
59
|
|
|
60
60
|
# Exceptions that represent expected, user-facing error conditions. They are
|
|
@@ -74,6 +74,7 @@ EXPECTED_EXCEPTIONS = (
|
|
|
74
74
|
NetworkConnectionError,
|
|
75
75
|
ModuleDoesNotExistError,
|
|
76
76
|
UnsupportedResourceType,
|
|
77
|
+
UnsupportedBase64Content,
|
|
77
78
|
GitNotInstalledError,
|
|
78
79
|
SystemExit,
|
|
79
80
|
)
|
|
@@ -139,8 +140,8 @@ def setup_logging(
|
|
|
139
140
|
return logging.getLevelName(configured_log_level)
|
|
140
141
|
|
|
141
142
|
|
|
142
|
-
def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI):
|
|
143
|
-
"""Check API connectivity
|
|
143
|
+
def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI) -> Optional[str]:
|
|
144
|
+
"""Check API connectivity, validate API key and client version, and return the user email."""
|
|
144
145
|
response = codeplainAPI.connection_check(system_config.client_version)
|
|
145
146
|
|
|
146
147
|
if not response.get("api_key_valid", False):
|
|
@@ -157,33 +158,58 @@ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI):
|
|
|
157
158
|
"Please update using: uv tool upgrade codeplain"
|
|
158
159
|
)
|
|
159
160
|
|
|
161
|
+
return response.get("user_email")
|
|
160
162
|
|
|
161
|
-
def render(args, run_state: RunState, event_bus: EventBus, default_log_level: str = "INFO"): # noqa: C901
|
|
162
|
-
template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS)
|
|
163
163
|
|
|
164
|
+
def warn_if_acceptance_tests_without_conformance_script(plain_module, args) -> None:
|
|
165
|
+
"""Warn when any loaded module (including required modules) defines acceptance tests
|
|
166
|
+
but no conformance tests script is configured.
|
|
167
|
+
|
|
168
|
+
Acceptance tests are treated as conformance tests, so a conformance tests script is required
|
|
169
|
+
to actually run them. Without it, the acceptance tests cannot be executed.
|
|
170
|
+
"""
|
|
171
|
+
if args.conformance_tests_script:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
module_names_with_acceptance_tests = [
|
|
175
|
+
module.module_name
|
|
176
|
+
for module in plain_module.all_required_modules + [plain_module]
|
|
177
|
+
if plain_spec.has_acceptance_tests(module.plain_source)
|
|
178
|
+
]
|
|
179
|
+
if not module_names_with_acceptance_tests:
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
module_names = ", ".join(module_names_with_acceptance_tests)
|
|
183
|
+
console.warning(
|
|
184
|
+
f"Acceptance tests were found ({module_names}) but no conformance tests script is configured. "
|
|
185
|
+
"Acceptance tests are treated as conformance tests and require a conformance tests script "
|
|
186
|
+
"(--conformance-tests-script or 'conformance_tests_script' in config) to be executed."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def render( # noqa: C901
|
|
191
|
+
plain_module: plain_modules.PlainModule,
|
|
192
|
+
args,
|
|
193
|
+
run_state: RunState,
|
|
194
|
+
event_bus: EventBus,
|
|
195
|
+
default_log_level: str = "INFO",
|
|
196
|
+
):
|
|
164
197
|
# Compute render range from either --render-range or --render-from
|
|
165
198
|
render_range = None
|
|
166
199
|
if args.render_range or args.render_from:
|
|
167
|
-
|
|
168
|
-
_, plain_source, _ = plain_file.plain_file_parser(args.filename, template_dirs)
|
|
169
|
-
render_range = plain_spec.compute_render_range(args, plain_source)
|
|
200
|
+
render_range = plain_spec.compute_render_range(args, plain_module.plain_source)
|
|
170
201
|
|
|
171
202
|
codeplainAPI = codeplain_api.CodeplainAPI(args.api_key, console)
|
|
172
203
|
assert args.api is not None and args.api != "", "API URL is required"
|
|
173
204
|
codeplainAPI.api_url = args.api
|
|
174
205
|
|
|
175
|
-
_check_connection(codeplainAPI)
|
|
206
|
+
run_state.user_email = _check_connection(codeplainAPI)
|
|
176
207
|
|
|
177
208
|
stop_event = threading.Event()
|
|
178
209
|
enter_pause_event = threading.Event()
|
|
179
210
|
signal.signal(signal.SIGTERM, lambda _signum, _frame: stop_event.set())
|
|
180
211
|
|
|
181
|
-
plain_module
|
|
182
|
-
args.filename,
|
|
183
|
-
args.build_folder,
|
|
184
|
-
args.conformance_tests_folder,
|
|
185
|
-
template_dirs,
|
|
186
|
-
)
|
|
212
|
+
warn_if_acceptance_tests_without_conformance_script(plain_module, args)
|
|
187
213
|
|
|
188
214
|
render_choice = None
|
|
189
215
|
if render_range is None:
|
|
@@ -308,28 +334,38 @@ def main(): # noqa: C901
|
|
|
308
334
|
console.error(f"Error fetching status: {str(e)}")
|
|
309
335
|
return
|
|
310
336
|
|
|
311
|
-
|
|
312
|
-
if args.dry_run or args.full_plain:
|
|
313
|
-
template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS)
|
|
337
|
+
template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS)
|
|
314
338
|
|
|
339
|
+
# Handle full plain early-exit (raw text dump; does not require a parsed module).
|
|
340
|
+
if args.full_plain:
|
|
315
341
|
try:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
console.info(full_plain_source)
|
|
322
|
-
return
|
|
323
|
-
|
|
324
|
-
if args.dry_run:
|
|
325
|
-
console.info("Printing dry run output...\n")
|
|
326
|
-
_, plain_source_tree, _ = plain_file.plain_file_parser(args.filename, template_dirs)
|
|
327
|
-
render_range = plain_spec.compute_render_range(args, plain_source_tree)
|
|
328
|
-
print_dry_run_output(plain_source_tree, render_range)
|
|
329
|
-
return
|
|
342
|
+
module_name = Path(args.filename).stem
|
|
343
|
+
plain_source = plain_file.read_module_plain_source(module_name, template_dirs)
|
|
344
|
+
[full_plain_source, _] = file_utils.get_loaded_templates(template_dirs, plain_source)
|
|
345
|
+
console.info("Full plain text:\n")
|
|
346
|
+
console.info(full_plain_source)
|
|
330
347
|
except Exception as e:
|
|
331
348
|
console.error(f"Error: {str(e)}")
|
|
332
|
-
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
# Parse the plain file (and its required modules) once; reused by dry-run and rendering.
|
|
352
|
+
try:
|
|
353
|
+
plain_module = plain_modules.PlainModule(
|
|
354
|
+
args.filename,
|
|
355
|
+
args.build_folder,
|
|
356
|
+
args.conformance_tests_folder,
|
|
357
|
+
template_dirs,
|
|
358
|
+
)
|
|
359
|
+
except Exception as e:
|
|
360
|
+
console.error(f"Error: {str(e)}")
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
if args.dry_run:
|
|
364
|
+
console.info("Printing dry run output...\n")
|
|
365
|
+
render_range = plain_spec.compute_render_range(args, plain_module.plain_source)
|
|
366
|
+
print_dry_run_output(plain_module.plain_source, render_range)
|
|
367
|
+
warn_if_acceptance_tests_without_conformance_script(plain_module, args)
|
|
368
|
+
return
|
|
333
369
|
|
|
334
370
|
event_bus = EventBus()
|
|
335
371
|
|
|
@@ -357,7 +393,7 @@ def main(): # noqa: C901
|
|
|
357
393
|
raise MissingAPIKey(
|
|
358
394
|
"Your API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument.\n"
|
|
359
395
|
)
|
|
360
|
-
render(args, run_state, event_bus, default_log_level)
|
|
396
|
+
render(plain_module, args, run_state, event_bus, default_log_level)
|
|
361
397
|
except BaseException as e:
|
|
362
398
|
if isinstance(e, KeyboardInterrupt):
|
|
363
399
|
error_message = "Keyboard interrupt"
|
plain2code_exceptions.py
CHANGED
plain2code_state.py
CHANGED
plain2code_telemetry.py
CHANGED
|
@@ -98,6 +98,8 @@ def capture_crash(exc_info, run_state: Optional[RunState], args) -> bool:
|
|
|
98
98
|
scope.set_tag("render_state", run_state.current_render_state)
|
|
99
99
|
scope.set_tag("current_module", run_state.current_module)
|
|
100
100
|
scope.set_tag("current_frid", run_state.current_frid)
|
|
101
|
+
if run_state.user_email:
|
|
102
|
+
scope.set_user({"email": run_state.user_email})
|
|
101
103
|
scope.set_tag("headless", bool(getattr(args, "headless", False)))
|
|
102
104
|
scope.set_tag("unittests_script_provided", bool(getattr(args, "unittests_script", None)))
|
|
103
105
|
scope.set_tag("conformance_tests_script_provided", bool(getattr(args, "conformance_tests_script", None)))
|
plain2code_utils.py
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
MAX_BASE64_BLOB_LENGTH = 8192
|
|
6
|
+
|
|
7
|
+
# Matches a long contiguous base64 / base64url run, optionally preceded by a data: URI header.
|
|
8
|
+
_BASE64_BLOB_PATTERN = re.compile(
|
|
9
|
+
r"(?:data:[\w.+-]+/[\w.+-]+;base64,)?[A-Za-z0-9+/_-]{%d,}={0,2}" % MAX_BASE64_BLOB_LENGTH
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_large_base64_blob(text: str) -> Optional[str]:
|
|
14
|
+
"""Return the first contiguous base64 blob at or above the threshold, or None."""
|
|
15
|
+
match = _BASE64_BLOB_PATTERN.search(text)
|
|
16
|
+
return match.group(0) if match else None
|
|
17
|
+
|
|
18
|
+
|
|
1
19
|
def format_duration_hms(total_seconds: int) -> str:
|
|
2
20
|
"""Format a duration in seconds as hours, minutes, and seconds (e.g. ``1h 2m 3.45s``, ``45.67s``)."""
|
|
3
21
|
if total_seconds < 0:
|
plain_file.py
CHANGED
|
@@ -17,8 +17,9 @@ from mistletoe.utils import traverse
|
|
|
17
17
|
import concept_utils
|
|
18
18
|
import file_utils
|
|
19
19
|
import plain_spec
|
|
20
|
-
from plain2code_exceptions import ModuleDoesNotExistError, PlainSyntaxError
|
|
20
|
+
from plain2code_exceptions import ModuleDoesNotExistError, PlainSyntaxError, UnsupportedBase64Content
|
|
21
21
|
from plain2code_nodes import Plain2CodeIncludeTag, Plain2CodeLoaderMixin
|
|
22
|
+
from plain2code_utils import find_large_base64_blob
|
|
22
23
|
|
|
23
24
|
RESOURCE_MARKER = "[resource]"
|
|
24
25
|
|
|
@@ -203,6 +204,26 @@ def _is_acceptance_test_heading(token) -> tuple[bool, str | None]:
|
|
|
203
204
|
return False, None
|
|
204
205
|
|
|
205
206
|
|
|
207
|
+
def _find_acceptance_test_heading(token):
|
|
208
|
+
"""
|
|
209
|
+
Recursively search a token tree for an `acceptance tests` heading.
|
|
210
|
+
|
|
211
|
+
Returns the heading token if one is found anywhere in the subtree, otherwise None.
|
|
212
|
+
Used to detect acceptance tests nested under sections other than functional specs.
|
|
213
|
+
"""
|
|
214
|
+
is_acceptance_test_heading, _ = _is_acceptance_test_heading(token)
|
|
215
|
+
if is_acceptance_test_heading:
|
|
216
|
+
return token
|
|
217
|
+
|
|
218
|
+
if getattr(token, "children", None):
|
|
219
|
+
for child in token.children:
|
|
220
|
+
found = _find_acceptance_test_heading(child)
|
|
221
|
+
if found is not None:
|
|
222
|
+
return found
|
|
223
|
+
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
206
227
|
def _process_single_acceptance_test_requirement(functional_requirement: mistletoe.block_token.ListItem):
|
|
207
228
|
"""
|
|
208
229
|
Process a single functionality to extract acceptance tests.
|
|
@@ -473,6 +494,11 @@ def parse_plain_source( # noqa: C901
|
|
|
473
494
|
|
|
474
495
|
specification_heading = token.children[0].children[0].children[0].content
|
|
475
496
|
|
|
497
|
+
if specification_heading == plain_spec.ACCEPTANCE_TEST_HEADING:
|
|
498
|
+
raise PlainSyntaxError(
|
|
499
|
+
f"Plain syntax error: Syntax error at line {token.line_number}: {plain_spec.ACCEPTANCE_TEST_HEADING} heading should be nested under specific functional spec."
|
|
500
|
+
)
|
|
501
|
+
|
|
476
502
|
if specification_heading not in plain_spec.ALLOWED_SPECIFICATION_HEADINGS:
|
|
477
503
|
raise PlainSyntaxError(
|
|
478
504
|
f"Plain syntax error: Syntax error at line {token.line_number}: Invalid specification heading (`{specification_heading}`). Allowed headings: {', '.join(plain_spec.ALLOWED_SPECIFICATION_HEADINGS)}"
|
|
@@ -511,6 +537,22 @@ def parse_plain_source( # noqa: C901
|
|
|
511
537
|
f"Plain syntax error: Syntax error at line {token.line_number}: Invalid source structure (`{token_text}`)"
|
|
512
538
|
)
|
|
513
539
|
|
|
540
|
+
# Acceptance tests are only allowed nested under functional specs. Reject them when
|
|
541
|
+
# they are nested under any other section (e.g. definitions, implementation reqs, test reqs).
|
|
542
|
+
for non_functional_heading in (
|
|
543
|
+
plain_spec.DEFINITIONS,
|
|
544
|
+
plain_spec.NON_FUNCTIONAL_REQUIREMENTS,
|
|
545
|
+
plain_spec.TEST_REQUIREMENTS,
|
|
546
|
+
):
|
|
547
|
+
section = plain_source.get(non_functional_heading)
|
|
548
|
+
if section is None:
|
|
549
|
+
continue
|
|
550
|
+
nested_acceptance_test_heading = _find_acceptance_test_heading(section)
|
|
551
|
+
if nested_acceptance_test_heading is not None:
|
|
552
|
+
raise PlainSyntaxError(
|
|
553
|
+
f"Plain syntax error: Syntax error at line {nested_acceptance_test_heading.line_number}: {plain_spec.ACCEPTANCE_TEST_HEADING} heading should be nested under specific functional spec."
|
|
554
|
+
)
|
|
555
|
+
|
|
514
556
|
if plain_source[plain_spec.DEFINITIONS] is not None:
|
|
515
557
|
with PlainRenderer() as renderer:
|
|
516
558
|
for token in plain_source[plain_spec.DEFINITIONS].children:
|
|
@@ -536,6 +578,16 @@ def read_module_plain_source(module_name: str, template_dirs: list[str]) -> str:
|
|
|
536
578
|
plain_source_text = file_utils.open_from(template_dirs, module_name + PLAIN_SOURCE_FILE_EXTENSION)
|
|
537
579
|
if plain_source_text is None:
|
|
538
580
|
raise ModuleDoesNotExistError(f"Module does not exist ({module_name}).")
|
|
581
|
+
|
|
582
|
+
blob = find_large_base64_blob(plain_source_text)
|
|
583
|
+
if blob is not None:
|
|
584
|
+
raise UnsupportedBase64Content(
|
|
585
|
+
f"Module '{module_name}' contains a base64-encoded blob ({len(blob)} characters) "
|
|
586
|
+
"inlined in the specification. This is not supported."
|
|
587
|
+
"Remove the base64 data from the .plain file or if necessary,"
|
|
588
|
+
"include the binary file path in the specification."
|
|
589
|
+
)
|
|
590
|
+
|
|
539
591
|
return plain_source_text
|
|
540
592
|
|
|
541
593
|
|
plain_spec.py
CHANGED
|
@@ -25,6 +25,15 @@ ALLOWED_SPECIFICATION_HEADINGS = [
|
|
|
25
25
|
ALLOWED_IMPORT_SPECIFICATION_HEADINGS = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS]
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def has_acceptance_tests(plain_source_tree) -> bool:
|
|
29
|
+
"""Return True if any functional requirement in the plain source defines acceptance tests."""
|
|
30
|
+
functional_requirements = plain_source_tree.get(FUNCTIONAL_REQUIREMENTS) or []
|
|
31
|
+
for functional_requirement in functional_requirements:
|
|
32
|
+
if functional_requirement.get(ACCEPTANCE_TESTS):
|
|
33
|
+
return True
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
28
37
|
def collect_specification_linked_resources(specification, specification_heading, linked_resources_list):
|
|
29
38
|
linked_resources = []
|
|
30
39
|
if "linked_resources" in specification:
|
|
@@ -9,12 +9,17 @@ from plain2code_exceptions import InternalClientError
|
|
|
9
9
|
from render_machine.actions.base_action import BaseAction
|
|
10
10
|
from render_machine.implementation_code_helpers import ImplementationCodeHelpers
|
|
11
11
|
from render_machine.render_context import RenderContext
|
|
12
|
-
from render_machine.render_types import TestExecutionPhase
|
|
12
|
+
from render_machine.render_types import RenderError, TestExecutionPhase
|
|
13
|
+
|
|
14
|
+
MAX_CONFORMANCE_TEST_FIX_ATTEMPTS = 20
|
|
15
|
+
MAX_CONFORMANCE_TEST_RERENDER_ATTEMPTS = 1
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
class FixConformanceTest(BaseAction):
|
|
16
19
|
IMPLEMENTATION_CODE_NOT_UPDATED = "implementation_code_not_updated"
|
|
17
20
|
IMPLEMENTATION_CODE_UPDATED = "implementation_code_updated"
|
|
21
|
+
LIMIT_EXCEEDED_OUTCOME = "conformance_test_fix_limit_exceeded"
|
|
22
|
+
REGENERATE_CONFORMANCE_TESTS_OUTCOME = "regenerate_conformance_tests"
|
|
18
23
|
|
|
19
24
|
ISSUE_REASON_CODE_CONFORMANCE_TESTS = 0
|
|
20
25
|
ISSUE_REASON_CODE_IMPLEMENTATION_CODE = 1
|
|
@@ -22,6 +27,23 @@ class FixConformanceTest(BaseAction):
|
|
|
22
27
|
ISSUE_REASON_CODE_CONFLICTING_ACCEPTANCE_TESTS = 3
|
|
23
28
|
|
|
24
29
|
def execute(self, render_context: RenderContext, previous_action_payload: Any | None):
|
|
30
|
+
ctx = render_context.conformance_tests_running_context
|
|
31
|
+
ctx.fix_attempts += 1
|
|
32
|
+
|
|
33
|
+
if ctx.fix_attempts >= MAX_CONFORMANCE_TEST_FIX_ATTEMPTS:
|
|
34
|
+
if ctx.conformance_tests_render_attempts >= MAX_CONFORMANCE_TEST_RERENDER_ATTEMPTS:
|
|
35
|
+
error_msg = f"The renderer was unable to produce an implementation that passes conformance tests for functionality '{render_context.frid_context.frid}' after many attempts. Please review and rewrite the specification. (Render ID: {render_context.run_state.render_id})"
|
|
36
|
+
render_context.last_error_message = error_msg
|
|
37
|
+
return (
|
|
38
|
+
self.LIMIT_EXCEEDED_OUTCOME,
|
|
39
|
+
RenderError.encode(message=error_msg).to_payload(),
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
ctx.regenerating_conformance_tests = True
|
|
43
|
+
return self.REGENERATE_CONFORMANCE_TESTS_OUTCOME, None
|
|
44
|
+
|
|
45
|
+
console.info(f"Running conformance tests attempt {ctx.fix_attempts + 1}.")
|
|
46
|
+
|
|
25
47
|
console.info(
|
|
26
48
|
f"Fixing conformance test for functionality {render_context.conformance_tests_running_context.current_testing_frid} in module {render_context.conformance_tests_running_context.current_testing_module_name}."
|
|
27
49
|
)
|
|
@@ -12,6 +12,12 @@ class PrepareTestingEnvironment(BaseAction):
|
|
|
12
12
|
FAILED_OUTCOME = "testing_environment_preparation_failed"
|
|
13
13
|
|
|
14
14
|
def execute(self, render_context: RenderContext, _previous_action_payload: Any | None):
|
|
15
|
+
if (
|
|
16
|
+
render_context.prepare_environment_script is None
|
|
17
|
+
or not render_context.conformance_tests_running_context.should_prepare_testing_environment
|
|
18
|
+
):
|
|
19
|
+
return self.SUCCESSFUL_OUTCOME, None
|
|
20
|
+
|
|
15
21
|
console.info(
|
|
16
22
|
f"Running testing environment preparation script {render_context.prepare_environment_script} for build folder {render_context.build_folder}."
|
|
17
23
|
)
|
|
@@ -5,13 +5,31 @@ from plain2code_console import console
|
|
|
5
5
|
from render_machine.actions.base_action import BaseAction
|
|
6
6
|
from render_machine.implementation_code_helpers import ImplementationCodeHelpers
|
|
7
7
|
from render_machine.render_context import RenderContext
|
|
8
|
+
from render_machine.render_types import RenderError
|
|
9
|
+
|
|
10
|
+
MAX_REFACTORING_ITERATIONS = 5
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
class RefactorCode(BaseAction):
|
|
11
14
|
SUCCESSFUL_OUTCOME = "refactoring_successful"
|
|
12
15
|
NO_FILES_REFACTORED_OUTCOME = "no_files_refactored"
|
|
16
|
+
ITERATION_LIMIT_EXCEEDED_OUTCOME = "refactoring_iteration_limit_exceeded"
|
|
13
17
|
|
|
14
18
|
def execute(self, render_context: RenderContext, _previous_action_payload: Any | None):
|
|
19
|
+
if render_context.frid_context.refactoring_iteration == 0:
|
|
20
|
+
console.info("Refactoring the generated code...")
|
|
21
|
+
|
|
22
|
+
render_context.frid_context.refactoring_iteration += 1
|
|
23
|
+
|
|
24
|
+
if render_context.frid_context.refactoring_iteration >= MAX_REFACTORING_ITERATIONS:
|
|
25
|
+
error_message = "Refactoring iterations limit of {MAX_REFACTORING_ITERATIONS} reached for functionality {render_context.frid_context.frid}."
|
|
26
|
+
render_context.last_error_message = error_message
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
self.ITERATION_LIMIT_EXCEEDED_OUTCOME,
|
|
30
|
+
RenderError.encode(message=error_message).to_payload(),
|
|
31
|
+
)
|
|
32
|
+
|
|
15
33
|
existing_files, existing_files_content = ImplementationCodeHelpers.fetch_existing_files(
|
|
16
34
|
render_context.build_folder
|
|
17
35
|
)
|
|
@@ -16,8 +16,22 @@ MAX_CODE_GENERATION_RETRIES = 2
|
|
|
16
16
|
class RenderFunctionalRequirement(BaseAction):
|
|
17
17
|
SUCCESSFUL_OUTCOME = "code_and_unit_tests_generated"
|
|
18
18
|
FUNCTIONAL_REQUIREMENT_TOO_COMPLEX_OUTCOME = "functional_requirement_too_complex"
|
|
19
|
+
ITERATION_LIMIT_EXCEEDED_OUTCOME = "frid_iteration_limit_exceeded"
|
|
19
20
|
|
|
20
21
|
def execute(self, render_context: RenderContext, _previous_action_payload: Any | None):
|
|
22
|
+
if render_context.frid_context.functional_requirement_render_attempts >= MAX_CODE_GENERATION_RETRIES:
|
|
23
|
+
error_msg = f"Unittests could not be fixed after rendering the functionality {render_context.frid_context.frid} for the {MAX_CODE_GENERATION_RETRIES} times."
|
|
24
|
+
render_context.last_error_message = error_msg
|
|
25
|
+
return self.ITERATION_LIMIT_EXCEEDED_OUTCOME, None
|
|
26
|
+
|
|
27
|
+
render_context.frid_context.functional_requirement_render_attempts += 1
|
|
28
|
+
|
|
29
|
+
if render_context.frid_context.functional_requirement_render_attempts > 1:
|
|
30
|
+
console.info(
|
|
31
|
+
f"Unittests could not be fixed after rendering the functionality. "
|
|
32
|
+
f"Restarting rendering functionality {render_context.frid_context.frid} from scratch."
|
|
33
|
+
)
|
|
34
|
+
|
|
21
35
|
render_utils.revert_changes_for_frid(render_context)
|
|
22
36
|
existing_files, existing_files_content = ImplementationCodeHelpers.fetch_existing_files(
|
|
23
37
|
render_context.build_folder
|
|
@@ -48,7 +62,7 @@ class RenderFunctionalRequirement(BaseAction):
|
|
|
48
62
|
render_context.run_state,
|
|
49
63
|
)
|
|
50
64
|
except FunctionalRequirementTooComplex as e:
|
|
51
|
-
error_message = f"The functionality:\n{render_context.frid_context.functional_requirement_text}\n is too complex to be implemented. Please break down the functionality into smaller parts
|
|
65
|
+
error_message = f"The functionality:\n{render_context.frid_context.functional_requirement_text}\n is too complex to be implemented. Please break down the functionality into smaller parts."
|
|
52
66
|
if e.proposed_breakdown:
|
|
53
67
|
error_message += "\nProposed breakdown:"
|
|
54
68
|
for _, part in e.proposed_breakdown["functional_requirements"].items():
|
render_machine/render_context.py
CHANGED
|
@@ -23,10 +23,6 @@ from render_machine.render_types import (
|
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
MAX_UNITTEST_FIX_ATTEMPTS = 20
|
|
26
|
-
MAX_CODE_GENERATION_RETRIES = 2
|
|
27
|
-
MAX_CONFORMANCE_TEST_RERENDER_ATTEMPTS = 1
|
|
28
|
-
MAX_REFACTORING_ITERATIONS = 5
|
|
29
|
-
MAX_CONFORMANCE_TEST_FIX_ATTEMPTS = 20
|
|
30
26
|
MAX_FUNCTIONAL_REQUIREMENT_RENDER_ATTEMPTS_FAILED_UNIT_DURING_CONFORMANCE_TESTS = 2
|
|
31
27
|
|
|
32
28
|
|
|
@@ -165,20 +161,6 @@ class RenderContext:
|
|
|
165
161
|
self.run_state.current_frid = frid
|
|
166
162
|
return
|
|
167
163
|
|
|
168
|
-
def check_frid_iteration_limit(self):
|
|
169
|
-
if self.frid_context.functional_requirement_render_attempts >= MAX_CODE_GENERATION_RETRIES:
|
|
170
|
-
error_msg = f"Unittests could not be fixed after rendering the functionality {self.frid_context.frid} for the {MAX_CODE_GENERATION_RETRIES} times."
|
|
171
|
-
self.dispatch_error(error_msg)
|
|
172
|
-
|
|
173
|
-
self.frid_context.functional_requirement_render_attempts += 1
|
|
174
|
-
|
|
175
|
-
if self.frid_context.functional_requirement_render_attempts > 1:
|
|
176
|
-
# this if is intended just for logging
|
|
177
|
-
console.info(
|
|
178
|
-
f"Unittests could not be fixed after rendering the functionality. "
|
|
179
|
-
f"Restarting rendering the functionality {self.frid_context.frid} from scratch."
|
|
180
|
-
)
|
|
181
|
-
|
|
182
164
|
def has_next_frid(self) -> bool:
|
|
183
165
|
next_frid = plain_spec.get_next_frid(self.plain_source_tree, self.frid_context.frid)
|
|
184
166
|
if self.render_range is None or len(self.render_range) == 0:
|
|
@@ -310,26 +292,6 @@ class RenderContext:
|
|
|
310
292
|
git_utils.revert_changes(self.build_folder)
|
|
311
293
|
self.machine.dispatch(triggers.START_NEW_REFACTORING_ITERATION)
|
|
312
294
|
|
|
313
|
-
def start_refactoring_code(self):
|
|
314
|
-
|
|
315
|
-
if self.frid_context.refactoring_iteration == 0:
|
|
316
|
-
console.info("Refactoring the generated code...")
|
|
317
|
-
|
|
318
|
-
self.frid_context.refactoring_iteration += 1
|
|
319
|
-
|
|
320
|
-
if self.frid_context.refactoring_iteration >= MAX_REFACTORING_ITERATIONS:
|
|
321
|
-
console.info(
|
|
322
|
-
f"Refactoring iterations limit of {MAX_REFACTORING_ITERATIONS} reached for functionality {self.frid_context.frid}."
|
|
323
|
-
)
|
|
324
|
-
self.machine.dispatch(triggers.PROCEED_FRID_PROCESSING)
|
|
325
|
-
|
|
326
|
-
def start_testing_environment_preparation(self):
|
|
327
|
-
if (
|
|
328
|
-
self.prepare_environment_script is None
|
|
329
|
-
or not self.conformance_tests_running_context.should_prepare_testing_environment
|
|
330
|
-
):
|
|
331
|
-
self.machine.dispatch(triggers.MARK_TESTING_ENVIRONMENT_PREPARED)
|
|
332
|
-
|
|
333
295
|
def start_conformance_tests_processing(self):
|
|
334
296
|
console.info("Implementing conformance tests...")
|
|
335
297
|
current_frid_specifications, _ = plain_spec.get_specifications_for_frid(
|
|
@@ -589,23 +551,6 @@ class RenderContext:
|
|
|
589
551
|
# Should never reach here
|
|
590
552
|
raise RuntimeError(f"Unexpected execution phase: {ctx.execution_phase}")
|
|
591
553
|
|
|
592
|
-
def start_fixing_conformance_tests(self):
|
|
593
|
-
self.conformance_tests_running_context.fix_attempts += 1
|
|
594
|
-
|
|
595
|
-
if self.conformance_tests_running_context.fix_attempts >= MAX_CONFORMANCE_TEST_FIX_ATTEMPTS:
|
|
596
|
-
if (
|
|
597
|
-
self.conformance_tests_running_context.conformance_tests_render_attempts
|
|
598
|
-
>= MAX_CONFORMANCE_TEST_RERENDER_ATTEMPTS
|
|
599
|
-
):
|
|
600
|
-
error_msg = f"The renderer was unable to produce an implementation that passes conformance tests for functionality '{self.frid_context.frid}' after many attempts. Please review and rewrite the specification. (Render ID: {self.run_state.render_id})"
|
|
601
|
-
self.dispatch_error(error_msg)
|
|
602
|
-
else:
|
|
603
|
-
self.conformance_tests_running_context.regenerating_conformance_tests = True
|
|
604
|
-
self.machine.dispatch(triggers.MARK_REGENERATION_OF_CONFORMANCE_TESTS)
|
|
605
|
-
|
|
606
|
-
def finish_fixing_conformance_tests(self):
|
|
607
|
-
console.info(f"Running conformance tests attempt {self.conformance_tests_running_context.fix_attempts + 1}.")
|
|
608
|
-
|
|
609
554
|
def start_render_completed(self):
|
|
610
555
|
self.run_state.set_render_succeeded(True)
|
|
611
556
|
|
|
@@ -73,12 +73,14 @@ class StateMachineConfig:
|
|
|
73
73
|
PrepareRepositories.SUCCESSFUL_OUTCOME: triggers.START_RENDER,
|
|
74
74
|
RenderFunctionalRequirement.SUCCESSFUL_OUTCOME: triggers.RENDER_FUNCTIONAL_REQUIREMENT,
|
|
75
75
|
RenderFunctionalRequirement.FUNCTIONAL_REQUIREMENT_TOO_COMPLEX_OUTCOME: triggers.HANDLE_ERROR,
|
|
76
|
+
RenderFunctionalRequirement.ITERATION_LIMIT_EXCEEDED_OUTCOME: triggers.HANDLE_ERROR,
|
|
76
77
|
RunUnitTests.SUCCESSFUL_OUTCOME: triggers.MARK_UNIT_TESTS_PASSED,
|
|
77
78
|
RunUnitTests.FAILED_OUTCOME: triggers.MARK_UNIT_TESTS_FAILED,
|
|
78
79
|
RunUnitTests.UNRECOVERABLE_ERROR_OUTCOME: triggers.HANDLE_ERROR,
|
|
79
80
|
FixUnitTests.SUCCESSFUL_OUTCOME: triggers.MARK_UNIT_TESTS_READY,
|
|
80
81
|
RefactorCode.SUCCESSFUL_OUTCOME: triggers.REFACTOR_CODE,
|
|
81
82
|
RefactorCode.NO_FILES_REFACTORED_OUTCOME: triggers.PROCEED_FRID_PROCESSING,
|
|
83
|
+
RefactorCode.ITERATION_LIMIT_EXCEEDED_OUTCOME: triggers.PROCEED_FRID_PROCESSING,
|
|
82
84
|
CommitImplementationCodeChanges.SUCCESSFUL_OUTCOME: triggers.PROCEED_FRID_PROCESSING,
|
|
83
85
|
FinishFunctionalRequirement.SUCCESSFUL_OUTCOME: triggers.PROCEED_FRID_PROCESSING,
|
|
84
86
|
CreateDist.SUCCESSFUL_OUTCOME: triggers.FINISH_RENDER,
|
|
@@ -90,6 +92,8 @@ class StateMachineConfig:
|
|
|
90
92
|
RunConformanceTests.UNRECOVERABLE_ERROR_OUTCOME: triggers.HANDLE_ERROR,
|
|
91
93
|
FixConformanceTest.IMPLEMENTATION_CODE_NOT_UPDATED: triggers.MARK_CONFORMANCE_TESTS_READY,
|
|
92
94
|
FixConformanceTest.IMPLEMENTATION_CODE_UPDATED: triggers.MARK_UNIT_TESTS_READY,
|
|
95
|
+
FixConformanceTest.LIMIT_EXCEEDED_OUTCOME: triggers.HANDLE_ERROR,
|
|
96
|
+
FixConformanceTest.REGENERATE_CONFORMANCE_TESTS_OUTCOME: triggers.MARK_REGENERATION_OF_CONFORMANCE_TESTS,
|
|
93
97
|
CommitConformanceTestsChanges.SUCCESSFUL_OUTCOME_IMPLEMENTATION_UPDATED: triggers.MARK_NEXT_CONFORMANCE_TESTS_POSTPROCESSING_STEP,
|
|
94
98
|
CommitConformanceTestsChanges.SUCCESSFUL_OUTCOME_IMPLEMENTATION_NOT_UPDATED: triggers.PROCEED_FRID_PROCESSING,
|
|
95
99
|
SummarizeConformanceTests.SUCCESSFUL_OUTCOME: triggers.MARK_NEXT_CONFORMANCE_TESTS_POSTPROCESSING_STEP,
|
|
@@ -135,16 +139,9 @@ class StateMachineConfig:
|
|
|
135
139
|
"name": States.CONFORMANCE_TESTING_INITIALISED.value,
|
|
136
140
|
"on_enter": render_context.start_conformance_tests_for_frid,
|
|
137
141
|
},
|
|
138
|
-
|
|
139
|
-
"name": States.CONFORMANCE_TEST_GENERATED.value,
|
|
140
|
-
"on_enter": render_context.start_testing_environment_preparation,
|
|
141
|
-
},
|
|
142
|
+
States.CONFORMANCE_TEST_GENERATED.value,
|
|
142
143
|
States.CONFORMANCE_TEST_ENV_PREPARED.value,
|
|
143
|
-
|
|
144
|
-
"name": States.CONFORMANCE_TEST_FAILED.value,
|
|
145
|
-
"on_enter": render_context.start_fixing_conformance_tests,
|
|
146
|
-
"on_exit": render_context.finish_fixing_conformance_tests,
|
|
147
|
-
},
|
|
144
|
+
States.CONFORMANCE_TEST_FAILED.value,
|
|
148
145
|
self.get_processing_unit_tests_states(
|
|
149
146
|
render_context, render_context._on_unit_test_limit_exceeded_in_conformance_tests
|
|
150
147
|
),
|
|
@@ -164,7 +161,6 @@ class StateMachineConfig:
|
|
|
164
161
|
refactoring_code_states = {
|
|
165
162
|
"name": States.REFACTORING_CODE.value,
|
|
166
163
|
"initial": States.READY_FOR_REFACTORING.value,
|
|
167
|
-
"on_enter": render_context.start_refactoring_code,
|
|
168
164
|
"children": [
|
|
169
165
|
States.READY_FOR_REFACTORING.value,
|
|
170
166
|
self.get_processing_unit_tests_states(
|
|
@@ -183,10 +179,7 @@ class StateMachineConfig:
|
|
|
183
179
|
"on_exit": render_context.finish_implementing_frid,
|
|
184
180
|
"children": [
|
|
185
181
|
{"name": States.STEP_COMPLETED.value},
|
|
186
|
-
|
|
187
|
-
"name": States.READY_FOR_FRID_IMPLEMENTATION.value,
|
|
188
|
-
"on_enter": render_context.check_frid_iteration_limit,
|
|
189
|
-
},
|
|
182
|
+
States.READY_FOR_FRID_IMPLEMENTATION.value,
|
|
190
183
|
self.get_processing_unit_tests_states(
|
|
191
184
|
render_context, render_context._on_unit_test_limit_exceeded_in_implementation
|
|
192
185
|
),
|
tests/test_file_utils.py
CHANGED
|
@@ -3,8 +3,9 @@ import tempfile
|
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
|
-
from file_utils import load_linked_resources
|
|
7
|
-
from plain2code_exceptions import UnsupportedResourceType
|
|
6
|
+
from file_utils import load_linked_resources, store_response_files
|
|
7
|
+
from plain2code_exceptions import UnsupportedBase64Content, UnsupportedResourceType
|
|
8
|
+
from plain2code_utils import MAX_BASE64_BLOB_LENGTH
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@pytest.fixture
|
|
@@ -39,3 +40,53 @@ def test_load_linked_resources_binary_file_raises_unsupported_resource_type(temp
|
|
|
39
40
|
def test_load_linked_resources_missing_file_raises_file_not_found(template_dir):
|
|
40
41
|
with pytest.raises(FileNotFoundError):
|
|
41
42
|
load_linked_resources([template_dir], [{"text": "Missing", "target": "missing.md"}], "my_thing")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_load_linked_resources_base64_blob_raises(template_dir):
|
|
46
|
+
file_path = os.path.join(template_dir, "request.txt")
|
|
47
|
+
with open(file_path, "w") as f:
|
|
48
|
+
f.write("curl -d 'selfie_image=" + "A" * (MAX_BASE64_BLOB_LENGTH + 100) + "'")
|
|
49
|
+
|
|
50
|
+
with pytest.raises(UnsupportedBase64Content) as exc_info:
|
|
51
|
+
load_linked_resources([template_dir], [{"text": "Request", "target": "request.txt"}], "my_thing")
|
|
52
|
+
|
|
53
|
+
assert "request.txt" in str(exc_info.value)
|
|
54
|
+
assert "my_thing" in str(exc_info.value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_load_linked_resources_small_base64_allowed(template_dir):
|
|
58
|
+
file_path = os.path.join(template_dir, "token.txt")
|
|
59
|
+
with open(file_path, "w") as f:
|
|
60
|
+
f.write("token=" + "A" * (MAX_BASE64_BLOB_LENGTH - 100))
|
|
61
|
+
|
|
62
|
+
result = load_linked_resources([template_dir], [{"text": "Token", "target": "token.txt"}], "my_thing")
|
|
63
|
+
|
|
64
|
+
assert "token.txt" in result
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_load_linked_resources_real_base64_image_raises(template_dir):
|
|
68
|
+
# Real base64-encoded JPEG that made Gemini 3 fail, inlined into a curl example resource.
|
|
69
|
+
sample_path = os.path.join(os.path.dirname(__file__), "data", "sample_base64_image.txt")
|
|
70
|
+
with open(sample_path) as f:
|
|
71
|
+
blob = f.read()
|
|
72
|
+
|
|
73
|
+
file_path = os.path.join(template_dir, "face_match_request.txt")
|
|
74
|
+
with open(file_path, "w") as f:
|
|
75
|
+
f.write(f"curl -d 'selfie_image={blob}' https://example.com/face-match")
|
|
76
|
+
|
|
77
|
+
with pytest.raises(UnsupportedBase64Content) as exc_info:
|
|
78
|
+
load_linked_resources([template_dir], [{"text": "Request", "target": "face_match_request.txt"}], "face_match")
|
|
79
|
+
|
|
80
|
+
assert "face_match_request.txt" in str(exc_info.value)
|
|
81
|
+
assert str(len(blob)) in str(exc_info.value)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_store_response_files_writes_unicode_as_utf8(template_dir):
|
|
85
|
+
# Content with a non-cp1252 character (📍 U+1F4CD) must be written as UTF-8
|
|
86
|
+
# regardless of the platform's default text encoding (e.g. cp1252 on Windows).
|
|
87
|
+
content = "Location 📍 marker"
|
|
88
|
+
store_response_files(template_dir, {"notes.md": content}, [])
|
|
89
|
+
|
|
90
|
+
file_path = os.path.join(template_dir, "notes.md")
|
|
91
|
+
with open(file_path, "rb") as f:
|
|
92
|
+
assert f.read().decode("utf-8") == content
|
tests/test_plain2code.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
from argparse import Namespace
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import plain2code
|
|
7
|
+
import plain_spec
|
|
8
|
+
from plain_modules import PlainModule
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _make_module(module_name, has_acceptance_tests, required_modules=None):
|
|
12
|
+
"""Build a minimal stand-in for a PlainModule.
|
|
13
|
+
|
|
14
|
+
The functional requirements only need an `acceptance_tests` key when the module
|
|
15
|
+
is expected to contain acceptance tests, because that is all
|
|
16
|
+
`plain_spec.has_acceptance_tests` inspects.
|
|
17
|
+
"""
|
|
18
|
+
functional_requirement = {"markdown": f"- {module_name} functionality."}
|
|
19
|
+
if has_acceptance_tests:
|
|
20
|
+
functional_requirement[plain_spec.ACCEPTANCE_TESTS] = [{"markdown": "- Test it."}]
|
|
21
|
+
|
|
22
|
+
return SimpleNamespace(
|
|
23
|
+
module_name=module_name,
|
|
24
|
+
plain_source={plain_spec.FUNCTIONAL_REQUIREMENTS: [functional_requirement]},
|
|
25
|
+
all_required_modules=required_modules or [],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_warns_when_acceptance_tests_present_and_no_conformance_script():
|
|
30
|
+
plain_module = _make_module("top", has_acceptance_tests=True)
|
|
31
|
+
args = Namespace(conformance_tests_script=None)
|
|
32
|
+
|
|
33
|
+
with patch("plain2code.console") as mock_console:
|
|
34
|
+
plain2code.warn_if_acceptance_tests_without_conformance_script(plain_module, args)
|
|
35
|
+
|
|
36
|
+
mock_console.warning.assert_called_once()
|
|
37
|
+
warning_message = mock_console.warning.call_args.args[0]
|
|
38
|
+
assert "top" in warning_message
|
|
39
|
+
assert "conformance tests script" in warning_message
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_no_warning_when_conformance_script_configured():
|
|
43
|
+
plain_module = _make_module("top", has_acceptance_tests=True)
|
|
44
|
+
args = Namespace(conformance_tests_script="run_conformance_tests.sh")
|
|
45
|
+
|
|
46
|
+
with patch("plain2code.console") as mock_console:
|
|
47
|
+
plain2code.warn_if_acceptance_tests_without_conformance_script(plain_module, args)
|
|
48
|
+
|
|
49
|
+
mock_console.warning.assert_not_called()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_no_warning_when_no_acceptance_tests():
|
|
53
|
+
plain_module = _make_module("top", has_acceptance_tests=False)
|
|
54
|
+
args = Namespace(conformance_tests_script=None)
|
|
55
|
+
|
|
56
|
+
with patch("plain2code.console") as mock_console:
|
|
57
|
+
plain2code.warn_if_acceptance_tests_without_conformance_script(plain_module, args)
|
|
58
|
+
|
|
59
|
+
mock_console.warning.assert_not_called()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_warns_when_acceptance_tests_only_in_required_module():
|
|
63
|
+
required_module = _make_module("dependency", has_acceptance_tests=True)
|
|
64
|
+
plain_module = _make_module("top", has_acceptance_tests=False, required_modules=[required_module])
|
|
65
|
+
args = Namespace(conformance_tests_script=None)
|
|
66
|
+
|
|
67
|
+
with patch("plain2code.console") as mock_console:
|
|
68
|
+
plain2code.warn_if_acceptance_tests_without_conformance_script(plain_module, args)
|
|
69
|
+
|
|
70
|
+
mock_console.warning.assert_called_once()
|
|
71
|
+
warning_message = mock_console.warning.call_args.args[0]
|
|
72
|
+
assert "dependency" in warning_message
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_warning_covers_required_modules_for_real_plain_module(get_test_data_path):
|
|
76
|
+
"""Integration test: a main module without acceptance tests that requires a
|
|
77
|
+
module with acceptance tests should still trigger the warning, naming the
|
|
78
|
+
required module. This mirrors the dry-run path which builds a real PlainModule."""
|
|
79
|
+
fixtures_dir = get_test_data_path("data/acceptance_tests_warning")
|
|
80
|
+
with tempfile.TemporaryDirectory() as build, tempfile.TemporaryDirectory() as conformance:
|
|
81
|
+
plain_module = PlainModule(
|
|
82
|
+
"main_requiring_acceptance_tests.plain",
|
|
83
|
+
build,
|
|
84
|
+
conformance,
|
|
85
|
+
[fixtures_dir],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
args = Namespace(conformance_tests_script=None)
|
|
89
|
+
with patch("plain2code.console") as mock_console:
|
|
90
|
+
plain2code.warn_if_acceptance_tests_without_conformance_script(plain_module, args)
|
|
91
|
+
|
|
92
|
+
mock_console.warning.assert_called_once()
|
|
93
|
+
warning_message = mock_console.warning.call_args.args[0]
|
|
94
|
+
assert "required_with_acceptance_tests" in warning_message
|
tests/test_plainfileparser.py
CHANGED
|
@@ -376,6 +376,114 @@ def test_acceptance_tests_block_include_with_trailing_newline_keeps_structure_an
|
|
|
376
376
|
plain_file.plain_file_parser("block_level_include.plain", [get_test_data_path("data/templates")])
|
|
377
377
|
|
|
378
378
|
|
|
379
|
+
def test_acceptance_tests_top_level_rejected():
|
|
380
|
+
plain_source = """
|
|
381
|
+
***acceptance tests***
|
|
382
|
+
|
|
383
|
+
- Test something.
|
|
384
|
+
"""
|
|
385
|
+
with pytest.raises(
|
|
386
|
+
PlainSyntaxError,
|
|
387
|
+
match=re.escape(
|
|
388
|
+
"Syntax error at line 1: acceptance tests heading should be nested under specific functional spec."
|
|
389
|
+
),
|
|
390
|
+
):
|
|
391
|
+
plain_file.parse_plain_source(plain_source, {}, [], [], [])
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_acceptance_tests_top_level_after_other_headings_rejected():
|
|
395
|
+
plain_source = """***definitions***
|
|
396
|
+
|
|
397
|
+
- :concept: is a concept.
|
|
398
|
+
|
|
399
|
+
***acceptance tests***
|
|
400
|
+
|
|
401
|
+
- Test something.
|
|
402
|
+
"""
|
|
403
|
+
with pytest.raises(
|
|
404
|
+
PlainSyntaxError,
|
|
405
|
+
match=re.escape(
|
|
406
|
+
"Syntax error at line 5: acceptance tests heading should be nested under specific functional spec."
|
|
407
|
+
),
|
|
408
|
+
):
|
|
409
|
+
plain_file.parse_plain_source(plain_source, {}, [], [], [])
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_acceptance_tests_nested_under_definitions_rejected():
|
|
413
|
+
plain_source = """***definitions***
|
|
414
|
+
|
|
415
|
+
- :concept: is a concept.
|
|
416
|
+
|
|
417
|
+
***acceptance tests***
|
|
418
|
+
|
|
419
|
+
- Test the :concept:.
|
|
420
|
+
|
|
421
|
+
***functional specs***
|
|
422
|
+
|
|
423
|
+
- Display "hello, world"
|
|
424
|
+
"""
|
|
425
|
+
with pytest.raises(
|
|
426
|
+
PlainSyntaxError,
|
|
427
|
+
match=re.escape(
|
|
428
|
+
"Syntax error at line 5: acceptance tests heading should be nested under specific functional spec."
|
|
429
|
+
),
|
|
430
|
+
):
|
|
431
|
+
plain_file.parse_plain_source(plain_source, {}, [], [], [])
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_acceptance_tests_nested_under_implementation_reqs_rejected():
|
|
435
|
+
plain_source = """***implementation reqs***
|
|
436
|
+
|
|
437
|
+
- First implementation requirement.
|
|
438
|
+
|
|
439
|
+
***acceptance tests***
|
|
440
|
+
|
|
441
|
+
- Test something.
|
|
442
|
+
"""
|
|
443
|
+
with pytest.raises(
|
|
444
|
+
PlainSyntaxError,
|
|
445
|
+
match=re.escape(
|
|
446
|
+
"Syntax error at line 5: acceptance tests heading should be nested under specific functional spec."
|
|
447
|
+
),
|
|
448
|
+
):
|
|
449
|
+
plain_file.parse_plain_source(plain_source, {}, [], [], [])
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def test_acceptance_tests_nested_under_test_reqs_rejected():
|
|
453
|
+
plain_source = """***test reqs***
|
|
454
|
+
|
|
455
|
+
- First test requirement.
|
|
456
|
+
|
|
457
|
+
***acceptance tests***
|
|
458
|
+
|
|
459
|
+
- Test something.
|
|
460
|
+
"""
|
|
461
|
+
with pytest.raises(
|
|
462
|
+
PlainSyntaxError,
|
|
463
|
+
match=re.escape(
|
|
464
|
+
"Syntax error at line 5: acceptance tests heading should be nested under specific functional spec."
|
|
465
|
+
),
|
|
466
|
+
):
|
|
467
|
+
plain_file.parse_plain_source(plain_source, {}, [], [], [])
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_acceptance_tests_nested_under_functional_spec_allowed():
|
|
471
|
+
plain_source = """***definitions***
|
|
472
|
+
|
|
473
|
+
- :concept: is a concept.
|
|
474
|
+
|
|
475
|
+
***functional specs***
|
|
476
|
+
|
|
477
|
+
- Display "hello, world"
|
|
478
|
+
|
|
479
|
+
***acceptance tests***
|
|
480
|
+
|
|
481
|
+
- Test the :concept:.
|
|
482
|
+
"""
|
|
483
|
+
result = plain_file.parse_plain_source(plain_source, {}, [], [], [])
|
|
484
|
+
assert plain_spec.FUNCTIONAL_REQUIREMENTS in result.plain_source
|
|
485
|
+
|
|
486
|
+
|
|
379
487
|
def test_concept_validation_definitions(get_test_data_path):
|
|
380
488
|
plain_file.plain_file_parser(
|
|
381
489
|
"concept_validation_definition.plain",
|
tests/test_plainspec.py
CHANGED
|
@@ -9,6 +9,42 @@ def test_get_linked_resources_invalid_input():
|
|
|
9
9
|
plain_spec.collect_linked_resources([], [], None, True)
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def test_has_acceptance_tests_true():
|
|
13
|
+
plain_source = {
|
|
14
|
+
plain_spec.FUNCTIONAL_REQUIREMENTS: [
|
|
15
|
+
{"markdown": "- First functionality."},
|
|
16
|
+
{
|
|
17
|
+
"markdown": "- Second functionality.",
|
|
18
|
+
plain_spec.ACCEPTANCE_TESTS: [{"markdown": "- Test the second functionality."}],
|
|
19
|
+
},
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
assert plain_spec.has_acceptance_tests(plain_source) is True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_has_acceptance_tests_false_when_no_acceptance_tests():
|
|
26
|
+
plain_source = {
|
|
27
|
+
plain_spec.FUNCTIONAL_REQUIREMENTS: [
|
|
28
|
+
{"markdown": "- First functionality."},
|
|
29
|
+
{"markdown": "- Second functionality."},
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
assert plain_spec.has_acceptance_tests(plain_source) is False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_has_acceptance_tests_false_when_empty_acceptance_tests():
|
|
36
|
+
plain_source = {
|
|
37
|
+
plain_spec.FUNCTIONAL_REQUIREMENTS: [
|
|
38
|
+
{"markdown": "- First functionality.", plain_spec.ACCEPTANCE_TESTS: []},
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
assert plain_spec.has_acceptance_tests(plain_source) is False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_has_acceptance_tests_false_when_no_functional_requirements():
|
|
45
|
+
assert plain_spec.has_acceptance_tests({}) is False
|
|
46
|
+
|
|
47
|
+
|
|
12
48
|
def test_get_frids_simple(get_test_data_path):
|
|
13
49
|
_, plain_source, _ = plain_file.plain_file_parser("simple.plain", [get_test_data_path("data/")])
|
|
14
50
|
|
tests/test_telemetry.py
CHANGED
|
@@ -80,12 +80,14 @@ def test_capture_crash_sends_event_with_tags(transport):
|
|
|
80
80
|
run_state.current_module = "my_module"
|
|
81
81
|
run_state.current_frid = "2.1"
|
|
82
82
|
run_state.current_render_state = "IMPLEMENTING_FRID"
|
|
83
|
+
run_state.user_email = "user@codeplain.ai"
|
|
83
84
|
|
|
84
85
|
assert capture_crash(make_exc_info(KeyError("boom")), run_state, make_args(headless=True))
|
|
85
86
|
sentry_sdk.flush(timeout=2)
|
|
86
87
|
|
|
87
88
|
assert len(transport.events) == 1
|
|
88
|
-
|
|
89
|
+
event = transport.events[0]
|
|
90
|
+
tags = event["tags"]
|
|
89
91
|
assert tags["render_id"] == run_state.render_id
|
|
90
92
|
assert tags["current_module"] == "my_module"
|
|
91
93
|
assert tags["current_frid"] == "2.1"
|
|
@@ -94,6 +96,19 @@ def test_capture_crash_sends_event_with_tags(transport):
|
|
|
94
96
|
assert tags["unittests_script_provided"] is True
|
|
95
97
|
assert tags["conformance_tests_script_provided"] is False
|
|
96
98
|
assert tags["prepare_environment_script_provided"] is False
|
|
99
|
+
assert event["user"]["email"] == "user@codeplain.ai"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_capture_crash_without_user_email(transport):
|
|
103
|
+
init_with_transport(transport)
|
|
104
|
+
|
|
105
|
+
run_state = RunState(spec_filename="test.plain")
|
|
106
|
+
|
|
107
|
+
assert capture_crash(make_exc_info(KeyError("boom")), run_state, make_args())
|
|
108
|
+
sentry_sdk.flush(timeout=2)
|
|
109
|
+
|
|
110
|
+
assert len(transport.events) == 1
|
|
111
|
+
assert "email" not in transport.events[0].get("user", {})
|
|
97
112
|
|
|
98
113
|
|
|
99
114
|
def test_capture_crash_without_run_state(transport):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# Standard template library for plain2code
|
|
File without changes
|
|
File without changes
|
|
File without changes
|