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.
@@ -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")
@@ -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
- change_box.mount(
124
- Label(
125
- f"--- {title_start} detected in {'required ' if is_required_module else 'current '}module [#5593FF]{pr.change.module_name}[/] ---",
126
- classes="rendering-info-row",
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
- f"{title_start} in a required module may affect the current module",
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"))