codeplain 0.3.9__py3-none-any.whl → 0.3.10.dev2__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.
- {codeplain-0.3.9.dist-info → codeplain-0.3.10.dev2.dist-info}/METADATA +36 -8
- {codeplain-0.3.9.dist-info → codeplain-0.3.10.dev2.dist-info}/RECORD +27 -26
- {codeplain-0.3.9.dist-info → codeplain-0.3.10.dev2.dist-info}/WHEEL +1 -1
- concept_utils.py +6 -0
- module_renderer.py +6 -0
- partial_rendering.py +35 -4
- plain2code.py +31 -3
- plain2code_arguments.py +7 -4
- plain2code_console.py +3 -1
- plain2code_exceptions.py +8 -0
- plain2code_logger.py +37 -15
- plain_file.py +18 -1
- plain_modules.py +197 -7
- render_machine/actions/prepare_repositories.py +5 -0
- render_machine/conformance_tests.py +40 -3
- render_machine/implementation_code_helpers.py +5 -1
- render_machine/render_context.py +13 -1
- system_config.py +35 -0
- tests/test_cli_output.py +13 -0
- tests/test_headless_logging.py +177 -0
- tests/test_plain_modules.py +267 -1
- tests/test_plainfileparser.py +41 -4
- tests/test_prepare_repositories_layout.py +158 -0
- tests/test_system_config.py +37 -0
- tui/plain_module_render_choice_tui.py +25 -8
- {codeplain-0.3.9.dist-info → codeplain-0.3.10.dev2.dist-info}/entry_points.txt +0 -0
- {codeplain-0.3.9.dist-info → codeplain-0.3.10.dev2.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,13 +8,18 @@ Each module renders into a single tree under the build folder:
|
|
|
8
8
|
<build>/<module>/tests/ git repo with the conformance tests
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import json
|
|
11
12
|
import os
|
|
13
|
+
import shutil
|
|
12
14
|
import tempfile
|
|
15
|
+
import zipfile
|
|
13
16
|
from pathlib import Path
|
|
14
17
|
from types import SimpleNamespace
|
|
15
18
|
|
|
16
19
|
import pytest
|
|
17
20
|
|
|
21
|
+
from git_utils import FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE, add_all_files_and_commit, init_git_repo
|
|
22
|
+
from partial_rendering import archive_missing_conformance_tests, get_plain_module_render_state, get_render_choices
|
|
18
23
|
from plain_modules import PlainModule
|
|
19
24
|
from render_machine.actions.prepare_repositories import PrepareRepositories
|
|
20
25
|
from render_machine.conformance_tests import CONFORMANCE_TESTS_DEFINITION_FILE_NAME, ConformanceTests
|
|
@@ -49,6 +54,29 @@ def _make_render_context(module: PlainModule, render_conformance_tests: bool) ->
|
|
|
49
54
|
)
|
|
50
55
|
|
|
51
56
|
|
|
57
|
+
def _init_repo_with_finished_frid(repo_path: str, module_name: str, frid: str = "1") -> None:
|
|
58
|
+
os.makedirs(repo_path, exist_ok=True)
|
|
59
|
+
init_git_repo(repo_path, module_name=module_name)
|
|
60
|
+
(Path(repo_path) / f"frid_{frid}.txt").write_text(f"frid {frid}\n")
|
|
61
|
+
add_all_files_and_commit(
|
|
62
|
+
repo_path,
|
|
63
|
+
FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format(frid),
|
|
64
|
+
module_name=module_name,
|
|
65
|
+
frid=frid,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _archive_module(module: PlainModule) -> None:
|
|
70
|
+
"""Zip the module's folder contents (code/, tests/ incl. .git) into <module>.module, then
|
|
71
|
+
remove the folder, so the module exists only as an archive."""
|
|
72
|
+
with zipfile.ZipFile(module.module_archive_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
73
|
+
for root, _dirs, files in os.walk(module.module_folder):
|
|
74
|
+
for name in files:
|
|
75
|
+
full = os.path.join(root, name)
|
|
76
|
+
zf.write(full, os.path.relpath(full, module.module_folder))
|
|
77
|
+
shutil.rmtree(module.module_folder)
|
|
78
|
+
|
|
79
|
+
|
|
52
80
|
# --------------------------------------------------------------------------
|
|
53
81
|
# PrepareRepositories — fresh render
|
|
54
82
|
# --------------------------------------------------------------------------
|
|
@@ -117,3 +145,133 @@ def test_cross_module_copy_lands_in_hidden_folder_under_tests(tmp_build_folder):
|
|
|
117
145
|
expected = os.path.join(tmp_build_folder, "top_module", "tests", ".required_module", "1_frid_feature")
|
|
118
146
|
assert new_folder == expected
|
|
119
147
|
assert source_folder == expected
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# --------------------------------------------------------------------------
|
|
151
|
+
# Zipped modules — consuming a required module shipped as "<module>.module"
|
|
152
|
+
# --------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.fixture
|
|
156
|
+
def root_module(get_test_data_path, tmp_build_folder):
|
|
157
|
+
"""pr_root -> pr_middle -> pr_leaf; root.required_modules[-1] == pr_middle."""
|
|
158
|
+
return PlainModule("pr_root.plain", tmp_build_folder, [get_test_data_path("data/partial_rendering")])
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_full_render_clones_from_archived_required_module(root_module):
|
|
162
|
+
"""A required module shipped only as a "<module>.module" archive is materialized and used as
|
|
163
|
+
the clone starting point, and its code hash is recorded correctly in the module metadata."""
|
|
164
|
+
previous = root_module.required_modules[-1]
|
|
165
|
+
_init_repo_with_finished_frid(previous.module_build_folder, previous.module_name)
|
|
166
|
+
_init_repo_with_finished_frid(previous.module_conformance_tests_folder, previous.module_name)
|
|
167
|
+
_archive_module(previous)
|
|
168
|
+
assert previous.is_archived_only()
|
|
169
|
+
|
|
170
|
+
render_context = _make_render_context(root_module, render_conformance_tests=False)
|
|
171
|
+
PrepareRepositories().execute(render_context, None)
|
|
172
|
+
|
|
173
|
+
# The root code repo was cloned (from the materialized archive), and the required module was
|
|
174
|
+
# materialized rather than left as an unusable archive.
|
|
175
|
+
assert os.path.isdir(os.path.join(root_module.module_build_folder, ".git"))
|
|
176
|
+
assert previous._resolved_module_folder is not None
|
|
177
|
+
assert not os.path.exists(previous._default_module_folder) # archive not unpacked in place
|
|
178
|
+
assert os.path.isfile(previous.module_archive_path) # archive preserved
|
|
179
|
+
|
|
180
|
+
# seed_module_metadata recorded the required module's real code hash (ordering guard).
|
|
181
|
+
metadata = root_module.load_module_metadata()
|
|
182
|
+
assert metadata["required_modules_code_hash"] == previous.get_module_code_hash()
|
|
183
|
+
|
|
184
|
+
previous.cleanup_scratch()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_archive_missing_conformance_tests_detects_code_only_required_module(root_module):
|
|
188
|
+
"""A required module shipped as a code-only .module (no tests/) is flagged only when conformance
|
|
189
|
+
testing is enabled."""
|
|
190
|
+
previous = root_module.required_modules[-1]
|
|
191
|
+
_init_repo_with_finished_frid(previous.module_build_folder, previous.module_name)
|
|
192
|
+
_archive_module(previous) # code/ only, no tests/
|
|
193
|
+
|
|
194
|
+
assert archive_missing_conformance_tests(root_module, True) is previous
|
|
195
|
+
assert archive_missing_conformance_tests(root_module, False) is None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_archive_with_tests_not_flagged(root_module):
|
|
199
|
+
previous = root_module.required_modules[-1]
|
|
200
|
+
_init_repo_with_finished_frid(previous.module_build_folder, previous.module_name)
|
|
201
|
+
_init_repo_with_finished_frid(previous.module_conformance_tests_folder, previous.module_name)
|
|
202
|
+
_archive_module(previous) # code/ + tests/
|
|
203
|
+
|
|
204
|
+
assert archive_missing_conformance_tests(root_module, True) is None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_render_state_flags_missing_tests_and_offers_rerender(root_module):
|
|
208
|
+
"""A code-only archive (conformance on) surfaces a 'missing_conformance_tests' render state with
|
|
209
|
+
a rerender choice and a quit choice."""
|
|
210
|
+
previous = root_module.required_modules[-1]
|
|
211
|
+
_init_repo_with_finished_frid(previous.module_build_folder, previous.module_name)
|
|
212
|
+
_archive_module(previous) # code/ only
|
|
213
|
+
|
|
214
|
+
state = get_plain_module_render_state(root_module, render_conformance_tests=True)
|
|
215
|
+
assert state is not None
|
|
216
|
+
assert state.change_type == "missing_conformance_tests"
|
|
217
|
+
assert state.change.module_name == previous.module_name
|
|
218
|
+
|
|
219
|
+
choice_types = {c.choice_type for c in get_render_choices(root_module, state).values()}
|
|
220
|
+
assert "quit" in choice_types
|
|
221
|
+
assert "rerender_affected" in choice_types
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_conformance_tests_resolver_overrides_default(tmp_build_folder):
|
|
225
|
+
"""A resolver maps an archived required module's name to its resolved (scratch) tests folder;
|
|
226
|
+
unknown names fall back to the default base-folder path."""
|
|
227
|
+
resolved = {"req": "/scratch/req/tests"}
|
|
228
|
+
conformance_tests = ConformanceTests(
|
|
229
|
+
tmp_build_folder,
|
|
230
|
+
CONFORMANCE_TESTS_DEFINITION_FILE_NAME,
|
|
231
|
+
resolve_module_tests_folder=lambda name: resolved.get(name),
|
|
232
|
+
)
|
|
233
|
+
assert conformance_tests.get_module_conformance_tests_folder("req") == "/scratch/req/tests"
|
|
234
|
+
assert conformance_tests.get_module_conformance_tests_folder("other") == os.path.join(
|
|
235
|
+
tmp_build_folder, "other", "tests"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_conformance_tests_json_stored_relative_read_absolute(tmp_build_folder):
|
|
240
|
+
"""folder_name is stored relative to the module's tests folder on disk, but presented as an
|
|
241
|
+
absolute path in memory — and the input dict is not mutated."""
|
|
242
|
+
conformance_tests = ConformanceTests(tmp_build_folder, CONFORMANCE_TESTS_DEFINITION_FILE_NAME)
|
|
243
|
+
tests_folder = conformance_tests.get_module_conformance_tests_folder("m")
|
|
244
|
+
os.makedirs(tests_folder)
|
|
245
|
+
|
|
246
|
+
absolute_folder = os.path.join(tests_folder, "1_feature")
|
|
247
|
+
json_in = {"1": {"folder_name": absolute_folder, "functional_requirement": "do a thing"}}
|
|
248
|
+
conformance_tests.dump_conformance_tests_json("m", json_in)
|
|
249
|
+
|
|
250
|
+
# On disk: relative.
|
|
251
|
+
with open(os.path.join(tests_folder, CONFORMANCE_TESTS_DEFINITION_FILE_NAME)) as f:
|
|
252
|
+
raw = json.load(f)
|
|
253
|
+
assert raw["1"]["folder_name"] == "1_feature"
|
|
254
|
+
# Input dict not mutated.
|
|
255
|
+
assert json_in["1"]["folder_name"] == absolute_folder
|
|
256
|
+
|
|
257
|
+
# On read: absolute again.
|
|
258
|
+
loaded = conformance_tests.get_conformance_tests_json("m")
|
|
259
|
+
assert loaded["1"]["folder_name"] == absolute_folder
|
|
260
|
+
assert loaded["1"]["functional_requirement"] == "do a thing"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_conformance_tests_json_resolves_against_scratch_base(tmp_build_folder):
|
|
264
|
+
"""A conformance_tests.json shipped in an archive (relative folder_name) resolves against the
|
|
265
|
+
module's scratch tests folder when the module is materialized."""
|
|
266
|
+
scratch_tests = os.path.join(tmp_build_folder, "scratch_extract", "tests")
|
|
267
|
+
os.makedirs(scratch_tests)
|
|
268
|
+
with open(os.path.join(scratch_tests, CONFORMANCE_TESTS_DEFINITION_FILE_NAME), "w") as f:
|
|
269
|
+
json.dump({"1": {"folder_name": "1_feature", "functional_requirement": "x"}}, f)
|
|
270
|
+
|
|
271
|
+
conformance_tests = ConformanceTests(
|
|
272
|
+
tmp_build_folder,
|
|
273
|
+
CONFORMANCE_TESTS_DEFINITION_FILE_NAME,
|
|
274
|
+
resolve_module_tests_folder=lambda name: scratch_tests if name == "m" else None,
|
|
275
|
+
)
|
|
276
|
+
loaded = conformance_tests.get_conformance_tests_json("m")
|
|
277
|
+
assert loaded["1"]["folder_name"] == os.path.join(scratch_tests, "1_feature")
|
tests/test_system_config.py
CHANGED
|
@@ -5,7 +5,11 @@ import pytest
|
|
|
5
5
|
from system_config import (
|
|
6
6
|
DEVELOPMENT_ENV,
|
|
7
7
|
ENVIRONMENT_VAR,
|
|
8
|
+
PRODUCTION_API_URL,
|
|
8
9
|
PRODUCTION_ENV,
|
|
10
|
+
TEST_API_URL,
|
|
11
|
+
_is_prerelease,
|
|
12
|
+
_resolve_default_api_url,
|
|
9
13
|
_resolve_environment,
|
|
10
14
|
system_config,
|
|
11
15
|
)
|
|
@@ -56,3 +60,36 @@ def test_system_config_exposes_resolved_environment():
|
|
|
56
60
|
|
|
57
61
|
if __name__ == "__main__":
|
|
58
62
|
pytest.main([__file__])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize(
|
|
66
|
+
"version",
|
|
67
|
+
[
|
|
68
|
+
"0.3.10.dev7", # dev build published on every merge to main
|
|
69
|
+
"0.3.10.dev16+gb710c2982", # local segment must not defeat detection
|
|
70
|
+
"0.4.0a1",
|
|
71
|
+
"0.4.0b2",
|
|
72
|
+
"0.4.0rc1",
|
|
73
|
+
"0.3.9.post1.dev2",
|
|
74
|
+
],
|
|
75
|
+
)
|
|
76
|
+
def test_prerelease_versions_default_to_test_api(version):
|
|
77
|
+
assert _is_prerelease(version) is True
|
|
78
|
+
assert _resolve_default_api_url(version) == TEST_API_URL
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize(
|
|
82
|
+
"version",
|
|
83
|
+
[
|
|
84
|
+
"0.3.9",
|
|
85
|
+
"1.0.0",
|
|
86
|
+
"0.3.9.post1", # a post-release is NOT a pre-release
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
def test_stable_versions_default_to_production_api(version):
|
|
90
|
+
assert _is_prerelease(version) is False
|
|
91
|
+
assert _resolve_default_api_url(version) == PRODUCTION_API_URL
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_system_config_exposes_default_api_url():
|
|
95
|
+
assert system_config.default_api_url in (PRODUCTION_API_URL, TEST_API_URL)
|
|
@@ -118,21 +118,38 @@ class PlainModuleRenderChoiceTUI(App):
|
|
|
118
118
|
info_panel.mount(change_box)
|
|
119
119
|
|
|
120
120
|
if pr.change:
|
|
121
|
-
title_start = "Spec changes" if pr.change_type == "spec_change" else "Code changes"
|
|
122
121
|
is_required_module = pr.change.module_name != self.plain_module.module_name
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
module_kind = "required " if is_required_module else "current "
|
|
123
|
+
if pr.change_type == "missing_conformance_tests":
|
|
124
|
+
change_box.mount(
|
|
125
|
+
Label(
|
|
126
|
+
f"--- Conformance tests missing from the archive of {module_kind}module "
|
|
127
|
+
f"[#5593FF]{pr.change.module_name}[/] ---",
|
|
128
|
+
classes="rendering-info-row",
|
|
129
|
+
)
|
|
127
130
|
)
|
|
128
|
-
)
|
|
129
|
-
if is_required_module:
|
|
130
131
|
change_box.mount(
|
|
131
132
|
Label(
|
|
132
|
-
|
|
133
|
+
"The .module archive has no conformance tests but testing is enabled; "
|
|
134
|
+
"the module must be re-rendered (its .module file will be removed).",
|
|
133
135
|
classes="rendering-info-title",
|
|
134
136
|
)
|
|
135
137
|
)
|
|
138
|
+
else:
|
|
139
|
+
title_start = "Spec changes" if pr.change_type == "spec_change" else "Code changes"
|
|
140
|
+
change_box.mount(
|
|
141
|
+
Label(
|
|
142
|
+
f"--- {title_start} detected in {module_kind}module [#5593FF]{pr.change.module_name}[/] ---",
|
|
143
|
+
classes="rendering-info-row",
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
if is_required_module:
|
|
147
|
+
change_box.mount(
|
|
148
|
+
Label(
|
|
149
|
+
f"{title_start} in a required module may affect the current module",
|
|
150
|
+
classes="rendering-info-title",
|
|
151
|
+
)
|
|
152
|
+
)
|
|
136
153
|
|
|
137
154
|
elif pr.last_render_module.is_module_fully_rendered():
|
|
138
155
|
change_box.mount(Label("The current module is fully rendered.", classes="rendering-info-title"))
|
|
File without changes
|
|
File without changes
|