Back to Repositories

Testing Rich Text Highlighting Patterns in Textualize/rich

This test suite validates highlighting functionality in the Rich library, focusing on syntax highlighting for various data formats including JSON, ISO8601 dates, and Python representations. It ensures proper text styling and formatting across different input patterns.

Test Coverage Overview

The test suite provides comprehensive coverage of highlighting patterns for different data types and formats.

Key areas tested include:
  • JSON data structure highlighting
  • ISO8601 date/time format parsing
  • Python repr() output formatting
  • Edge cases like empty strings and complex numbers
Integration points include text styling, span management, and regex pattern matching.

Implementation Analysis

Testing approach uses pytest’s parametrize feature for data-driven testing of highlighting patterns. The implementation validates both the highlighting logic and the specific styling rules through detailed regex pattern matching and span verification.

Key patterns include:
  • Fixture-based test data
  • Parametrized test cases
  • Assertion-based validation

Technical Details

Testing tools and configuration:
  • pytest framework for test execution
  • Rich library’s Text and Span classes
  • Custom highlighter classes (JSONHighlighter, ISO8601Highlighter, etc.)
  • Regular expressions for pattern matching
  • Type hints and annotations

Best Practices Demonstrated

The test suite demonstrates several testing best practices including comprehensive edge case coverage, clear test organization, and thorough validation of formatting rules.

Notable practices:
  • Modular test organization
  • Extensive use of parametrized testing
  • Clear separation of test cases
  • Thorough validation of output formats

textualize/rich

tests/test_highlighter.py

            
"""Tests for the highlighter classes."""
import json
from typing import List

import pytest

from rich.highlighter import (
    ISO8601Highlighter,
    JSONHighlighter,
    NullHighlighter,
    ReprHighlighter,
)
from rich.text import Span, Text


def test_wrong_type():
    highlighter = NullHighlighter()
    with pytest.raises(TypeError):
        highlighter([])


