Skip to content
Snippets Groups Projects
test_table_creator.py 34.85 KiB
# coding=utf-8
# flake8: noqa E501
"""
Unit testing for cmd2/table_creator.py module
"""

import pytest

from cmd2 import (
    Bg,
    Fg,
    TextStyle,
    ansi,
)
from cmd2.table_creator import (
    AlternatingTable,
    BorderedTable,
    Column,
    HorizontalAlignment,
    SimpleTable,
    TableCreator,
    VerticalAlignment,
)

# Turn off black formatting for entire file so multiline strings
# can be visually aligned to match the tables being tested.
# fmt: off


def test_column_creation():
    # Width less than 1
    with pytest.raises(ValueError) as excinfo:
        Column("Column 1", width=0)
    assert "Column width cannot be less than 1" in str(excinfo.value)

    # Width specified
    c = Column("header", width=20)
    assert c.width == 20

    # max_data_lines less than 1
    with pytest.raises(ValueError) as excinfo:
        Column("Column 1", max_data_lines=0)
    assert "Max data lines cannot be less than 1" in str(excinfo.value)

    # No width specified, blank label
    c = Column("")
    assert c.width < 0
    tc = TableCreator([c])
    assert tc.cols[0].width == 1

    # No width specified, label isn't blank but has no width
    c = Column(ansi.style('', fg=Fg.GREEN))
    assert c.width < 0
    tc = TableCreator([c])
    assert tc.cols[0].width == 1

    # No width specified, label has width
    c = Column("a line")
    assert c.width < 0
    tc = TableCreator([c])
    assert tc.cols[0].width == ansi.style_aware_wcswidth("a line")

    # No width specified, label has width and multiple lines
    c = Column("short\nreally long")
    assert c.width < 0
    tc = TableCreator([c])
    assert tc.cols[0].width == ansi.style_aware_wcswidth("really long")

    # No width specified, label has tabs
    c = Column("line\twith\ttabs")
    assert c.width < 0
    tc = TableCreator([c])
    assert tc.cols[0].width == ansi.style_aware_wcswidth("line    with    tabs")

    # Add basic tests for style_header_text and style_data_text to make sure these members don't get removed.
    c = Column("Column 1")
    assert c.style_header_text is True
    assert c.style_data_text is True

    c = Column("Column 1", style_header_text=False)
    assert c.style_header_text is False
    assert c.style_data_text is True

    c = Column("Column 1", style_data_text=False)
    assert c.style_header_text is True
    assert c.style_data_text is False


def test_column_alignment():
    column_1 = Column(
        "Col 1",
        width=10,
        header_horiz_align=HorizontalAlignment.LEFT,
        header_vert_align=VerticalAlignment.TOP,
        data_horiz_align=HorizontalAlignment.RIGHT,
        data_vert_align=VerticalAlignment.BOTTOM,
    )
    column_2 = Column(
        "Col 2",
        width=10,
        header_horiz_align=HorizontalAlignment.RIGHT,
        header_vert_align=VerticalAlignment.BOTTOM,
        data_horiz_align=HorizontalAlignment.CENTER,
        data_vert_align=VerticalAlignment.MIDDLE,
    )
    column_3 = Column(
        "Col 3",
        width=10,
        header_horiz_align=HorizontalAlignment.CENTER,
        header_vert_align=VerticalAlignment.MIDDLE,
        data_horiz_align=HorizontalAlignment.LEFT,
        data_vert_align=VerticalAlignment.TOP,
    )
    column_4 = Column("Three\nline\nheader", width=10)

    columns = [column_1, column_2, column_3, column_4]
    tc = TableCreator(columns)

    # Check defaults
    assert column_4.header_horiz_align == HorizontalAlignment.LEFT
    assert column_4.header_vert_align == VerticalAlignment.BOTTOM
    assert column_4.data_horiz_align == HorizontalAlignment.LEFT
    assert column_4.data_vert_align == VerticalAlignment.TOP

    # Create a header row
    row_data = [col.header for col in columns]
    header = tc.generate_row(row_data=row_data, is_header=True)
    assert header == (
        'Col 1                               Three     \n'
        '                          Col 3     line      \n'
        '                 Col 2              header    '
    )

    # Create a data row
    row_data = ["Val 1", "Val 2", "Val 3", "Three\nline\ndata"]
    row = tc.generate_row(row_data=row_data, is_header=False)
    assert row == (
        '                        Val 3       Three     \n'
        '              Val 2                 line      \n'
        '     Val 1                          data      '
    )


