diff --git a/render_machine/actions/fix_conformance_test.py b/render_machine/actions/fix_conformance_test.py index eee87979..ae3ddbe2 100644 --- a/render_machine/actions/fix_conformance_test.py +++ b/render_machine/actions/fix_conformance_test.py @@ -9,6 +9,7 @@ from render_machine.actions.base_action import BaseAction from render_machine.implementation_code_helpers import ImplementationCodeHelpers from render_machine.render_context import RenderContext +from render_machine.render_types import TestExecutionPhase class FixConformanceTest(BaseAction): @@ -151,6 +152,12 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any | style=console.OUTPUT_STYLE, ) render_context.conformance_tests_running_context.should_prepare_testing_environment = True + + # Record which test triggered the change and transition to retry phase + ctx = render_context.conformance_tests_running_context + ctx.test_that_triggered_code_change = (ctx.current_testing_module_name, ctx.current_testing_frid) + ctx.execution_phase = TestExecutionPhase.RETRYING_AFTER_CODE_CHANGE + return self.IMPLEMENTATION_CODE_UPDATED, None else: return self.IMPLEMENTATION_CODE_NOT_UPDATED, None diff --git a/render_machine/actions/render_conformance_tests.py b/render_machine/actions/render_conformance_tests.py index 8ea9c457..6453cb02 100644 --- a/render_machine/actions/render_conformance_tests.py +++ b/render_machine/actions/render_conformance_tests.py @@ -8,6 +8,7 @@ from render_machine.actions.base_action import BaseAction from render_machine.implementation_code_helpers import ImplementationCodeHelpers from render_machine.render_context import RenderContext +from render_machine.render_types import AcceptanceTestPhase, TestExecutionPhase class RenderConformanceTests(BaseAction): @@ -20,19 +21,38 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any | return self._render_acceptance_test(render_context) def _should_render_conformance_tests(self, render_context: RenderContext) -> bool: - return render_context.conformance_tests_running_context.conformance_test_phase_index == 0 + """Check if we should render full conformance tests (first time) vs just acceptance tests (incremental).""" - def _render_conformance_tests(self, render_context: RenderContext): - if render_context.verbose: - console.info("Implementing test requirements:") - console.print_list( - render_context.conformance_tests_running_context.current_testing_frid_specifications[ - plain_spec.TEST_REQUIREMENTS - ], - style=console.INFO_STYLE, - ) + ctx = render_context.conformance_tests_running_context + + # If we're in regression phase, tests already exist - don't render anything new + if ctx.execution_phase == TestExecutionPhase.RUNNING_REGRESSION: + return True # Return True to skip rendering (conformance tests already exist) + + # Render full conformance tests when: + # 1. NOT_STARTED: First render, before acceptance tests begin + if ctx.acceptance_test_phase == AcceptanceTestPhase.NOT_STARTED: + return True + # 2. NOT_APPLICABLE: No acceptance tests defined, always use full conformance test path + if ctx.acceptance_test_phase == AcceptanceTestPhase.NOT_APPLICABLE: + # Return True so _render_conformance_tests() is called, which will skip if tests already exist + return True + + # 3. All other cases (IN_PROGRESS, COMPLETED): Render acceptance tests incrementally + return False + + def _render_conformance_tests(self, render_context: RenderContext): + # Check if tests already exist (e.g., during regression) - if so, skip rendering if not render_context.conformance_tests_running_context.current_conformance_tests_exist(): + if render_context.verbose: + console.info("Implementing test requirements:") + console.print_list( + render_context.conformance_tests_running_context.current_testing_frid_specifications[ + plain_spec.TEST_REQUIREMENTS + ], + style=console.INFO_STYLE, + ) fr_subfolder_name = render_context.codeplain_api.generate_folder_name_from_functional_requirement( frid=render_context.conformance_tests_running_context.current_testing_frid, module_name=render_context.conformance_tests_running_context.current_testing_module_name, @@ -128,6 +148,10 @@ def _render_conformance_tests(self, render_context: RenderContext): return self.SUCCESSFUL_OUTCOME, None def _render_acceptance_test(self, render_context: RenderContext): + if plain_spec.ACCEPTANCE_TESTS not in render_context.frid_context.specifications: + # If there are no acceptance tests defined, continue. + return self.SUCCESSFUL_OUTCOME, None + _, existing_files_content = ImplementationCodeHelpers.fetch_existing_files(render_context.build_folder) _, memory_files_content = MemoryManager.fetch_memory_files(render_context.memory_manager.memory_folder) ( @@ -140,8 +164,9 @@ def _render_acceptance_test(self, render_context: RenderContext): render_context.conformance_tests_running_context.get_current_conformance_test_folder_name(), ) + # Get the current acceptance test being rendered (completed count is 1-based) acceptance_test = render_context.frid_context.specifications[plain_spec.ACCEPTANCE_TESTS][ - render_context.conformance_tests_running_context.conformance_test_phase_index - 1 + render_context.conformance_tests_running_context.acceptance_tests_completed - 1 ] if render_context.verbose: diff --git a/render_machine/render_context.py b/render_machine/render_context.py index 6698636f..7229fb7f 100644 --- a/render_machine/render_context.py +++ b/render_machine/render_context.py @@ -14,9 +14,11 @@ from render_machine import triggers from render_machine.conformance_tests import CONFORMANCE_TESTS_DEFINITION_FILE_NAME, ConformanceTests from render_machine.render_types import ( + AcceptanceTestPhase, ConformanceTestsRunningContext, FridContext, ScriptExecutionHistory, + TestExecutionPhase, UnitTestsRunningContext, ) @@ -278,7 +280,6 @@ def get_next_conformance_tests_running_context(self): def start_unittests_processing_in_conformance_tests(self): self.start_unittests_processing() - self.conformance_tests_running_context = self.get_first_conformance_tests_running_context() def finish_unittests_processing(self): existing_files = file_utils.list_all_text_files(self.build_folder) @@ -351,82 +352,263 @@ def start_testing_environment_preparation(self): def start_conformance_tests_processing(self): console.info("Implementing conformance tests...") + current_frid_specifications, _ = plain_spec.get_specifications_for_frid( + self.plain_source_tree, self.frid_context.frid + ) self.conformance_tests_running_context = ConformanceTestsRunningContext( current_testing_module_name=self.module_name, - current_testing_frid=None, - current_testing_frid_specifications=None, - conformance_test_phase_index=0, + current_testing_frid=self.frid_context.frid, + current_testing_frid_specifications=current_frid_specifications, fix_attempts=0, conformance_tests_json=self.conformance_tests.get_conformance_tests_json(self.module_name), conformance_tests_render_attempts=0, should_prepare_testing_environment=True, + frid_being_implemented=self.frid_context.frid, ) def finish_conformance_tests_processing(self): self.conformance_tests_running_context = None - def start_conformance_tests_for_frid(self): - if self.conformance_tests_running_context.regenerating_conformance_tests: - if self.verbose: - console.info( - f"Recreating conformance tests for functionality {self.conformance_tests_running_context.current_testing_frid}." - ) + # ========== Helper Methods for Conformance Test Execution ========== + + def _should_run_current_frid_tests(self) -> bool: + """Check if we should run/continue testing the current FRID.""" + ctx = self.conformance_tests_running_context + return ( + ctx.execution_phase == TestExecutionPhase.TESTING_CURRENT_FRID + and ctx.current_testing_module_name == self.module_name + and ctx.current_testing_frid == ctx.frid_being_implemented + ) - existing_conformance_tests_folder = self.conformance_tests_running_context.get_conformance_tests_json( - self.conformance_tests_running_context.current_testing_module_name - ).pop(self.conformance_tests_running_context.current_testing_frid) + def _has_more_acceptance_test_phases(self) -> bool: + """Check if there are more acceptance test phases to run.""" + ctx = self.conformance_tests_running_context - file_utils.delete_folder(existing_conformance_tests_folder["folder_name"]) + if ctx.acceptance_test_phase == AcceptanceTestPhase.NOT_APPLICABLE: + return False + if ctx.acceptance_test_phase == AcceptanceTestPhase.COMPLETED: + return False - self.conformance_tests_running_context.conformance_tests_render_attempts += 1 - self.conformance_tests_running_context.fix_attempts = 0 - self.conformance_tests_running_context.regenerating_conformance_tests = False + acceptance_tests = self.frid_context.specifications.get(plain_spec.ACCEPTANCE_TESTS, []) + return ctx.acceptance_tests_completed < len(acceptance_tests) + + def _start_regression_phase(self): + """Transition to regression testing phase.""" + ctx = self.conformance_tests_running_context + + # Only reset code_changed flag if starting fresh regression (not restarting after fix) + if ctx.execution_phase != TestExecutionPhase.RETRYING_AFTER_CODE_CHANGE: + ctx.code_changed_during_regression = False + + ctx.execution_phase = TestExecutionPhase.RUNNING_REGRESSION + ctx.current_testing_frid = None # Will be set by get_first_conformance_tests_running_context + + def _get_next_test_to_run(self): + """Determine which test to run next based on current phase.""" + ctx = self.conformance_tests_running_context + + if ctx.current_testing_frid is None: + return self.get_first_conformance_tests_running_context() else: - # This block is now only executed for the main (last) module. This means that no conformance tests - # postprocessing is taking place. Maybe it should? - if ( - self.conformance_tests_running_context.current_testing_module_name == self.module_name - and self.conformance_tests_running_context.current_testing_frid == self.frid_context.frid - ): - if not self.frid_context.specifications.get( + return self.get_next_conformance_tests_running_context() + + def _has_reached_implementation_frid(self) -> bool: + """Check if regression has reached the FRID being implemented.""" + ctx = self.conformance_tests_running_context + return ( + ctx.execution_phase == TestExecutionPhase.RUNNING_REGRESSION + and ctx.current_testing_module_name == self.module_name + and (ctx.current_testing_frid is None or ctx.current_testing_frid == ctx.frid_being_implemented) + ) + + def _setup_test_specifications(self): + """Load specifications for the current test.""" + ctx = self.conformance_tests_running_context + + if ctx.current_testing_module_name == self.module_name: + ctx.current_testing_frid_specifications, _ = plain_spec.get_specifications_for_frid( + self.plain_source_tree, ctx.current_testing_frid + ) + else: + ctx.current_testing_frid_specifications = ctx.get_conformance_tests_json(ctx.current_testing_module_name)[ + ctx.current_testing_frid + ]["functional_requirement"] + + # ========== Phase Handlers ========== + + def _handle_test_regeneration(self): + """Handle regeneration of conformance tests after too many failures.""" + ctx = self.conformance_tests_running_context + + if self.verbose: + console.info(f"Recreating conformance tests for functionality {ctx.current_testing_frid}.") + + existing_folder = ctx.get_conformance_tests_json(ctx.current_testing_module_name).pop(ctx.current_testing_frid) + file_utils.delete_folder(existing_folder["folder_name"]) + + ctx.conformance_tests_render_attempts += 1 + ctx.fix_attempts = 0 + ctx.regenerating_conformance_tests = False + + def _handle_retry_after_code_change(self): + """Re-run the test that failed and triggered a code change.""" + ctx = self.conformance_tests_running_context + + # The test that failed is still in current_testing_frid - just re-run it + self._setup_test_specifications() + + if ctx.current_conformance_tests_exist(): + self.machine.dispatch(triggers.MARK_CONFORMANCE_TESTS_READY) + + def _on_conformance_test_passed_after_retry(self): + """Called when a test passes after being retried due to code changes.""" + ctx = self.conformance_tests_running_context + + if ctx.execution_phase == TestExecutionPhase.RETRYING_AFTER_CODE_CHANGE: + # Test passed after code change - mark that code changed and restart regression + ctx.code_changed_during_regression = True + ctx.test_that_triggered_code_change = None + self._start_regression_phase() + + def _handle_current_frid_testing(self): + """Handle incremental testing of the current FRID being implemented.""" + ctx = self.conformance_tests_running_context + + # Wait for tests to be rendered + if not ctx.current_conformance_tests_exist(): + return + + # Initialize acceptance test phase AFTER tests exist and pass + # This ensures full conformance tests run first before acceptance tests + if ctx.acceptance_test_phase == AcceptanceTestPhase.NOT_STARTED: + acceptance_tests = self.frid_context.specifications.get(plain_spec.ACCEPTANCE_TESTS) + if not acceptance_tests: + ctx.acceptance_test_phase = AcceptanceTestPhase.NOT_APPLICABLE + # Increment counter immediately to signal "we're starting the test process" + # This prevents re-running the test after it's rendered + ctx.acceptance_tests_completed = 1 + else: + ctx.acceptance_test_phase = AcceptanceTestPhase.IN_PROGRESS + + # Handle case: No acceptance tests + if ctx.acceptance_test_phase == AcceptanceTestPhase.NOT_APPLICABLE: + if ctx.acceptance_tests_completed == 1: + # Test has run once - move to regression + ctx.acceptance_test_phase = AcceptanceTestPhase.COMPLETED + self._start_regression_phase() + self._handle_regression_testing() + return + else: + # This shouldn't happen since we set completed=1 during initialization + raise RuntimeError(f"Unexpected state: acceptance_tests_completed={ctx.acceptance_tests_completed}") + + # Handle case: Has acceptance tests + if ctx.acceptance_test_phase == AcceptanceTestPhase.IN_PROGRESS: + if self._has_more_acceptance_test_phases(): + # Run next phase + if ctx.acceptance_tests_completed == 0: + ctx.current_testing_frid_high_level_implementation_plan = None + + ctx.acceptance_tests_completed += 1 + acceptance_tests = self.frid_context.specifications[plain_spec.ACCEPTANCE_TESTS][ + : ctx.acceptance_tests_completed + ] + ctx.get_conformance_tests_json(ctx.current_testing_module_name)[ctx.current_testing_frid][ plain_spec.ACCEPTANCE_TESTS - ) or self.conformance_tests_running_context.conformance_test_phase_index == len( - self.frid_context.specifications[plain_spec.ACCEPTANCE_TESTS] - ): + ] = acceptance_tests + return + else: + # All phases done - move to regression + ctx.acceptance_test_phase = AcceptanceTestPhase.COMPLETED + self._start_regression_phase() + self._handle_regression_testing() + return + + # Should not reach here + raise RuntimeError(f"Unexpected acceptance test phase: {ctx.acceptance_test_phase}") + + def _handle_regression_testing(self): + """Handle regression testing of all earlier FRIDs.""" + + # Get next test to run + self.conformance_tests_running_context = self._get_next_test_to_run() + + # Get reference to the updated context + ctx = self.conformance_tests_running_context + + # Set up specs and run test + self._setup_test_specifications() + + if ctx.current_conformance_tests_exist(): + # Check if this is the implementation FRID (last test to run) + if self._has_reached_implementation_frid(): + # Reached implementation FRID - only re-run it if code changed during regression + if ctx.code_changed_during_regression: + # Code changed - run the implementation FRID again to verify no regression + # After it passes, mark as completed on next iteration + ctx.execution_phase = TestExecutionPhase.COMPLETED + else: + # No code changes - skip re-running implementation FRID, mark as completed immediately + ctx.execution_phase = TestExecutionPhase.COMPLETED self.machine.dispatch(triggers.MARK_ALL_CONFORMANCE_TESTS_PASSED) return - if self.conformance_tests_running_context.conformance_test_phase_index == 0: - self.conformance_tests_running_context.current_testing_frid_high_level_implementation_plan = None - self.conformance_tests_running_context.conformance_test_phase_index += 1 - current_acceptance_tests = self.frid_context.specifications[plain_spec.ACCEPTANCE_TESTS][ - : self.conformance_tests_running_context.conformance_test_phase_index - ] - self.conformance_tests_running_context.get_conformance_tests_json( - self.conformance_tests_running_context.current_testing_module_name - )[self.frid_context.frid][plain_spec.ACCEPTANCE_TESTS] = current_acceptance_tests - return + self.machine.dispatch(triggers.MARK_CONFORMANCE_TESTS_READY) - if self.conformance_tests_running_context.current_testing_frid is None: - self.conformance_tests_running_context = self.get_first_conformance_tests_running_context() - else: - self.conformance_tests_running_context = self.get_next_conformance_tests_running_context() + # ========== Main Conformance Test Orchestration ========== - if self.conformance_tests_running_context.current_testing_module_name == self.module_name: - self.conformance_tests_running_context.current_testing_frid_specifications, _ = ( - plain_spec.get_specifications_for_frid( - self.plain_source_tree, self.conformance_tests_running_context.current_testing_frid - ) - ) - else: - self.conformance_tests_running_context.current_testing_frid_specifications = ( - self.conformance_tests_running_context.get_conformance_tests_json( - self.conformance_tests_running_context.current_testing_module_name - )[self.conformance_tests_running_context.current_testing_frid]["functional_requirement"] - ) + def start_conformance_tests_for_frid(self): + """ + Orchestrate conformance test execution. + + Flow: + 1. Handle test regeneration (if needed) + 2. Handle test passed after retry (transition to regression) + 3. Handle code changes (retry failed test) + 4. Handle current FRID testing (incremental phases) + 5. Handle regression testing (all earlier FRIDs) + 6. Detect completion + """ + + ctx = self.conformance_tests_running_context - if self.conformance_tests_running_context.current_conformance_tests_exist(): - self.machine.dispatch(triggers.MARK_CONFORMANCE_TESTS_READY) + # ========== STEP 1: Handle Test Regeneration ========== + if ctx.regenerating_conformance_tests: + self._handle_test_regeneration() + return + + # ========== STEP 2: Test Passed After Retry - Transition to Regression ========== + # This happens when MOVE_TO_NEXT_CONFORMANCE_TEST is triggered after a retry succeeds + if ( + ctx.execution_phase == TestExecutionPhase.RETRYING_AFTER_CODE_CHANGE + and ctx.test_that_triggered_code_change is not None + ): + # The test that had code changes has now passed + self._on_conformance_test_passed_after_retry() + # Fall through to handle regression + + # ========== STEP 3: Handle Code Changes (Retry) ========== + if ctx.execution_phase == TestExecutionPhase.RETRYING_AFTER_CODE_CHANGE: + self._handle_retry_after_code_change() + return + + # ========== STEP 3: Handle Current FRID Testing ========== + if self._should_run_current_frid_tests(): + self._handle_current_frid_testing() + return + + # ========== STEP 4: Handle Regression Testing ========== + if ctx.execution_phase == TestExecutionPhase.RUNNING_REGRESSION: + self._handle_regression_testing() + return + + # ========== STEP 5: Completion ========== + if ctx.execution_phase == TestExecutionPhase.COMPLETED: + self.machine.dispatch(triggers.MARK_ALL_CONFORMANCE_TESTS_PASSED) + return + + # Should never reach here + raise RuntimeError(f"Unexpected execution phase: {ctx.execution_phase}") def start_fixing_conformance_tests(self): self.conformance_tests_running_context.fix_attempts += 1 diff --git a/render_machine/render_types.py b/render_machine/render_types.py index d0d7a4fc..a8bbd478 100644 --- a/render_machine/render_types.py +++ b/render_machine/render_types.py @@ -1,9 +1,42 @@ from dataclasses import dataclass, field +from enum import Enum, auto from typing import Any, Optional import plain_spec +class TestExecutionPhase(Enum): + """Explicit phases of conformance test execution.""" + + # Initial testing of current FRID + TESTING_CURRENT_FRID = auto() + + # Running regression tests on earlier FRIDs + RUNNING_REGRESSION = auto() + + # Re-running a specific test that failed after code change + RETRYING_AFTER_CODE_CHANGE = auto() + + # All tests passed + COMPLETED = auto() + + +class AcceptanceTestPhase(Enum): + """Phases for incremental acceptance test execution.""" + + # No acceptance tests have been run yet + NOT_STARTED = auto() + + # Currently running acceptance tests incrementally + IN_PROGRESS = auto() + + # All acceptance tests completed + COMPLETED = auto() + + # No acceptance tests defined for this FRID + NOT_APPLICABLE = auto() + + @dataclass class FridContext: frid: str @@ -30,11 +63,11 @@ def __init__( conformance_tests_json: dict, conformance_tests_render_attempts: int, current_testing_frid_specifications: Optional[dict[str, list]], - conformance_test_phase_index: int, should_prepare_testing_environment: bool, conflicting_requirement_count: int = 0, conflicting_module_name: Optional[str] = None, conflicting_frid: Optional[str] = None, + frid_being_implemented: Optional[str] = None, ): self.current_testing_module_name = current_testing_module_name self.current_testing_frid = current_testing_frid @@ -42,15 +75,20 @@ def __init__( self._conformance_tests_json = {current_testing_module_name: conformance_tests_json} self.conformance_tests_render_attempts = conformance_tests_render_attempts self.current_testing_frid_specifications = current_testing_frid_specifications - self.conformance_test_phase_index = conformance_test_phase_index self.should_prepare_testing_environment = should_prepare_testing_environment self.conflicting_requirement_count = conflicting_requirement_count self.conflicting_module_name = conflicting_module_name self.conflicting_frid = conflicting_frid - # will be propagated only when: - # - current_testing_frid == frid noqa: E800 - # - conformance_test_phase_index == 0 (conformance tests phase) + + self.execution_phase: TestExecutionPhase = TestExecutionPhase.TESTING_CURRENT_FRID + self.acceptance_test_phase: AcceptanceTestPhase = AcceptanceTestPhase.NOT_STARTED + self.acceptance_tests_completed: int = 0 + self.frid_being_implemented: Optional[str] = frid_being_implemented + self.test_that_triggered_code_change: Optional[tuple[str, str]] = None + self.code_changed_during_regression: bool = False + self.regenerating_conformance_tests: bool = False + self.current_testing_frid_high_level_implementation_plan: Optional[str] = None self.previous_conformance_tests_issue_old: Optional[str] = None self.previous_conformance_tests_issue_frid: Optional[str] = None @@ -89,9 +127,12 @@ def get_current_acceptance_tests(self) -> Optional[list[str]]: def get_current_acceptance_test(self) -> Optional[str]: """Get the current acceptance test text (raw, unformatted).""" - return self.current_testing_frid_specifications[plain_spec.ACCEPTANCE_TESTS][ - self.conformance_test_phase_index - 1 - ] + if plain_spec.ACCEPTANCE_TESTS not in self.current_testing_frid_specifications: + return None + acceptance_tests = self.current_testing_frid_specifications[plain_spec.ACCEPTANCE_TESTS] + if not acceptance_tests or self.acceptance_tests_completed == 0: + return None + return acceptance_tests[self.acceptance_tests_completed - 1] def set_conformance_tests_summary(self, summary: list[dict]): self.get_conformance_tests_json(self.current_testing_module_name)[self.current_testing_frid][ @@ -103,7 +144,9 @@ def get_context_summary(self) -> dict: "current_testing_module_name": self.current_testing_module_name, "current_testing_frid": self.current_testing_frid, "fix_attempts": self.fix_attempts, - "conformance_test_phase_index": self.conformance_test_phase_index, + "execution_phase": self.execution_phase.name, + "acceptance_test_phase": self.acceptance_test_phase.name, + "acceptance_tests_completed": self.acceptance_tests_completed, } diff --git a/tui/state_handlers.py b/tui/state_handlers.py index 8b3aa43d..7f74c137 100644 --- a/tui/state_handlers.py +++ b/tui/state_handlers.py @@ -4,6 +4,7 @@ from typing import Optional from plain2code_events import RenderContextSnapshot +from render_machine.render_types import AcceptanceTestPhase from render_machine.states import States from . import components as tui_components @@ -19,15 +20,17 @@ ) -def format_acceptance_test_text(raw_text: str) -> str: +def format_acceptance_test_text(raw_text: str | None) -> str: """Format acceptance test text for display by removing list prefix if present. Args: - raw_text: The raw acceptance test text from specifications + raw_text: The raw acceptance test text from specifications (can be None during state transitions) Returns: - Formatted text with "- " prefix removed if present + Formatted text with "- " prefix removed if present, or empty string if None """ + if raw_text is None: + return "" if raw_text.startswith("- "): return raw_text[2:] return raw_text @@ -194,7 +197,9 @@ def handle(self, segments: list[str], snapshot: RenderContextSnapshot, previous_ if segments[2] != States.POSTPROCESSING_CONFORMANCE_TESTS.value: if segments[2] == States.CONFORMANCE_TESTING_INITIALISED.value: - if snapshot.conformance_tests_running_context.conformance_test_phase_index == 0: + phase = snapshot.conformance_tests_running_context.acceptance_test_phase + # Rendering full conformance tests (if there are no acceptance tests available or haven't been started yet) + if phase == AcceptanceTestPhase.NOT_STARTED or phase == AcceptanceTestPhase.NOT_APPLICABLE: rendering_text = f"Rendering conformance tests for functionality {snapshot.conformance_tests_running_context.current_testing_frid}" update_progress_item_substates( self.tui,