diff --git a/python/gvtest/live_display.py b/python/gvtest/live_display.py index 7fd498d..da8b813 100644 --- a/python/gvtest/live_display.py +++ b/python/gvtest/live_display.py @@ -98,8 +98,11 @@ def stop(self) -> None: def test_started( self, test_id: int, name: str, config: str ) -> None: - """No-op for progress mode (no running panel).""" - pass + """Log START message above the progress bar.""" + self.log( + f"[blue]{'START'.ljust(8)}[/blue]" + f"[bold]{name}[/bold] {config}" + ) def test_finished( self, test_id: int, status: str diff --git a/python/gvtest/runner.py b/python/gvtest/runner.py index d226708..9f0bacf 100644 --- a/python/gvtest/runner.py +++ b/python/gvtest/runner.py @@ -255,7 +255,7 @@ def run(self) -> None: from gvtest.live_display import LiveDisplay from rich.console import Console self.live_display = LiveDisplay( - Console(highlight=False) + Console(highlight=False, stderr=True) ) # Start with 0, update total after enqueue self.live_display.start(0) diff --git a/python/gvtest/tests.py b/python/gvtest/tests.py index 657099d..ac3b18b 100644 --- a/python/gvtest/tests.py +++ b/python/gvtest/tests.py @@ -40,7 +40,7 @@ from rich.console import Console from rich.table import Table -_console = Console(highlight=False) +_console = Console(highlight=False, stderr=True) class TestRun(object): @@ -268,36 +268,28 @@ def __exec_process(self, command: str, envvars: dict[str, str] | None = None) -> if envvars is not None: env.update(envvars) - # Use a PTY to isolate terminal state — prevents - # tests from corrupting the parent terminal - # (e.g. raw mode, no-echo from SDL2/ncurses) - master_fd, slave_fd = os.openpty() - + # Use pipes + start_new_session for isolation. + # The new session prevents child processes from + # corrupting the parent terminal (SDL2/ncurses), + # and os.killpg() can kill the whole group. proc: subprocess.Popen[bytes] = subprocess.Popen( - command, stdout=slave_fd, - stderr=slave_fd, stdin=slave_fd, + command, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, shell=True, cwd=self.test.path, env=env, start_new_session=True ) - # Close slave in parent — only the child uses it - os.close(slave_fd) - self.current_proc = proc self.lock.release() - # Read from master fd - master_file = os.fdopen(master_fd, 'rb') - try: - for line in io.TextIOWrapper( - master_file, encoding="utf-8", - errors='replace' - ): - self.__dump_test_msg(line) - except IOError: - # PTY closed when process exits - pass + assert proc.stdout is not None + for line in io.TextIOWrapper( + proc.stdout, encoding="utf-8", + errors='replace' + ): + self.__dump_test_msg(line) retval: int = proc.wait() self.current_proc = None diff --git a/python/gvtest/tui.py b/python/gvtest/tui.py index 962dc5d..1834af1 100644 --- a/python/gvtest/tui.py +++ b/python/gvtest/tui.py @@ -198,7 +198,7 @@ def run(self, stdscr: Any) -> None: curses.init_pair(3, curses.COLOR_GREEN, -1) curses.init_pair(4, curses.COLOR_YELLOW, -1) curses.init_pair(5, curses.COLOR_MAGENTA, -1) - curses.init_pair(6, curses.COLOR_CYAN, -1) + curses.init_pair(6, curses.COLOR_BLUE, -1) # Progress bar colors (background) curses.init_pair(7, curses.COLOR_BLACK, curses.COLOR_GREEN) @@ -601,6 +601,9 @@ def _draw_targets( completed / total * bar_w ) filled_w = max(1, min(filled_w, bar_w)) + # Ensure at least 1 empty char when not done + if completed < total and filled_w == bar_w: + filled_w = bar_w - 1 # Distribute within filled_w g = max(1, round( passed / completed * filled_w @@ -753,6 +756,10 @@ def _draw_progress( self.completed / self.total * bar_w ) filled_w = max(1, min(filled_w, bar_w)) + # Ensure at least 1 empty char when not done + if (self.completed < self.total + and filled_w == bar_w): + filled_w = bar_w - 1 g = max(1, round( self.passed / self.completed * filled_w )) if self.passed > 0 else 0