def test_blank_last_line():
    """This tests that an empty line is inserted when the last data line is blank"""
    column_1 = Column("Col 1", width=10)
    tc = TableCreator([column_1])

    row_data = ['my line\n\n']
    row = tc.generate_row(row_data=row_data, is_header=False)
    assert row == ('my line   \n'
                   '          ')

    row_data = ['\n']
    row = tc.generate_row(row_data=row_data, is_header=False)
    assert row == '          '

    row_data = ['']
    row = tc.generate_row(row_data=row_data, is_header=False)
    assert row == '          '


def test_wrap_text():
    column_1 = Column("Col 1", width=10)
    tc = TableCreator([column_1])

    # Test normal wrapping
    row_data = ['Some text to wrap\nA new line that will wrap\nNot wrap\n 1  2   3']
    row = tc.generate_row(row_data=row_data, is_header=False)
    assert row == ('Some text \n'
                   'to wrap   \n'
                   'A new line\n'
                   'that will \n'
                   'wrap      \n'
                   'Not wrap  \n'
                   ' 1  2   3 ')

    # Test preserving a multiple space sequence across a line break
    row_data = ['First      last one']
    row = tc.generate_row(row_data=row_data, is_header=False)
    assert row == ('First     \n'
                   ' last one ')


def test_wrap_text_max_lines():
    column_1 = Column("Col 1", width=10, max_data_lines=2)
    tc = TableCreator([column_1])

    # Test not needing to truncate the final line
    row_data = ['First line last line']
    row = tc.generate_row(row_data=row_data, is_header=False)
    assert row == ('First line\n'
                   'last line ')

    # Test having to truncate the last word because it's too long for the final line
    row_data = ['First line last lineextratext']
    row = tc.generate_row(row_data=row_data, is_header=False)
    assert row == ('First line\n'
                   'last line…')

    # Test having to truncate the last word because it fits the final line but there is more text not being included
    row_data = ['First line thistxtfit extra']
    row = tc.generate_row(row_data=row_data, is_header=False)
    assert row == ('First line\n'
                   'thistxtfi…')

    # Test having to truncate the last word because it fits the final line but there are more lines not being included
    row_data = ['First line thistxtfit\nextra']
    row = tc.generate_row(row_data=row_data, is_header=False)
    assert row == ('First line\n'
                   'thistxtfi…')

    # Test having space left on the final line and adding an ellipsis because there are more lines not being included
    row_data = ['First line last line\nextra line']
    row = tc.generate_row(row_data=row_data, is_header=False)
    assert row == ('First line\n'
                   'last line…')


def test_wrap_long_word():
    # Make sure words wider than column start on own line and wrap
    column_1 = Column("LongColumnName", width=10)
    column_2 = Column("Col 2", width=10)

    columns = [column_1, column_2]
    tc = TableCreator(columns)

    # Test header row
    row_data = [col.header for col in columns]
    header = tc.generate_row(row_data, is_header=True)
    assert header == ('LongColumn            \n'
                      'Name        Col 2     ')

    # Test data row
    row_data = list()

    # Long word should start on the first line (style should not affect width)
    row_data.append(ansi.style("LongerThan10", fg=Fg.GREEN))

    # Long word should start on the second line
    row_data.append("Word LongerThan10")

    row = tc.generate_row(row_data=row_data, is_header=False)
    expected = (
            TextStyle.RESET_ALL
            + Fg.GREEN
            + "LongerThan"
            + TextStyle.RESET_ALL
            + "  Word      \n"
            + TextStyle.RESET_ALL
            + Fg.GREEN
            + "10"
            + Fg.RESET
            + TextStyle.RESET_ALL
            + '        '
            + TextStyle.RESET_ALL
            + '  LongerThan\n'
              '            10        '
    )
    assert row == expected