highlight_tests = [
    ("", []),
    (" ", []),
    (
        "<foo>",
        [
            Span(0, 1, "repr.tag_start"),
            Span(1, 4, "repr.tag_name"),
            Span(4, 5, "repr.tag_end"),
        ],
    ),
    (
        "<foo: 23>",
        [
            Span(0, 1, "repr.tag_start"),
            Span(1, 5, "repr.tag_name"),
            Span(5, 8, "repr.tag_contents"),
            Span(8, 9, "repr.tag_end"),
            Span(6, 8, "repr.number"),
        ],
    ),
    (
        "<foo: <bar: 23>>",
        [
            Span(0, 1, "repr.tag_start"),
            Span(1, 5, "repr.tag_name"),
            Span(5, 15, "repr.tag_contents"),
            Span(15, 16, "repr.tag_end"),
            Span(12, 14, "repr.number"),
        ],
    ),
    (
        "False True None",
        [
            Span(0, 5, "repr.bool_false"),
            Span(6, 10, "repr.bool_true"),
            Span(11, 15, "repr.none"),
        ],
    ),
    ("foo=bar", [Span(0, 3, "repr.attrib_name"), Span(4, 7, "repr.attrib_value")]),
    (
        'foo="bar"',
        [
            Span(0, 3, "repr.attrib_name"),
            Span(4, 9, "repr.attrib_value"),
            Span(4, 9, "repr.str"),
        ],
    ),
    (
        "<Permission.WRITE|READ: 3>",
        [
            Span(0, 1, "repr.tag_start"),
            Span(1, 23, "repr.tag_name"),
            Span(23, 25, "repr.tag_contents"),
            Span(25, 26, "repr.tag_end"),
            Span(24, 25, "repr.number"),
        ],
    ),
    ("( )", [Span(0, 1, "repr.brace"), Span(2, 3, "repr.brace")]),
    ("[ ]", [Span(0, 1, "repr.brace"), Span(2, 3, "repr.brace")]),
    ("{ }", [Span(0, 1, "repr.brace"), Span(2, 3, "repr.brace")]),
    (" 1 ", [Span(1, 2, "repr.number")]),
    (" 1.2 ", [Span(1, 4, "repr.number")]),
    (" 0xff ", [Span(1, 5, "repr.number")]),
    (" 1e10 ", [Span(1, 5, "repr.number")]),
    (" 1j ", [Span(1, 3, "repr.number_complex")]),
    (" 3.14j ", [Span(1, 6, "repr.number_complex")]),
    (
        " (3.14+2.06j) ",
        [
            Span(1, 2, "repr.brace"),
            Span(12, 13, "repr.brace"),
            Span(2, 12, "repr.number_complex"),
        ],
    ),
    (
        " (3+2j) ",
        [
            Span(1, 2, "repr.brace"),
            Span(6, 7, "repr.brace"),
            Span(2, 6, "repr.number_complex"),
        ],
    ),
    (
        " (123456.4321-1234.5678j) ",
        [
            Span(1, 2, "repr.brace"),
            Span(24, 25, "repr.brace"),
            Span(2, 24, "repr.number_complex"),
        ],
    ),
    (
        " (-123123-2.1312342342423422e+25j) ",
        [
            Span(1, 2, "repr.brace"),
            Span(33, 34, "repr.brace"),
            Span(2, 33, "repr.number_complex"),
        ],
    ),
    (" /foo ", [Span(1, 2, "repr.path"), Span(2, 5, "repr.filename")]),
    (" /foo/bar.html ", [Span(1, 6, "repr.path"), Span(6, 14, "repr.filename")]),
    ("01-23-45-67-89-AB", [Span(0, 17, "repr.eui48")]),  # 6x2 hyphen
    ("01-23-45-FF-FE-67-89-AB", [Span(0, 23, "repr.eui64")]),  # 8x2 hyphen
    ("01:23:45:67:89:AB", [Span(0, 17, "repr.ipv6")]),  # 6x2 colon
    ("01:23:45:FF:FE:67:89:AB", [Span(0, 23, "repr.ipv6")]),  # 8x2 colon
    ("0123.4567.89AB", [Span(0, 14, "repr.eui48")]),  # 3x4 dot
    ("0123.45FF.FE67.89AB", [Span(0, 19, "repr.eui64")]),  # 4x4 dot
    ("ed-ed-ed-ed-ed-ed", [Span(0, 17, "repr.eui48")]),  # lowercase
    ("ED-ED-ED-ED-ED-ED", [Span(0, 17, "repr.eui48")]),  # uppercase
    ("Ed-Ed-Ed-Ed-Ed-Ed", [Span(0, 17, "repr.eui48")]),  # mixed case
    ("0-00-1-01-2-02", [Span(0, 14, "repr.eui48")]),  # dropped zero
    (" https://example.org ", [Span(1, 20, "repr.url")]),
    (" http://example.org ", [Span(1, 19, "repr.url")]),
    (" http://example.org/index.html ", [Span(1, 30, "repr.url")]),
    (" http://example.org/index.html#anchor ", [Span(1, 37, "repr.url")]),
    ("https://www.youtube.com/@LinusTechTips", [Span(0, 38, "repr.url")]),
    (
        " http://example.org/index.html?param1=value1 ",
        [
            Span(31, 37, "repr.attrib_name"),
            Span(38, 44, "repr.attrib_value"),
            Span(1, 44, "repr.url"),
        ],
    ),
    (" http://example.org/~folder ", [Span(1, 27, "repr.url")]),
    ("No place like 127.0.0.1", [Span(14, 23, "repr.ipv4")]),
    ("''", [Span(0, 2, "repr.str")]),
    ("'hello'", [Span(0, 7, "repr.str")]),
    ("'''hello'''", [Span(0, 11, "repr.str")]),
    ('""', [Span(0, 2, "repr.str")]),
    ('"hello"', [Span(0, 7, "repr.str")]),
    ('"""hello"""', [Span(0, 11, "repr.str")]),
    ("\\'foo'", []),
    ("it's no 'string'", [Span(8, 16, "repr.str")]),
    ("78351748-9b32-4e08-ad3e-7e9ff124d541", [Span(0, 36, "repr.uuid")]),
]


@pytest.mark.parametrize("test, spans", highlight_tests)
def test_highlight_regex(test: str, spans: List[Span]):
    """Tests for the regular expressions used in ReprHighlighter."""
    text = Text(test)
    highlighter = ReprHighlighter()
    highlighter.highlight(text)
    print(text.spans)
    assert text.spans == spans


