Skip to content

Commit c8de6aa

Browse files
committed
Enable requiring zipped modules ENG-168
1 parent e3d964c commit c8de6aa

12 files changed

Lines changed: 774 additions & 26 deletions

module_renderer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ def _render_module(
111111
):
112112
return False, False
113113

114+
# We are going to (re)render this module. If it currently exists only as a
115+
# "<module>.module" archive, unpack it into the real plain_modules/<module>/ folder now,
116+
# before RenderContext snapshots the build folder path and before the git repos, metadata,
117+
# and memory folder are touched.
118+
plain_module.ensure_module_unpacked()
119+
114120
memory_manager = MemoryManager(
115121
self.codeplainAPI,
116122
plain_module.module_memory_folder,

partial_rendering.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class PlainModuleRenderState:
1212
last_render_module: PlainModule
1313
last_render_frid: str | None
1414
change: PlainModule | None = None
15-
change_type: Literal["spec_change", "code_change"] | None = None
15+
change_type: Literal["spec_change", "code_change", "missing_conformance_tests"] | None = None
1616

1717

1818
@dataclass
@@ -56,6 +56,19 @@ def code_change(plain_module: PlainModule) -> PlainModule | None:
5656
return None
5757

5858

59+
def archive_missing_conformance_tests(plain_module: PlainModule, render_conformance_tests: bool) -> PlainModule | None:
60+
"""Return the first module available only as a "<module>.module" archive that has no conformance
61+
tests while conformance testing is enabled. Such an archive cannot be consumed as-is (regression
62+
needs the tests), so the module must be re-rendered."""
63+
if not render_conformance_tests:
64+
return None
65+
all_modules = plain_module.all_required_modules + [plain_module]
66+
for _module in all_modules:
67+
if _module.is_archived_only() and not _module.archive_has_conformance_tests():
68+
return _module
69+
return None
70+
71+
5972
def module_comes_before_or_equal(
6073
all_required_modules: list[PlainModule],
6174
module1: PlainModule,
@@ -70,15 +83,24 @@ def module_comes_before_or_equal(
7083
raise ValueError(f"Module {module1.module_name} and {module2.module_name} not found in {all_required_modules}")
7184

7285

73-
def get_plain_module_render_state(plain_module: PlainModule) -> PlainModuleRenderState | None:
86+
def get_plain_module_render_state(
87+
plain_module: PlainModule, render_conformance_tests: bool = False
88+
) -> PlainModuleRenderState | None:
7489
sc = spec_change(plain_module)
7590
cc = code_change(plain_module)
91+
mt = archive_missing_conformance_tests(plain_module, render_conformance_tests)
7692
all_required_modules = plain_module.all_required_modules
7793
last_rendered_module_name, last_rendered_frid = plain_module.get_module_render_status()
78-
if last_rendered_module_name is None and last_rendered_frid is None:
94+
if last_rendered_module_name is None and last_rendered_frid is None and mt is None:
7995
return None
8096

81-
if last_rendered_module_name == plain_module.module_name:
97+
if last_rendered_module_name is None:
98+
# Nothing has been rendered yet, but an archived module blocks consumption (no tests).
99+
# Anchor the state on that module so the user is prompted to re-render it.
100+
assert mt is not None # guaranteed by the early return above
101+
module = mt
102+
last_rendered_frid = None
103+
elif last_rendered_module_name == plain_module.module_name:
82104
module = plain_module
83105
else:
84106
found_module: PlainModule | None = None
@@ -99,6 +121,13 @@ def get_plain_module_render_state(plain_module: PlainModule) -> PlainModuleRende
99121
change_type=None,
100122
)
101123

124+
# An archive that lacks conformance tests (while testing is enabled) is a hard blocker: it cannot
125+
# be consumed as-is, so it takes precedence over spec/code changes.
126+
if mt is not None:
127+
pr.change = mt
128+
pr.change_type = "missing_conformance_tests"
129+
return pr
130+
102131
if sc is None and cc is None:
103132
return pr
104133

@@ -129,6 +158,8 @@ def get_all_affected_modules_from_change(
129158
start_module = plain_module.get_next_module(plain_module_render_state.change.module_name)
130159
else:
131160
start_module = plain_module_render_state.change
161+
elif plain_module_render_state.change_type == "missing_conformance_tests":
162+
start_module = plain_module_render_state.change
132163
else:
133164
raise ValueError(f"Unknown change type: {plain_module_render_state.change_type}")
134165

plain2code.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
ImportedModuleWithFunctionalitiesError,
2929
InvalidAPIKey,
3030
InvalidFridArgument,
31+
InvalidModuleArchiveError,
3132
MissingAPIKey,
3233
MissingFunctionalitiesError,
3334
MissingPreviousFunctionalitiesError,
@@ -79,6 +80,7 @@
7980
UnsupportedResourceType,
8081
UnsupportedBase64Content,
8182
GitNotInstalledError,
83+
InvalidModuleArchiveError,
8284
SystemExit,
8385
)
8486

@@ -212,6 +214,15 @@ def render( # noqa: C901
212214

213215
warn_if_acceptance_tests_without_conformance_script(plain_module, args)
214216

217+
# A built module can be distributed as a "<module>.module" zip archive instead of an unpacked directory.
218+
# Extract any such module to a scratch location up front. Scratch dirs are removed in main()'s finally.
219+
# Skip an archive-only module that lacks conformance tests while testing is enabled: it cannot be
220+
# consumed as-is, so leave it unmaterialized so it is detected below as needing a re-render.
221+
for module in plain_module.all_required_modules + [plain_module]:
222+
if args.render_conformance_tests and module.is_archived_only() and not module.archive_has_conformance_tests():
223+
continue
224+
module.materialize()
225+
215226
# The module_metadata file lives outside the code git repo. That means that a crash mid-render can leave it
216227
# claiming a functionality was implemented even thought it wasn't yet committed (because of the crash).
217228
# Out of precaution, this reconciles every module_metadata against the code repo.
@@ -220,7 +231,7 @@ def render( # noqa: C901
220231

221232
render_choice = None
222233
if render_range is None:
223-
plain_module_render_state = get_plain_module_render_state(plain_module)
234+
plain_module_render_state = get_plain_module_render_state(plain_module, args.render_conformance_tests)
224235
if plain_module_render_state is not None:
225236
render_choices = get_render_choices(plain_module, plain_module_render_state, args.force_render)
226237
ask_user = True
@@ -417,6 +428,9 @@ def main(): # noqa: C901
417428
args.filename,
418429
error_message=error_message,
419430
)
431+
# Remove any scratch extractions created for archive-only ("<module>.module") modules.
432+
for module in plain_module.all_required_modules + [plain_module]:
433+
module.cleanup_scratch()
420434

421435
if args.headless and (exc_info is not None or not run_state.render_succeeded):
422436
sys.exit(1)

plain2code_exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,11 @@ class GitNotInstalledError(Exception):
117117
"""Raised when git is not installed or not found on PATH."""
118118

119119
pass
120+
121+
122+
class InvalidModuleArchiveError(Exception):
123+
"""Raised when a ``<module>.module`` archive is missing, corrupt, or has an
124+
unexpected layout (not a zip, missing ``code/``/``tests/``, ``.git`` not a real
125+
directory, detached HEAD, or an unsafe member path)."""
126+
127+
pass

0 commit comments

Comments
 (0)