def test_wrap_long_word_max_data_lines():
    column_1 = Column("Col 1", width=10, max_data_lines=2)
    column_2 = Column("Col 2", width=10, max_data_lines=2)
    column_3 = Column("Col 3", width=10, max_data_lines=2)
    column_4 = Column("Col 4", width=10, max_data_lines=1)

    columns = [column_1, column_2, column_3, column_4]
    tc = TableCreator(columns)

    row_data = list()

    # This long word will exactly fit the last line and it's the final word in the text. No ellipsis should appear.
    row_data.append("LongerThan10FitsLast")

    # This long word will exactly fit the last line but it's not the final word in the text.
    # Make sure ellipsis word's final character.
    row_data.append("LongerThan10FitsLast\nMore lines")

    # This long word will run over the last line. Make sure it is truncated.
    row_data.append("LongerThan10RunsOverLast")

    # This long word will start on the final line after another word. Therefore it won't wrap but will instead be truncated.
    row_data.append("A LongerThan10RunsOverLast")

    row = tc.generate_row(row_data=row_data, is_header=False)
    assert row == ('LongerThan  LongerThan  LongerThan  A LongerT…\n'
                   '10FitsLast  10FitsLas…  10RunsOve…            ')


def test_wrap_long_char_wider_than_max_width():
    """
    This tests case where a character is wider than max_width. This can happen if max_width
    is 1 and the text contains wide characters (e.g. East Asian). Replace it with an ellipsis.
    """
    column_1 = Column("Col 1", width=1)
    tc = TableCreator([column_1])
    row = tc.generate_row(row_data=[''], is_header=False)
    assert row == ''


def test_generate_row_exceptions():
    column_1 = Column("Col 1")
    tc = TableCreator([column_1])
    row_data = ['fake']

    # fill_char too long
    with pytest.raises(TypeError) as excinfo:
        tc.generate_row(row_data=row_data, is_header=False, fill_char='too long')
    assert "Fill character must be exactly one character long" in str(excinfo.value)

    # Unprintable characters
    for arg in ['fill_char', 'pre_line', 'inter_cell', 'post_line']:
        kwargs = {arg: '\n'}
        with pytest.raises(ValueError) as excinfo:
            tc.generate_row(row_data=row_data, is_header=False, **kwargs)
        assert "{} contains an unprintable character".format(arg) in str(excinfo.value)

    # Data with too many columns
    row_data = ['Data 1', 'Extra Column']
    with pytest.raises(ValueError) as excinfo:
        tc.generate_row(row_data=row_data, is_header=False)
    assert "Length of row_data must match length of cols" in str(excinfo.value)


def test_tabs():
    column_1 = Column("Col\t1", width=20)
    column_2 = Column("Col 2")
    columns = [column_1, column_2]
    tc = TableCreator(columns, tab_width=2)

    row_data = [col.header for col in columns]
    row = tc.generate_row(row_data, is_header=True, fill_char='\t', pre_line='\t', inter_cell='\t', post_line='\t')
    assert row == '  Col  1                Col 2  '

    with pytest.raises(ValueError) as excinfo:
        TableCreator([column_1, column_2], tab_width=0)
    assert "Tab width cannot be less than 1" in str(excinfo.value)