def test_highlight_json_with_indent():
    json_string = json.dumps({"name": "apple", "count": 1}, indent=4)
    text = Text(json_string)
    highlighter = JSONHighlighter()
    highlighter.highlight(text)
    assert text.spans == [
        Span(0, 1, "json.brace"),
        Span(6, 12, "json.str"),
        Span(14, 21, "json.str"),
        Span(27, 34, "json.str"),
        Span(36, 37, "json.number"),
        Span(38, 39, "json.brace"),
        Span(6, 12, "json.key"),
        Span(27, 34, "json.key"),
    ]


def test_highlight_json_string_only():
    json_string = '"abc"'
    text = Text(json_string)
    highlighter = JSONHighlighter()
    highlighter.highlight(text)
    assert text.spans == [Span(0, 5, "json.str")]


def test_highlight_json_empty_string_only():
    json_string = '""'
    text = Text(json_string)
    highlighter = JSONHighlighter()
    highlighter.highlight(text)
    assert text.spans == [Span(0, 2, "json.str")]


def test_highlight_json_no_indent():
    json_string = json.dumps({"name": "apple", "count": 1}, indent=None)
    text = Text(json_string)
    highlighter = JSONHighlighter()
    highlighter.highlight(text)
    assert text.spans == [
        Span(0, 1, "json.brace"),
        Span(1, 7, "json.str"),
        Span(9, 16, "json.str"),
        Span(18, 25, "json.str"),
        Span(27, 28, "json.number"),
        Span(28, 29, "json.brace"),
        Span(1, 7, "json.key"),
        Span(18, 25, "json.key"),
    ]


