diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2020-04-09 16:37:46 -0400 |
---|---|---|
committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2020-04-09 16:37:46 -0400 |
commit | d815d2cd19a24bac89bd19416fb2b7cd0dadfe03 (patch) | |
tree | 2570d13b2561822911b000b36519be742ec89345 /tests | |
parent | 0d4be64b6ec76fcf5f87933293dbc7c134a32cf0 (diff) | |
download | cmd2-git-d815d2cd19a24bac89bd19416fb2b7cd0dadfe03.tar.gz |
Initial commit of table creation API
Diffstat (limited to 'tests')
-rw-r--r-- | tests/test_argparse_custom.py | 10 | ||||
-rwxr-xr-x | tests/test_cmd2.py | 6 | ||||
-rw-r--r-- | tests/test_table_creator.py | 380 | ||||
-rw-r--r-- | tests/test_utils.py | 45 |
4 files changed, 427 insertions, 14 deletions
diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py index ce789f8e..92b2ecb4 100644 --- a/tests/test_argparse_custom.py +++ b/tests/test_argparse_custom.py @@ -7,8 +7,8 @@ import argparse import pytest import cmd2 -from cmd2 import Cmd2ArgumentParser -from cmd2.argparse_custom import generate_range_error, INFINITY +from cmd2 import Cmd2ArgumentParser, constants +from cmd2.argparse_custom import generate_range_error from .conftest import run_cmd @@ -188,7 +188,7 @@ def test_apcustom_narg_tuple_other_ranges(): parser = Cmd2ArgumentParser() arg = parser.add_argument('arg', nargs=(2,)) assert arg.nargs == argparse.ONE_OR_MORE - assert arg.nargs_range == (2, INFINITY) + assert arg.nargs_range == (2, constants.INFINITY) # Test finite range parser = Cmd2ArgumentParser() @@ -216,10 +216,10 @@ def test_apcustom_print_message(capsys): def test_generate_range_error(): # max is INFINITY - err_str = generate_range_error(1, INFINITY) + err_str = generate_range_error(1, constants.INFINITY) assert err_str == "expected at least 1 argument" - err_str = generate_range_error(2, INFINITY) + err_str = generate_range_error(2, constants.INFINITY) assert err_str == "expected at least 2 arguments" # min and max are equal diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index b86ddfa6..bb99e15b 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -166,10 +166,8 @@ def test_set_allow_style(base_app, new_val, is_valid, expected): assert not err assert "now: {!r}".format(new_val.capitalize()) in out[1] - # Reload ansi module to reset allow_style to its default since it's an - # application-wide setting that can affect other unit tests. - import importlib - importlib.reload(ansi) + # Reset allow_style to its default since it's an application-wide setting that can affect other unit tests + ansi.allow_style = ansi.STYLE_TERMINAL class OnChangeHookApp(cmd2.Cmd): diff --git a/tests/test_table_creator.py b/tests/test_table_creator.py new file mode 100644 index 00000000..a85ab214 --- /dev/null +++ b/tests/test_table_creator.py @@ -0,0 +1,380 @@ +# coding=utf-8 +# flake8: noqa E501 +""" +Unit testing for cmd2/table_creator.py module +""" +import pytest + +from cmd2 import ansi +from cmd2.table_creator import (AlternatingTable, BorderedTable, Column, HorizontalAlignment, + SimpleTable, TableCreator, VerticalAlignment) + + +def test_column_creation(): + # No width specified, blank label + c = Column("") + assert c.width == 1 + + # No width specified, label isn't blank but has no width + c = Column(ansi.style('', fg=ansi.fg.green)) + assert c.width == 1 + + # No width specified, label has width + c = Column("short\nreally long") + assert c.width == ansi.style_aware_wcswidth("really long") + + # 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) + + +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.LEFT, data_vert_align=VerticalAlignment.TOP) + column_2 = Column("Col 2", width=10, + header_horiz_align=HorizontalAlignment.CENTER, header_vert_align=VerticalAlignment.MIDDLE, + data_horiz_align=HorizontalAlignment.CENTER, data_vert_align=VerticalAlignment.MIDDLE) + column_3 = Column("Col 3", width=10, + header_horiz_align=HorizontalAlignment.RIGHT, header_vert_align=VerticalAlignment.BOTTOM, + data_horiz_align=HorizontalAlignment.RIGHT, data_vert_align=VerticalAlignment.BOTTOM) + 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 + header = tc.generate_row() + assert header == ('Col 1 Three \n' + ' Col 2 line \n' + ' Col 3 header \n') + + # Create a data row + row_data = ["Val 1", "Val 2", "Val 3", "Three\nline\ndata"] + row = tc.generate_row(row_data=row_data) + assert row == ('Val 1 Three \n' + ' Val 2 line \n' + ' Val 3 data \n') + + +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) + assert row == ('Some text \n' + 'to wrap \n' + 'A new line\n' + 'that will \n' + 'wrap \n' + 'Not wrap \n' + ' 1 2 3 \n') + + # Test preserving a multiple space sequence across a line break + row_data = ['First last one'] + row = tc.generate_row(row_data=row_data) + assert row == ('First \n' + ' last one \n') + + +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) + assert row == ('First line\n' + 'last line \n') + + # 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) + assert row == ('First line\n' + 'last line…\n') + + # 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) + assert row == ('First line\n' + 'thistxtfi…\n') + + # 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) + assert row == ('First line\n' + 'thistxtfi…\n') + + # 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) + assert row == ('First line\n' + 'last line…\n') + + +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 + header = tc.generate_row() + assert header == ('LongColumn \n' + 'Name Col 2 \n') + + # 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=ansi.fg.green)) + + # Long word should start on the second line + row_data.append("Word LongerThan10") + + row = tc.generate_row(row_data=row_data) + expected = (ansi.RESET_ALL + ansi.fg.green + "LongerThan" + ansi.RESET_ALL + " Word \n" + + ansi.RESET_ALL + ansi.fg.green + "10" + ansi.fg.reset + ansi.RESET_ALL + ' ' + ansi.RESET_ALL + ' LongerThan\n' + ' 10 \n') + 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) + assert row == ('LongerThan LongerThan LongerThan A LongerT…\n' + '10FitsLast 10FitsLas… 10RunsOve… \n') + + +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=['深']) + assert row == '…\n' + + +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, 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, **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) + 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") + tc = TableCreator([column_1, column_2], tab_width=2) + + row = tc.generate_row(fill_char='\t', pre_line='\t', + inter_cell='\t', post_line='\t') + assert row == ' Col 1 Col 2 \n' + + +def test_simple_table(): + 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 + 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 \n') + + # 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 \n') + + # No divider + st = SimpleTable([column_1, column_2], divider_char=None) + table = st.generate_table(row_data) + + assert table == ('Col 1 Col 2 \n' + 'Col 1 Row 1 Col 2 Row 1 \n' + '\n' + 'Col 1 Row 2 Col 2 Row 2 \n') + + # 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 \n') + + # 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 \n') + + # 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) + + +def test_bordered_table(): + 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' + '╚═════════════════╧═════════════════╝\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' + '╚══════════════════════════════════╝\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' + '╚═════════════════╧═════════════════╝\n') + + +def test_alternating_table(): + 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[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m │ \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' + '╚═════════════════╧═════════════════╝\n') + + # Other bg colors + at = AlternatingTable([column_1, column_2], bg_odd=ansi.bg.bright_blue, bg_even=ansi.bg.green) + table = at.generate_table(row_data) + assert table == ('╔═════════════════╤═════════════════╗\n' + '║ Col 1 │ Col 2 ║\n' + '╠═════════════════╪═════════════════╣\n' + '\x1b[104m║ \x1b[49m\x1b[0m\x1b[104mCol 1 Row 1\x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\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[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104m ║\x1b[49m\n' + '\x1b[42m║ \x1b[49m\x1b[0m\x1b[42mCol 1 Row 2\x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42m │ \x1b[49m\x1b[0m\x1b[42mCol 2 Row 2\x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42m ║\x1b[49m\n' + '╚═════════════════╧═════════════════╝\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[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' + '╚══════════════════════════════════╝\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[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m │ \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' + '╚═════════════════╧═════════════════╝\n') diff --git a/tests/test_utils.py b/tests/test_utils.py index 7546184e..27bf4743 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -368,14 +368,49 @@ def test_align_text_fill_char_is_tab(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) assert aligned == text + ' ' -def test_align_text_fill_char_has_color(): +def test_align_text_with_style(): from cmd2 import ansi - text = 'foo' - fill_char = ansi.fg.bright_yellow + '-' + ansi.fg.reset - width = 5 + # Single line with only left fill + text = ansi.style('line1', fg=ansi.fg.bright_blue) + fill_char = ansi.style('-', fg=ansi.fg.bright_yellow) + width = 6 + + aligned = cu.align_text(text, cu.TextAlignment.RIGHT, fill_char=fill_char, width=width) + + left_fill = ansi.RESET_ALL + fill_char + ansi.RESET_ALL + right_fill = ansi.RESET_ALL + line_1_text = ansi.fg.bright_blue + 'line1' + ansi.FG_RESET + + assert aligned == (left_fill + line_1_text + right_fill) + + # Single line with only right fill + text = ansi.style('line1', fg=ansi.fg.bright_blue) + fill_char = ansi.style('-', fg=ansi.fg.bright_yellow) + width = 6 + aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) - assert aligned == text + fill_char * 2 + + left_fill = ansi.RESET_ALL + right_fill = ansi.RESET_ALL + fill_char + ansi.RESET_ALL + line_1_text = ansi.fg.bright_blue + 'line1' + ansi.FG_RESET + + assert aligned == (left_fill + line_1_text + right_fill) + + # Multiple lines to show that style is preserved across all lines. Also has left and right fill. + text = ansi.style('line1\nline2', fg=ansi.fg.bright_blue) + fill_char = ansi.style('-', fg=ansi.fg.bright_yellow) + width = 7 + + aligned = cu.align_text(text, cu.TextAlignment.CENTER, fill_char=fill_char, width=width) + + left_fill = ansi.RESET_ALL + fill_char + ansi.RESET_ALL + right_fill = ansi.RESET_ALL + fill_char + ansi.RESET_ALL + line_1_text = ansi.fg.bright_blue + 'line1' + line_2_text = ansi.fg.bright_blue + 'line2' + ansi.FG_RESET + + assert aligned == (left_fill + line_1_text + right_fill + '\n' + + left_fill + line_2_text + right_fill) def test_align_text_width_is_too_small(): text = 'foo' |