def test_simple_table_creation():
    column_1 = Column("Col 1", width=16)
    column_2 = Column("Col 2", width=16)

    row_data = list()
    row_data.append(["Col 1 Row 1", "Col 2 Row 1"])
    row_data.append(["Col 1 Row 2", "Col 2 Row 2"])

    # Default options
    st = SimpleTable([column_1, column_2])
    table = st.generate_table(row_data)

    assert table == (
        'Col 1             Col 2           \n'
        '----------------------------------\n'
        'Col 1 Row 1       Col 2 Row 1     \n'
        '                                  \n'
        'Col 1 Row 2       Col 2 Row 2     '
    )

    # Custom column spacing
    st = SimpleTable([column_1, column_2], column_spacing=5)
    table = st.generate_table(row_data)

    assert table == (
        'Col 1                Col 2           \n'
        '-------------------------------------\n'
        'Col 1 Row 1          Col 2 Row 1     \n'
        '                                     \n'
        'Col 1 Row 2          Col 2 Row 2     '
    )

    # Custom divider
    st = SimpleTable([column_1, column_2], divider_char='')
    table = st.generate_table(row_data)

    assert table == (
        'Col 1             Col 2           \n'
        '──────────────────────────────────\n'
        'Col 1 Row 1       Col 2 Row 1     \n'
        '                                  \n'
        'Col 1 Row 2       Col 2 Row 2     '
    )

    # No divider
    st = SimpleTable([column_1, column_2], divider_char=None)
    no_divider_1 = st.generate_table(row_data)

    st = SimpleTable([column_1, column_2], divider_char='')
    no_divider_2 = st.generate_table(row_data)

    assert no_divider_1 == no_divider_2 == (
        'Col 1             Col 2           \n'
        'Col 1 Row 1       Col 2 Row 1     \n'
        '                                  \n'
        'Col 1 Row 2       Col 2 Row 2     '
    )

    # No row spacing
    st = SimpleTable([column_1, column_2])
    table = st.generate_table(row_data, row_spacing=0)
    assert table == (
        'Col 1             Col 2           \n'
        '----------------------------------\n'
        'Col 1 Row 1       Col 2 Row 1     \n'
        'Col 1 Row 2       Col 2 Row 2     '
    )

    # No header
    st = SimpleTable([column_1, column_2])
    table = st.generate_table(row_data, include_header=False)

    assert table == ('Col 1 Row 1       Col 2 Row 1     \n'
                     '                                  \n'
                     'Col 1 Row 2       Col 2 Row 2     ')

    # Wide custom divider (divider needs no padding)
    st = SimpleTable([column_1, column_2], divider_char='')
    table = st.generate_table(row_data)

    assert table == (
        'Col 1             Col 2           \n'
        '深深深深深深深深深深深深深深深深深\n'
        'Col 1 Row 1       Col 2 Row 1     \n'
        '                                  \n'
        'Col 1 Row 2       Col 2 Row 2     '
    )

    # Wide custom divider (divider needs padding)
    st = SimpleTable([column_1, Column("Col 2", width=17)],
                     divider_char='')
    table = st.generate_table(row_data)

    assert table == (
        'Col 1             Col 2            \n'
        '深深深深深深深深深深深深深深深深深 \n'
        'Col 1 Row 1       Col 2 Row 1      \n'
        '                                   \n'
        'Col 1 Row 2       Col 2 Row 2      '
    )

    # Invalid column spacing
    with pytest.raises(ValueError) as excinfo:
        SimpleTable([column_1, column_2], column_spacing=-1)
    assert "Column spacing cannot be less than 0" in str(excinfo.value)

    # Invalid divider character
    with pytest.raises(TypeError) as excinfo:
        SimpleTable([column_1, column_2], divider_char='too long')
    assert "Divider character must be exactly one character long" in str(excinfo.value)

    with pytest.raises(ValueError) as excinfo:
        SimpleTable([column_1, column_2], divider_char='\n')
    assert "Divider character is an unprintable character" in str(excinfo.value)

    # Invalid row spacing
    st = SimpleTable([column_1, column_2])
    with pytest.raises(ValueError) as excinfo:
        st.generate_table(row_data, row_spacing=-1)
    assert "Row spacing cannot be less than 0" in str(excinfo.value)

    # Test header and data colors
    st = SimpleTable([column_1, column_2], divider_char=None, header_bg=Bg.GREEN, data_bg=Bg.LIGHT_BLUE)
    table = st.generate_table(row_data)
    assert table == (
        '\x1b[0m\x1b[42mCol 1\x1b[49m\x1b[0m\x1b[42m           \x1b[49m\x1b[0m\x1b[42m  \x1b[49m\x1b[0m\x1b[42mCol 2\x1b[49m\x1b[0m\x1b[42m           \x1b[49m\x1b[0m\n'
        '\x1b[0m\x1b[104mCol 1 Row 1\x1b[49m\x1b[0m\x1b[104m     \x1b[49m\x1b[0m\x1b[104m  \x1b[49m\x1b[0m\x1b[104mCol 2 Row 1\x1b[49m\x1b[0m\x1b[104m     \x1b[49m\x1b[0m\n'
        '\x1b[0m\x1b[0m\x1b[104m                                  \x1b[49m\x1b[0m\n'
        '\x1b[0m\x1b[104mCol 1 Row 2\x1b[49m\x1b[0m\x1b[104m     \x1b[49m\x1b[0m\x1b[104m  \x1b[49m\x1b[0m\x1b[104mCol 2 Row 2\x1b[49m\x1b[0m\x1b[104m     \x1b[49m\x1b[0m'
    )

    # Make sure SimpleTable respects style_header_text and style_data_text flags.
    # Don't apply parent table's background colors to header or data text in second column.
    st = SimpleTable([column_1, Column("Col 2", width=16, style_header_text=False, style_data_text=False)],
                     divider_char=None, header_bg=Bg.GREEN, data_bg=Bg.LIGHT_BLUE)
    table = st.generate_table(row_data)
    assert table == (
        '\x1b[0m\x1b[42mCol 1\x1b[49m\x1b[0m\x1b[42m           \x1b[49m\x1b[0m\x1b[42m  \x1b[49m\x1b[0mCol 2\x1b[0m\x1b[42m           \x1b[49m\x1b[0m\n'
        '\x1b[0m\x1b[104mCol 1 Row 1\x1b[49m\x1b[0m\x1b[104m     \x1b[49m\x1b[0m\x1b[104m  \x1b[49m\x1b[0mCol 2 Row 1\x1b[0m\x1b[104m     \x1b[49m\x1b[0m\n'
        '\x1b[0m\x1b[0m\x1b[104m                                  \x1b[49m\x1b[0m\n'
        '\x1b[0m\x1b[104mCol 1 Row 2\x1b[49m\x1b[0m\x1b[104m     \x1b[49m\x1b[0m\x1b[104m  \x1b[49m\x1b[0mCol 2 Row 2\x1b[0m\x1b[104m     \x1b[49m\x1b[0m'
    )