iso8601_highlight_tests = [
    ("2008-08", [Span(0, 4, "iso8601.year"), Span(5, 7, "iso8601.month")]),
    (
        "2008-08-30",
        [
            Span(0, 10, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(5, 7, "iso8601.month"),
            Span(8, 10, "iso8601.day"),
        ],
    ),
    (
        "20080830",
        [
            Span(0, 8, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(4, 6, "iso8601.month"),
            Span(6, 8, "iso8601.day"),
        ],
    ),
    (
        "2008-243",
        [
            Span(0, 8, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(5, 8, "iso8601.day"),
        ],
    ),
    (
        "2008243",
        [
            Span(0, 7, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(4, 7, "iso8601.day"),
        ],
    ),
    (
        "2008-W35",
        [
            Span(0, 8, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(6, 8, "iso8601.week"),
        ],
    ),
    (
        "2008W35",
        [
            Span(0, 7, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(5, 7, "iso8601.week"),
        ],
    ),
    (
        "2008-W35-6",
        [
            Span(0, 10, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(6, 8, "iso8601.week"),
            Span(9, 10, "iso8601.day"),
        ],
    ),
    (
        "2008W356",
        [
            Span(0, 8, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(5, 7, "iso8601.week"),
            Span(7, 8, "iso8601.day"),
        ],
    ),
    (
        "17:21",
        [
            Span(0, 5, "iso8601.time"),
            Span(0, 2, "iso8601.hour"),
            Span(3, 5, "iso8601.minute"),
        ],
    ),
    (
        "1721",
        [
            Span(0, 4, "iso8601.time"),
            Span(0, 2, "iso8601.hour"),
            Span(2, 4, "iso8601.minute"),
        ],
    ),
    (
        "172159",
        [
            Span(0, 6, "iso8601.time"),
            Span(0, 2, "iso8601.hour"),
            Span(2, 4, "iso8601.minute"),
            Span(4, 6, "iso8601.second"),
        ],
    ),
    ("Z", [Span(0, 1, "iso8601.timezone")]),
    ("+07", [Span(0, 3, "iso8601.timezone")]),
    ("+07:00", [Span(0, 6, "iso8601.timezone")]),
    (
        "17:21:59+07:00",
        [
            Span(0, 8, "iso8601.time"),
            Span(0, 2, "iso8601.hour"),
            Span(3, 5, "iso8601.minute"),
            Span(6, 8, "iso8601.second"),
            Span(8, 14, "iso8601.timezone"),
        ],
    ),
    (
        "172159+0700",
        [
            Span(0, 6, "iso8601.time"),
            Span(0, 2, "iso8601.hour"),
            Span(2, 4, "iso8601.minute"),
            Span(4, 6, "iso8601.second"),
            Span(6, 11, "iso8601.timezone"),
        ],
    ),
    (
        "172159+07",
        [
            Span(0, 6, "iso8601.time"),
            Span(0, 2, "iso8601.hour"),
            Span(2, 4, "iso8601.minute"),
            Span(4, 6, "iso8601.second"),
            Span(6, 9, "iso8601.timezone"),
        ],
    ),
    (
        "2008-08-30 17:21:59",
        [
            Span(0, 10, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(4, 5, "iso8601.hyphen"),
            Span(5, 7, "iso8601.month"),
            Span(8, 10, "iso8601.day"),
            Span(11, 19, "iso8601.time"),
            Span(11, 13, "iso8601.hour"),
            Span(14, 16, "iso8601.minute"),
            Span(17, 19, "iso8601.second"),
        ],
    ),
    (
        "20080830 172159",
        [
            Span(0, 8, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(4, 6, "iso8601.month"),
            Span(6, 8, "iso8601.day"),
            Span(9, 15, "iso8601.time"),
            Span(9, 11, "iso8601.hour"),
            Span(11, 13, "iso8601.minute"),
            Span(13, 15, "iso8601.second"),
        ],
    ),
    (
        "2008-08-30",
        [
            Span(0, 10, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(5, 7, "iso8601.month"),
            Span(8, 10, "iso8601.day"),
        ],
    ),
    (
        "2008-08-30+07:00",
        [
            Span(0, 10, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(5, 7, "iso8601.month"),
            Span(8, 10, "iso8601.day"),
            Span(10, 16, "iso8601.timezone"),
        ],
    ),
    (
        "01:45:36",
        [
            Span(0, 8, "iso8601.time"),
            Span(0, 2, "iso8601.hour"),
            Span(3, 5, "iso8601.minute"),
            Span(6, 8, "iso8601.second"),
        ],
    ),
    (
        "01:45:36.123+07:00",
        [
            Span(0, 12, "iso8601.time"),
            Span(0, 2, "iso8601.hour"),
            Span(3, 5, "iso8601.minute"),
            Span(6, 8, "iso8601.second"),
            Span(8, 12, "iso8601.frac"),
            Span(12, 18, "iso8601.timezone"),
        ],
    ),
    (
        "01:45:36.123+07:00",
        [
            Span(0, 12, "iso8601.time"),
            Span(0, 2, "iso8601.hour"),
            Span(3, 5, "iso8601.minute"),
            Span(6, 8, "iso8601.second"),
            Span(8, 12, "iso8601.frac"),
            Span(12, 18, "iso8601.timezone"),
        ],
    ),
    (
        "2008-08-30T01:45:36",
        [
            Span(0, 10, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(5, 7, "iso8601.month"),
            Span(8, 10, "iso8601.day"),
            Span(11, 19, "iso8601.time"),
            Span(11, 13, "iso8601.hour"),
            Span(14, 16, "iso8601.minute"),
            Span(17, 19, "iso8601.second"),
        ],
    ),
    (
        "2008-08-30T01:45:36.123Z",
        [
            Span(0, 10, "iso8601.date"),
            Span(0, 4, "iso8601.year"),
            Span(5, 7, "iso8601.month"),
            Span(8, 10, "iso8601.day"),
            Span(11, 23, "iso8601.time"),
            Span(11, 13, "iso8601.hour"),
            Span(14, 16, "iso8601.minute"),
            Span(17, 19, "iso8601.second"),
            Span(19, 23, "iso8601.ms"),
            Span(23, 24, "iso8601.timezone"),
        ],
    ),
]


@pytest.mark.parametrize("test, spans", iso8601_highlight_tests)
def test_highlight_iso8601_regex(test: str, spans: List[Span]):
    """Tests for the regular expressions used in ISO8601Highlighter."""
    text = Text(test)
    highlighter = ISO8601Highlighter()
    highlighter.highlight(text)
    print(text.spans)
    assert text.spans == spans