Back to Repositories

Testing Live Display Implementation in Textualize/Rich

This test suite validates the Live display functionality in the Rich library, focusing on dynamic console output updates and terminal rendering. It covers various display modes, overflow handling, and console redirection scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of Rich’s Live display component, including:

  • Live display state management and lifecycle
  • Growing display with various overflow modes (ellipsis, crop, visible)
  • Console redirection and file output handling
  • Screen mode rendering and terminal control sequences

Implementation Analysis

Tests utilize pytest fixtures and context managers to validate the Live display functionality. The implementation focuses on terminal control sequences, display updates, and proper cleanup of display state. Key patterns include context manager usage for resource management and console capture for output verification.

Technical Details

Testing tools and configuration:

  • pytest for test framework
  • Rich Console capture for output verification
  • Custom console configuration for width/height control
  • ANSI escape sequence validation
  • Threading support for auto-refresh testing

Best Practices Demonstrated

The test suite demonstrates several testing best practices:

  • Isolated test cases with clear scope
  • Comprehensive edge case coverage
  • Proper resource cleanup
  • Explicit output validation
  • Controlled test environment configuration

textualize/rich

tests/test_live.py

            
# encoding=utf-8
import time
from typing import Optional

# import pytest
from rich.console import Console
from rich.live import Live
from rich.text import Text


def create_capture_console(
    *, width: int = 60, height: int = 80, force_terminal: Optional[bool] = True
) -> Console:
    return Console(
        width=width,
        height=height,
        force_terminal=force_terminal,
        legacy_windows=False,
        color_system=None,  # use no color system to reduce complexity of output,
        _environ={},
    )


def test_live_state() -> None:
    with Live("") as live:
        assert live._started
        live.start()

        assert live.renderable == ""

        assert live._started
        live.stop()
        assert not live._started

    assert not live._started


def test_growing_display() -> None:
    console = create_capture_console()
    console.begin_capture()
    with Live(console=console, auto_refresh=False) as live:
        display = ""
        for step in range(10):
            display += f"Step {step}
"
            live.update(display, refresh=True)
    output = console.end_capture()
    print(repr(output))
    assert (
        output
        == "\x1b[?25lStep 0
\r\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
Step 9
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
Step 9

\x1b[?25h"
    )


def test_growing_display_transient() -> None:
    console = create_capture_console()
    console.begin_capture()
    with Live(console=console, auto_refresh=False, transient=True) as live:
        display = ""
        for step in range(10):
            display += f"Step {step}
"
            live.update(display, refresh=True)
    output = console.end_capture()
    assert (
        output
        == "\x1b[?25lStep 0
\r\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
Step 9
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
Step 9

\x1b[?25h\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
    )


def test_growing_display_overflow_ellipsis() -> None:
    console = create_capture_console(height=5)
    console.begin_capture()
    with Live(
        console=console, auto_refresh=False, vertical_overflow="ellipsis"
    ) as live:
        display = ""
        for step in range(10):
            display += f"Step {step}
"
            live.update(display, refresh=True)
    output = console.end_capture()
    assert (
        output
        == "\x1b[?25lStep 0
\r\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
                            ...                             \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
                            ...                             \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
                            ...                             \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
                            ...                             \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
                            ...                             \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
                            ...                             \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
Step 9

\x1b[?25h"
    )


def test_growing_display_overflow_crop() -> None:
    console = create_capture_console(height=5)
    console.begin_capture()
    with Live(console=console, auto_refresh=False, vertical_overflow="crop") as live:
        display = ""
        for step in range(10):
            display += f"Step {step}
"
            live.update(display, refresh=True)
    output = console.end_capture()
    assert (
        output
        == "\x1b[?25lStep 0
\r\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
Step 9

\x1b[?25h"
    )


def test_growing_display_overflow_visible() -> None:
    console = create_capture_console(height=5)
    console.begin_capture()
    with Live(console=console, auto_refresh=False, vertical_overflow="visible") as live:
        display = ""
        for step in range(10):
            display += f"Step {step}
"
            live.update(display, refresh=True)
    output = console.end_capture()
    assert (
        output
        == "\x1b[?25lStep 0
\r\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
Step 9
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
Step 9

\x1b[?25h"
    )


def test_growing_display_autorefresh() -> None:
    """Test generating a table but using auto-refresh from threading"""
    console = create_capture_console(height=5)
    console.begin_capture()
    with Live(console=console, auto_refresh=True, vertical_overflow="visible") as live:
        display = ""
        for step in range(10):
            display += f"Step {step}
"
            live.update(display)
            time.sleep(0.2)

    # no way to truly test w/ multithreading, just make sure it doesn't crash


def test_growing_display_console_redirect() -> None:
    console = create_capture_console()
    console.begin_capture()
    with Live(console=console, auto_refresh=False) as live:
        display = ""
        for step in range(10):
            console.print(f"Running step {step}")
            display += f"Step {step}
"
            live.update(display, refresh=True)
    output = console.end_capture()
    assert (
        output
        == "\x1b[?25lRunning step 0
\r\x1b[2KStep 0
\r\x1b[2K\x1b[1A\x1b[2KRunning step 1
Step 0
\r\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 2
Step 0
Step 1
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 3
Step 0
Step 1
Step 2
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 4
Step 0
Step 1
Step 2
Step 3
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 5
Step 0
Step 1
Step 2
Step 3
Step 4
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 6
Step 0
Step 1
Step 2
Step 3
Step 4
Step 5
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 7
Step 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 8
Step 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 9
Step 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
Step 9
\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
Step 9

\x1b[?25h"
    )


def test_growing_display_file_console() -> None:
    console = create_capture_console(force_terminal=False)
    console.begin_capture()
    with Live(console=console, auto_refresh=False) as live:
        display = ""
        for step in range(10):
            display += f"Step {step}
"
            live.update(display, refresh=True)
    output = console.end_capture()
    assert (
        output
        == "Step 0
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Step 7
Step 8
Step 9
"
    )


def test_live_screen() -> None:
    console = create_capture_console(width=20, height=5)
    console.begin_capture()
    with Live(Text("foo"), screen=True, console=console, auto_refresh=False) as live:
        live.refresh()
    result = console.end_capture()
    print(repr(result))
    expected = "\x1b[?1049h\x1b[H\x1b[?25l\x1b[Hfoo                 
                    
                    
                    
                    \x1b[Hfoo                 
                    
                    
                    
                    \x1b[?25h\x1b[?1049l"
    assert result == expected