def test_simple_table_width():
    # Base width
    for num_cols in range(1, 10):
        assert SimpleTable.base_width(num_cols) == (num_cols - 1) * 2

    # Invalid num_cols value
    with pytest.raises(ValueError) as excinfo:
        SimpleTable.base_width(0)
    assert "Column count cannot be less than 1" in str(excinfo.value)

    # Total width
    column_1 = Column("Col 1", width=16)
    column_2 = Column("Col 2", width=16)

    row_data = list()
    row_data.append(["Col 1 Row 1", "Col 2 Row 1"])
    row_data.append(["Col 1 Row 2", "Col 2 Row 2"])

    st = SimpleTable([column_1, column_2])
    assert st.total_width() == 34


def test_simple_generate_data_row_exceptions():
    column_1 = Column("Col 1")
    tc = SimpleTable([column_1])

    # Data with too many columns
    row_data = ['Data 1', 'Extra Column']
    with pytest.raises(ValueError) as excinfo:
        tc.generate_data_row(row_data=row_data)
    assert "Length of row_data must match length of cols" in str(excinfo.value)


def test_bordered_table_creation():
    column_1 = Column("Col 1", width=15)
    column_2 = Column("Col 2", width=15)

    row_data = list()
    row_data.append(["Col 1 Row 1", "Col 2 Row 1"])
    row_data.append(["Col 1 Row 2", "Col 2 Row 2"])

    # Default options
    bt = BorderedTable([column_1, column_2])
    table = bt.generate_table(row_data)
    assert table == (
        '╔═════════════════╤═════════════════╗\n'
        '║ Col 1           │ Col 2           ║\n'
        '╠═════════════════╪═════════════════╣\n'
        '║ Col 1 Row 1     │ Col 2 Row 1     ║\n'
        '╟─────────────────┼─────────────────╢\n'
        '║ Col 1 Row 2     │ Col 2 Row 2     ║\n'
        '╚═════════════════╧═════════════════╝'
    )

    # No column borders
    bt = BorderedTable([column_1, column_2], column_borders=False)
    table = bt.generate_table(row_data)
    assert table == (
        '╔══════════════════════════════════╗\n'
        '║ Col 1            Col 2           ║\n'
        '╠══════════════════════════════════╣\n'
        '║ Col 1 Row 1      Col 2 Row 1     ║\n'
        '╟──────────────────────────────────╢\n'
        '║ Col 1 Row 2      Col 2 Row 2     ║\n'
        '╚══════════════════════════════════╝'
    )

    # No header
    bt = BorderedTable([column_1, column_2])
    table = bt.generate_table(row_data, include_header=False)
    assert table == (
        '╔═════════════════╤═════════════════╗\n'
        '║ Col 1 Row 1     │ Col 2 Row 1     ║\n'
        '╟─────────────────┼─────────────────╢\n'
        '║ Col 1 Row 2     │ Col 2 Row 2     ║\n'
        '╚═════════════════╧═════════════════╝'
    )

    # Non-default padding
    bt = BorderedTable([column_1, column_2], padding=2)
    table = bt.generate_table(row_data)
    assert table == (
        '╔═══════════════════╤═══════════════════╗\n'
        '║  Col 1            │  Col 2            ║\n'
        '╠═══════════════════╪═══════════════════╣\n'
        '║  Col 1 Row 1      │  Col 2 Row 1      ║\n'
        '╟───────────────────┼───────────────────╢\n'
        '║  Col 1 Row 2      │  Col 2 Row 2      ║\n'
        '╚═══════════════════╧═══════════════════╝'
    )

    # Invalid padding
    with pytest.raises(ValueError) as excinfo:
        BorderedTable([column_1, column_2], padding=-1)
    assert "Padding cannot be less than 0" in str(excinfo.value)

    # Test border, header, and data colors
    bt = BorderedTable([column_1, column_2], border_fg=Fg.LIGHT_YELLOW, border_bg=Bg.WHITE,
                       header_bg=Bg.GREEN, data_bg=Bg.LIGHT_BLUE)
    table = bt.generate_table(row_data)
    assert table == (
        '\x1b[93m\x1b[107m╔═\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m═══════════════\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m═╤═\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m═══════════════\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m═╗\x1b[39m\x1b[49m\n'
        '\x1b[93m\x1b[107m║\x1b[39m\x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42mCol 1\x1b[49m\x1b[0m\x1b[42m          \x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[93m\x1b[107m│\x1b[39m\x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42mCol 2\x1b[49m\x1b[0m\x1b[42m          \x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[93m\x1b[107m║\x1b[39m\x1b[49m\n'
        '\x1b[93m\x1b[107m╠═\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m═══════════════\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m═╪═\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m═══════════════\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m═╣\x1b[39m\x1b[49m\n'
        '\x1b[93m\x1b[107m║\x1b[39m\x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104mCol 1 Row 1\x1b[49m\x1b[0m\x1b[104m    \x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[93m\x1b[107m│\x1b[39m\x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104mCol 2 Row 1\x1b[49m\x1b[0m\x1b[104m    \x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[93m\x1b[107m║\x1b[39m\x1b[49m\n'
        '\x1b[93m\x1b[107m╟─\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m───────────────\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m─┼─\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m───────────────\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m─╢\x1b[39m\x1b[49m\n'
        '\x1b[93m\x1b[107m║\x1b[39m\x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104mCol 1 Row 2\x1b[49m\x1b[0m\x1b[104m    \x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[93m\x1b[107m│\x1b[39m\x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104mCol 2 Row 2\x1b[49m\x1b[0m\x1b[104m    \x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[93m\x1b[107m║\x1b[39m\x1b[49m\n'
        '\x1b[93m\x1b[107m╚═\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m═══════════════\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m═╧═\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m═══════════════\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m═╝\x1b[39m\x1b[49m'
    )

    # Make sure BorderedTable respects style_header_text and style_data_text flags.
    # Don't apply parent table's background colors to header or data text in second column.
    bt = BorderedTable([column_1, Column("Col 2", width=15, style_header_text=False, style_data_text=False)],
                       header_bg=Bg.GREEN, data_bg=Bg.LIGHT_BLUE)
    table = bt.generate_table(row_data)
    assert table == (
        '╔═════════════════╤═════════════════╗\n'
        '\x1b[42m \x1b[49m\x1b[0m\x1b[42mCol 1\x1b[49m\x1b[0m\x1b[42m          \x1b[49m\x1b[0m\x1b[42m \x1b[49m│\x1b[42m \x1b[49m\x1b[0mCol 2\x1b[0m\x1b[42m          \x1b[49m\x1b[0m\x1b[42m \x1b[49m║\n'
        '╠═════════════════╪═════════════════╣\n'
        '\x1b[104m \x1b[49m\x1b[0m\x1b[104mCol 1 Row 1\x1b[49m\x1b[0m\x1b[104m    \x1b[49m\x1b[0m\x1b[104m \x1b[49m│\x1b[104m \x1b[49m\x1b[0mCol 2 Row 1\x1b[0m\x1b[104m    \x1b[49m\x1b[0m\x1b[104m \x1b[49m║\n'
        '╟─────────────────┼─────────────────╢\n'
        '\x1b[104m \x1b[49m\x1b[0m\x1b[104mCol 1 Row 2\x1b[49m\x1b[0m\x1b[104m    \x1b[49m\x1b[0m\x1b[104m \x1b[49m│\x1b[104m \x1b[49m\x1b[0mCol 2 Row 2\x1b[0m\x1b[104m    \x1b[49m\x1b[0m\x1b[104m \x1b[49m║\n'
        '╚═════════════════╧═════════════════╝'
    )


