diff --git a/plain2code_events.py b/plain2code_events.py index f9fb40b4..0f5891ec 100644 --- a/plain2code_events.py +++ b/plain2code_events.py @@ -22,6 +22,8 @@ class RenderContextSnapshot: unit_tests_running_context: Optional[UnitTestsRunningContext] script_execution_history: Optional[ScriptExecutionHistory] module_name: str + # Where the functionality stands among those of its module, as "3/12". None before one is picked. + functionality_position: Optional[str] = None @dataclass diff --git a/plain2code_utils.py b/plain2code_utils.py index 6ef6d08f..de2f6593 100644 --- a/plain2code_utils.py +++ b/plain2code_utils.py @@ -31,6 +31,18 @@ def format_duration_hms(total_seconds: float) -> str: } +def functionality_position(frids: list[str], frid: Optional[str]) -> Optional[str]: + """Where a functionality stands among the functionalities of its module, as "3/12". + + The count is over every functionality of the module, not over a render range, so the number + says where the render is in the specification whatever part of it was asked for. + """ + if frid is None or frid not in frids: + return None + + return f"{frids.index(frid) + 1}/{len(frids)}" + + def qualified_frid(module_name: Optional[str], frid: Optional[str]) -> Optional[str]: """A functionality named as `:`. diff --git a/render_machine/actions/render_functional_requirement.py b/render_machine/actions/render_functional_requirement.py index 7bee5d3f..31ab95de 100644 --- a/render_machine/actions/render_functional_requirement.py +++ b/render_machine/actions/render_functional_requirement.py @@ -19,6 +19,21 @@ class RenderFunctionalRequirement(BaseAction): FUNCTIONAL_REQUIREMENT_TOO_COMPLEX_OUTCOME = "functional_requirement_too_complex" ITERATION_LIMIT_EXCEEDED_OUTCOME = "frid_iteration_limit_exceeded" + @staticmethod + def functionality_heading(render_context) -> str: + """The block logged when a functionality starts: its module, its ID, and where it stands.""" + msg = "-------------------------------------\n" + msg += f"Module: {render_context.module_name}\n" + msg += f"Rendering functionality {render_context.frid_context.frid}" + position = render_context.current_functionality_position() + if position: + msg += f" ({position})" + if render_context.frid_context.functional_requirement_render_attempts > 1: + msg += f", attempt number {render_context.frid_context.functional_requirement_render_attempts}/{MAX_CODE_GENERATION_RETRIES}" + msg += f":\n{render_context.frid_context.functional_requirement_text}\n" + msg += "-------------------------------------" + return msg + def execute(self, render_context: RenderContext, _previous_action_payload: Any | None): if render_context.frid_context.functional_requirement_render_attempts >= MAX_CODE_GENERATION_RETRIES: # A render attempt is only restarted once its unit tests used up every fix round. @@ -49,14 +64,7 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any | ) _, memory_files_content = MemoryManager.fetch_memory_files(render_context.memory_manager.memory_folder) - msg = "-------------------------------------\n" - msg += f"Module: {render_context.module_name}\n" - msg += f"Rendering functionality {render_context.frid_context.frid}" - if render_context.frid_context.functional_requirement_render_attempts > 1: - msg += f", attempt number {render_context.frid_context.functional_requirement_render_attempts}/{MAX_CODE_GENERATION_RETRIES}" - msg += f":\n{render_context.frid_context.functional_requirement_text}\n" - msg += "-------------------------------------" - console.info(msg) + console.info(self.functionality_heading(render_context)) try: render_utils.print_inputs(render_context, existing_files_content, "Files sent as input to code generation:") diff --git a/render_machine/render_context.py b/render_machine/render_context.py index 07633285..9bca66ae 100644 --- a/render_machine/render_context.py +++ b/render_machine/render_context.py @@ -10,7 +10,7 @@ from plain2code_console import RETRY_COLOR, console from plain2code_events import RenderContextSnapshot from plain2code_state import RunState -from plain2code_utils import qualified_frid +from plain2code_utils import functionality_position, qualified_frid from plain_modules import PlainModule from render_machine import triggers from render_machine.conformance_tests import CONFORMANCE_TESTS_DEFINITION_FILE_NAME, ConformanceTests @@ -122,6 +122,13 @@ def dispatch_error(self, error_message: str): self.last_error_message = error_message self.machine.dispatch(triggers.HANDLE_ERROR) + def current_functionality_position(self) -> Optional[str]: + """Where the functionality being implemented stands among those of its module, as "3/12".""" + return functionality_position( + list(plain_spec.get_frids(self.plain_source_tree)), + self.frid_context.frid if self.frid_context else None, + ) + def create_snapshot(self) -> RenderContextSnapshot: return RenderContextSnapshot( frid_context=deepcopy(self.frid_context) if self.frid_context else None, @@ -133,6 +140,7 @@ def create_snapshot(self) -> RenderContextSnapshot: ), script_execution_history=deepcopy(self.script_execution_history), module_name=self.module_name, + functionality_position=self.current_functionality_position(), ) def get_required_modules_functionalities(self): diff --git a/tests/test_functionality_position.py b/tests/test_functionality_position.py new file mode 100644 index 00000000..463a2b0c --- /dev/null +++ b/tests/test_functionality_position.py @@ -0,0 +1,90 @@ +"""The TUI names the functionality being implemented and where it stands in its module. + +The func-id says which functionality it is. It does not say how far the module has come, which the +number before the renderer took func-ids used to say. +""" + +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +import file_utils # noqa: F401 # imported before plain_modules, which it imports back from +from plain2code_events import RenderContextSnapshot +from plain2code_utils import functionality_position +from render_machine.actions.render_functional_requirement import RenderFunctionalRequirement +from tui.state_handlers import FridReadyHandler + +FRIDS = ["add-login-button", "add-logout-button", "remember-the-session"] + + +class TestFunctionalityPosition: + def test_the_first_functionality(self): + assert functionality_position(FRIDS, "add-login-button") == "1/3" + + def test_the_last_functionality(self): + assert functionality_position(FRIDS, "remember-the-session") == "3/3" + + @pytest.mark.parametrize("frid", [None, "no-such-functionality"], ids=["none", "unknown"]) + def test_nothing_to_place(self, frid): + assert functionality_position(FRIDS, frid) is None + + def test_a_module_with_one_functionality(self): + assert functionality_position(["only-one"], "only-one") == "1/1" + + +def _snapshot(position): + return RenderContextSnapshot( + frid_context=SimpleNamespace(frid="add-login-button", functional_requirement_text="Add a login button."), + conformance_tests_running_context=None, + unit_tests_running_context=None, + script_execution_history=None, + module_name="", + functionality_position=position, + ) + + +def _functionality_line(position): + handler = FridReadyHandler(MagicMock(), unittests_script=None, conformance_tests_script=None) + with ( + patch("tui.state_handlers.get_frid_progress") as get_progress, + patch("tui.state_handlers.update_progress_item_status"), + ): + handler.handle([], _snapshot(position), []) + return get_progress.return_value.update_functionality_text.call_args.args[0] + + +def test_the_tui_line_carries_the_position(): + assert _functionality_line("3/12") == "Functionality add-login-button (3/12): Add a login button." + + +def test_the_tui_line_without_a_position_reads_as_before(): + assert _functionality_line(None) == "Functionality add-login-button: Add a login button." + + +def _log_heading(frids, frid, attempts=1): + """The heading the renderer logs when it starts a functionality.""" + render_context = SimpleNamespace( + module_name="shop", + frid_context=SimpleNamespace( + frid=frid, + functional_requirement_text="Add a login button.", + functional_requirement_render_attempts=attempts, + ), + current_functionality_position=lambda: functionality_position(frids, frid), + ) + return RenderFunctionalRequirement.functionality_heading(render_context) + + +def test_the_log_heading_carries_the_position(): + assert "Rendering functionality add-login-button (1/3):" in _log_heading(FRIDS, "add-login-button") + + +def test_the_log_heading_keeps_the_attempt_after_the_position(): + heading = _log_heading(FRIDS, "add-login-button", attempts=2) + + assert "Rendering functionality add-login-button (1/3), attempt number 2/" in heading + + +def test_the_log_heading_without_a_position_reads_as_before(): + assert "Rendering functionality add-login-button:" in _log_heading([], "add-login-button") diff --git a/tui/state_handlers.py b/tui/state_handlers.py index ee6ec2c9..cb842ab6 100644 --- a/tui/state_handlers.py +++ b/tui/state_handlers.py @@ -62,8 +62,10 @@ def handle(self, _: list[str], snapshot: RenderContextSnapshot, _previous_state_ # Update FRID text functionality = qualified_frid(snapshot.module_name, snapshot.frid_context.frid) + # The position says how far the module has come; the func-id alone does not. + position = f" ({snapshot.functionality_position})" if snapshot.functionality_position else "" rendering_functionality_text = ( - f"{tui_components.FRIDProgress.RENDERING_FUNCTIONALITY_TEXT}{functionality}: " + f"{tui_components.FRIDProgress.RENDERING_FUNCTIONALITY_TEXT}{functionality}{position}: " f"{snapshot.frid_context.functional_requirement_text}" ) get_frid_progress(self.tui).update_functionality_text(rendering_functionality_text)