def test_bordered_table_width():
    # Default behavior (column_borders=True, padding=1)
    assert BorderedTable.base_width(1) == 4
    assert BorderedTable.base_width(2) == 7
    assert BorderedTable.base_width(3) == 10

    # No column borders
    assert BorderedTable.base_width(1, column_borders=False) == 4
    assert BorderedTable.base_width(2, column_borders=False) == 6
    assert BorderedTable.base_width(3, column_borders=False) == 8

    # No padding
    assert BorderedTable.base_width(1, padding=0) == 2
    assert BorderedTable.base_width(2, padding=0) == 3
    assert BorderedTable.base_width(3, padding=0) == 4

    # Extra padding
    assert BorderedTable.base_width(1, padding=3) == 8
    assert BorderedTable.base_width(2, padding=3) == 15
    assert BorderedTable.base_width(3, padding=3) == 22

    # Invalid num_cols value
    with pytest.raises(ValueError) as excinfo:
        BorderedTable.base_width(0)
    assert "Column count cannot be less than 1" in str(excinfo.value)

    # Total width
    column_1 = Column("Col 1", width=15)
    column_2 = Column("Col 2", width=15)

    row_data = list()
    row_data.append(["Col 1 Row 1", "Col 2 Row 1"])
    row_data.append(["Col 1 Row 2", "Col 2 Row 2"])

    bt = BorderedTable([column_1, column_2])
    assert bt.total_width() == 37


def test_bordered_generate_data_row_exceptions():
    column_1 = Column("Col 1")
    tc = BorderedTable([column_1])

    # Data with too many columns
    row_data = ['Data 1', 'Extra Column']
    with pytest.raises(ValueError) as excinfo:
        tc.generate_data_row(row_data=row_data)
    assert "Length of row_data must match length of cols" in str(excinfo.value)


def test_alternating_table_creation():
    column_1 = Column("Col 1", width=15)
    column_2 = Column("Col 2", width=15)

    row_data = list()
    row_data.append(["Col 1 Row 1", "Col 2 Row 1"])
    row_data.append(["Col 1 Row 2", "Col 2 Row 2"])

    # Default options
    at = AlternatingTable([column_1, column_2])
    table = at.generate_table(row_data)
    assert table == (
        '╔═════════════════╤═════════════════╗\n'
        '║ Col 1           │ Col 2           ║\n'
        '╠═════════════════╪═════════════════╣\n'
        '║ Col 1 Row 1     │ Col 2 Row 1     ║\n'
        '\x1b[100m \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m    \x1b[49m\x1b[0m\x1b[100m \x1b[49m│\x1b[100m \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m    \x1b[49m\x1b[0m\x1b[100m \x1b[49m║\n'
        '╚═════════════════╧═════════════════╝'
    )

    # No column borders
    at = AlternatingTable([column_1, column_2], column_borders=False)
    table = at.generate_table(row_data)
    assert table == (
        '╔══════════════════════════════════╗\n'
        '║ Col 1            Col 2           ║\n'
        '╠══════════════════════════════════╣\n'
        '║ Col 1 Row 1      Col 2 Row 1     ║\n'
        '\x1b[100m \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m    \x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m    \x1b[49m\x1b[0m\x1b[100m \x1b[49m║\n'
        '╚══════════════════════════════════╝'
    )

    # No header
    at = AlternatingTable([column_1, column_2])
    table = at.generate_table(row_data, include_header=False)
    assert table == (
        '╔═════════════════╤═════════════════╗\n'
        '║ Col 1 Row 1     │ Col 2 Row 1     ║\n'
        '\x1b[100m \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m    \x1b[49m\x1b[0m\x1b[100m \x1b[49m│\x1b[100m \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m    \x1b[49m\x1b[0m\x1b[100m \x1b[49m║\n'
        '╚═════════════════╧═════════════════╝'
    )

    # Non-default padding
    at = AlternatingTable([column_1, column_2], padding=2)
    table = at.generate_table(row_data)
    assert table == (
        '╔═══════════════════╤═══════════════════╗\n'
        '║  Col 1            │  Col 2            ║\n'
        '╠═══════════════════╪═══════════════════╣\n'
        '║  Col 1 Row 1      │  Col 2 Row 1      ║\n'
        '\x1b[100m  \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m    \x1b[49m\x1b[0m\x1b[100m  \x1b[49m│\x1b[100m  \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m    \x1b[49m\x1b[0m\x1b[100m  \x1b[49m║\n'
        '╚═══════════════════╧═══════════════════╝'
    )

    # Invalid padding
    with pytest.raises(ValueError) as excinfo:
        AlternatingTable([column_1, column_2], padding=-1)
    assert "Padding cannot be less than 0" in str(excinfo.value)

    # Test border, header, and data colors
    at = AlternatingTable([column_1, column_2], border_fg=Fg.LIGHT_YELLOW, border_bg=Bg.WHITE,
                          header_bg=Bg.GREEN, odd_bg=Bg.LIGHT_BLUE, even_bg=Bg.LIGHT_RED)
    table = at.generate_table(row_data)
    assert table == (
        '\x1b[93m\x1b[107m╔═\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m═══════════════\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m═╤═\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m═══════════════\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m═╗\x1b[39m\x1b[49m\n'
        '\x1b[93m\x1b[107m║\x1b[39m\x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42mCol 1\x1b[49m\x1b[0m\x1b[42m          \x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[93m\x1b[107m│\x1b[39m\x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42mCol 2\x1b[49m\x1b[0m\x1b[42m          \x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[93m\x1b[107m║\x1b[39m\x1b[49m\n'
        '\x1b[93m\x1b[107m╠═\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m═══════════════\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m═╪═\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m═══════════════\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m═╣\x1b[39m\x1b[49m\n'
        '\x1b[93m\x1b[107m║\x1b[39m\x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104mCol 1 Row 1\x1b[49m\x1b[0m\x1b[104m    \x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[93m\x1b[107m│\x1b[39m\x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104mCol 2 Row 1\x1b[49m\x1b[0m\x1b[104m    \x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[93m\x1b[107m║\x1b[39m\x1b[49m\n'
        '\x1b[93m\x1b[107m║\x1b[39m\x1b[49m\x1b[101m \x1b[49m\x1b[0m\x1b[101mCol 1 Row 2\x1b[49m\x1b[0m\x1b[101m    \x1b[49m\x1b[0m\x1b[101m \x1b[49m\x1b[93m\x1b[107m│\x1b[39m\x1b[49m\x1b[101m \x1b[49m\x1b[0m\x1b[101mCol 2 Row 2\x1b[49m\x1b[0m\x1b[101m    \x1b[49m\x1b[0m\x1b[101m \x1b[49m\x1b[93m\x1b[107m║\x1b[39m\x1b[49m\n'
        '\x1b[93m\x1b[107m╚═\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m═══════════════\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m═╧═\x1b[39m\x1b[49m\x1b[0m\x1b[0m\x1b[93m\x1b[107m═══════════════\x1b[39m\x1b[49m\x1b[0m\x1b[93m\x1b[107m═╝\x1b[39m\x1b[49m'
    )

    # Make sure AlternatingTable respects style_header_text and style_data_text flags.
    # Don't apply parent table's background colors to header or data text in second column.
    at = AlternatingTable([column_1, Column("Col 2", width=15, style_header_text=False, style_data_text=False)],
                          header_bg=Bg.GREEN, odd_bg=Bg.LIGHT_BLUE, even_bg=Bg.LIGHT_RED)
    table = at.generate_table(row_data)
    assert table == (
        '╔═════════════════╤═════════════════╗\n'
        '\x1b[42m \x1b[49m\x1b[0m\x1b[42mCol 1\x1b[49m\x1b[0m\x1b[42m          \x1b[49m\x1b[0m\x1b[42m \x1b[49m│\x1b[42m \x1b[49m\x1b[0mCol 2\x1b[0m\x1b[42m          \x1b[49m\x1b[0m\x1b[42m \x1b[49m║\n'
        '╠═════════════════╪═════════════════╣\n'
        '\x1b[104m \x1b[49m\x1b[0m\x1b[104mCol 1 Row 1\x1b[49m\x1b[0m\x1b[104m    \x1b[49m\x1b[0m\x1b[104m \x1b[49m│\x1b[104m \x1b[49m\x1b[0mCol 2 Row 1\x1b[0m\x1b[104m    \x1b[49m\x1b[0m\x1b[104m \x1b[49m║\n'
        '\x1b[101m \x1b[49m\x1b[0m\x1b[101mCol 1 Row 2\x1b[49m\x1b[0m\x1b[101m    \x1b[49m\x1b[0m\x1b[101m \x1b[49m│\x1b[101m \x1b[49m\x1b[0mCol 2 Row 2\x1b[0m\x1b[101m    \x1b[49m\x1b[0m\x1b[101m \x1b[49m║\n'
        '╚═════════════════╧═════════════════╝'
    )