# coding=utf-8 # flake8: noqa E302 """ Unit testing for cmd2/utils.py module. """ import os import signal import sys import time import pytest import cmd2.utils as cu from cmd2.constants import ( HORIZONTAL_ELLIPSIS, ) try: import mock except ImportError: from unittest import ( mock, ) HELLO_WORLD = 'Hello, world!' def test_strip_quotes_no_quotes(): base_str = HELLO_WORLD stripped = cu.strip_quotes(base_str) assert base_str == stripped def test_strip_quotes_with_quotes(): base_str = '"' + HELLO_WORLD + '"' stripped = cu.strip_quotes(base_str) assert stripped == HELLO_WORLD def test_remove_duplicates_no_duplicates(): no_dups = [5, 4, 3, 2, 1] assert cu.remove_duplicates(no_dups) == no_dups def test_remove_duplicates_with_duplicates(): duplicates = [1, 1, 2, 3, 9, 9, 7, 8] assert cu.remove_duplicates(duplicates) == [1, 2, 3, 9, 7, 8] def test_unicode_normalization(): s1 = 'café' s2 = 'cafe\u0301' assert s1 != s2 assert cu.norm_fold(s1) == cu.norm_fold(s2) def test_unicode_casefold(): micro = 'µ' micro_cf = micro.casefold() assert micro != micro_cf assert cu.norm_fold(micro) == cu.norm_fold(micro_cf) def test_alphabetical_sort(): my_list = ['café', 'µ', 'A', 'micro', 'unity', 'cafeteria'] assert cu.alphabetical_sort(my_list) == ['A', 'cafeteria', 'café', 'micro', 'unity', 'µ'] my_list = ['a3', 'a22', 'A2', 'A11', 'a1'] assert cu.alphabetical_sort(my_list) == ['a1', 'A11', 'A2', 'a22', 'a3'] def test_try_int_or_force_to_lower_case(): str1 = '17' assert cu.try_int_or_force_to_lower_case(str1) == 17 str1 = 'ABC' assert cu.try_int_or_force_to_lower_case(str1) == 'abc' str1 = 'X19' assert cu.try_int_or_force_to_lower_case(str1) == 'x19' str1 = '' assert cu.try_int_or_force_to_lower_case(str1) == '' def test_natural_keys(): my_list = ['café', 'µ', 'A', 'micro', 'unity', 'x1', 'X2', 'X11', 'X0', 'x22'] my_list.sort(key=cu.natural_keys) assert my_list == ['A', 'café', 'micro', 'unity', 'X0', 'x1', 'X2', 'X11', 'x22', 'µ'] my_list = ['a3', 'a22', 'A2', 'A11', 'a1'] my_list.sort(key=cu.natural_keys) assert my_list == ['a1', 'A2', 'a3', 'A11', 'a22'] def test_natural_sort(): my_list = ['café', 'µ', 'A', 'micro', 'unity', 'x1', 'X2', 'X11', 'X0', 'x22'] assert cu.natural_sort(my_list) == ['A', 'café', 'micro', 'unity', 'X0', 'x1', 'X2', 'X11', 'x22', 'µ'] my_list = ['a3', 'a22', 'A2', 'A11', 'a1'] assert cu.natural_sort(my_list) == ['a1', 'A2', 'a3', 'A11', 'a22'] def test_is_quoted_short(): my_str = '' assert not cu.is_quoted(my_str) your_str = '"' assert not cu.is_quoted(your_str) def test_is_quoted_yes(): my_str = '"This is a test"' assert cu.is_quoted(my_str) your_str = "'of the emergengy broadcast system'" assert cu.is_quoted(your_str) def test_is_quoted_no(): my_str = '"This is a test' assert not cu.is_quoted(my_str) your_str = "of the emergengy broadcast system'" assert not cu.is_quoted(your_str) simple_str = "hello world" assert not cu.is_quoted(simple_str) def test_quote_string(): my_str = "Hello World" assert cu.quote_string(my_str) == '"' + my_str + '"' my_str = "'Hello World'" assert cu.quote_string(my_str) == '"' + my_str + '"' my_str = '"Hello World"' assert cu.quote_string(my_str) == "'" + my_str + "'" def test_quote_string_if_needed_yes(): my_str = "Hello World" assert cu.quote_string_if_needed(my_str) == '"' + my_str + '"' your_str = '"foo" bar' assert cu.quote_string_if_needed(your_str) == "'" + your_str + "'" def test_quote_string_if_needed_no(): my_str = "HelloWorld" assert cu.quote_string_if_needed(my_str) == my_str your_str = "'Hello World'" assert cu.quote_string_if_needed(your_str) == your_str @pytest.fixture def stdout_sim(): stdsim = cu.StdSim(sys.stdout, echo=True) return stdsim def test_stdsim_write_str(stdout_sim): my_str = 'Hello World' stdout_sim.write(my_str) assert stdout_sim.getvalue() == my_str def test_stdsim_write_bytes(stdout_sim): b_str = b'Hello World' with pytest.raises(TypeError): stdout_sim.write(b_str) def test_stdsim_buffer_write_bytes(stdout_sim): b_str = b'Hello World' stdout_sim.buffer.write(b_str) assert stdout_sim.getvalue() == b_str.decode() assert stdout_sim.getbytes() == b_str def test_stdsim_buffer_write_str(stdout_sim): my_str = 'Hello World' with pytest.raises(TypeError): stdout_sim.buffer.write(my_str) def test_stdsim_read(stdout_sim): my_str = 'Hello World' stdout_sim.write(my_str) # getvalue() returns the value and leaves it unaffected internally assert stdout_sim.getvalue() == my_str # read() returns the value and then clears the internal buffer assert stdout_sim.read() == my_str assert stdout_sim.getvalue() == '' stdout_sim.write(my_str) assert stdout_sim.getvalue() == my_str assert stdout_sim.read(2) == my_str[:2] assert stdout_sim.getvalue() == my_str[2:] def test_stdsim_read_bytes(stdout_sim): b_str = b'Hello World' stdout_sim.buffer.write(b_str) # getbytes() returns the value and leaves it unaffected internally assert stdout_sim.getbytes() == b_str # read_bytes() returns the value and then clears the internal buffer assert stdout_sim.readbytes() == b_str assert stdout_sim.getbytes() == b'' def test_stdsim_clear(stdout_sim): my_str = 'Hello World' stdout_sim.write(my_str) assert stdout_sim.getvalue() == my_str stdout_sim.clear() assert stdout_sim.getvalue() == '' def test_stdsim_getattr_exist(stdout_sim): # Here the StdSim getattr is allowing us to access methods within StdSim my_str = 'Hello World' stdout_sim.write(my_str) val_func = getattr(stdout_sim, 'getvalue') assert val_func() == my_str def test_stdsim_getattr_noexist(stdout_sim): # Here the StdSim getattr is allowing us to access methods defined by the inner stream assert not stdout_sim.isatty() def test_stdsim_pause_storage(stdout_sim): # Test pausing storage for string data my_str = 'Hello World' stdout_sim.pause_storage = False stdout_sim.write(my_str) assert stdout_sim.read() == my_str stdout_sim.pause_storage = True stdout_sim.write(my_str) assert stdout_sim.read() == '' # Test pausing storage for binary data b_str = b'Hello World' stdout_sim.pause_storage = False stdout_sim.buffer.write(b_str) assert stdout_sim.readbytes() == b_str stdout_sim.pause_storage = True stdout_sim.buffer.write(b_str) assert stdout_sim.getbytes() == b'' def test_stdsim_line_buffering(base_app): # This exercises the case of writing binary data that contains new lines/carriage returns to a StdSim # when line buffering is on. The output should immediately be flushed to the underlying stream. import os import tempfile file = tempfile.NamedTemporaryFile(mode='wt') file.line_buffering = True stdsim = cu.StdSim(file, echo=True) saved_size = os.path.getsize(file.name) bytes_to_write = b'hello\n' stdsim.buffer.write(bytes_to_write) assert os.path.getsize(file.name) == saved_size + len(bytes_to_write) saved_size = os.path.getsize(file.name) bytes_to_write = b'hello\r' stdsim.buffer.write(bytes_to_write) assert os.path.getsize(file.name) == saved_size + len(bytes_to_write) @pytest.fixture def pr_none(): import subprocess # Start a long running process so we have time to run tests on it before it finishes # Put the new process into a separate group so its signal are isolated from ours kwargs = dict() if sys.platform.startswith('win'): command = 'timeout -t 5 /nobreak' kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP else: command = 'sleep 5' kwargs['start_new_session'] = True proc = subprocess.Popen(command, shell=True, **kwargs) pr = cu.ProcReader(proc, None, None) return pr def test_proc_reader_send_sigint(pr_none): assert pr_none._proc.poll() is None pr_none.send_sigint() wait_start = time.monotonic() pr_none.wait() wait_finish = time.monotonic() # Make sure the process exited before sleep of 5 seconds finished # 3 seconds accounts for some delay but is long enough for the process to exit assert wait_finish - wait_start < 3 ret_code = pr_none._proc.poll() if sys.platform.startswith('win'): assert ret_code is not None else: assert ret_code == -signal.SIGINT def test_proc_reader_terminate(pr_none): assert pr_none._proc.poll() is None pr_none.terminate() wait_start = time.monotonic() pr_none.wait() wait_finish = time.monotonic() # Make sure the process exited before sleep of 5 seconds finished # 3 seconds accounts for some delay but is long enough for the process to exit assert wait_finish - wait_start < 3 ret_code = pr_none._proc.poll() if sys.platform.startswith('win'): assert ret_code is not None else: assert ret_code == -signal.SIGTERM @pytest.fixture def context_flag(): return cu.ContextFlag() def test_context_flag_bool(context_flag): assert not context_flag with context_flag: assert context_flag def test_context_flag_exit_err(context_flag): with pytest.raises(ValueError): context_flag.__exit__() def test_truncate_line(): line = 'long' max_width = 3 truncated = cu.truncate_line(line, max_width) assert truncated == 'lo' + HORIZONTAL_ELLIPSIS def test_truncate_line_already_fits(): line = 'long' max_width = 4 truncated = cu.truncate_line(line, max_width) assert truncated == line def test_truncate_line_with_newline(): line = 'fo\no' max_width = 2 with pytest.raises(ValueError): cu.truncate_line(line, max_width) def test_truncate_line_width_is_too_small(): line = 'foo' max_width = 0 with pytest.raises(ValueError): cu.truncate_line(line, max_width) def test_truncate_line_wide_text(): line = '苹苹other' max_width = 6 truncated = cu.truncate_line(line, max_width) assert truncated == '苹苹o' + HORIZONTAL_ELLIPSIS def test_truncate_line_split_wide_text(): """Test when truncation results in a string which is shorter than max_width""" line = '1苹2苹' max_width = 3 truncated = cu.truncate_line(line, max_width) assert truncated == '1' + HORIZONTAL_ELLIPSIS def test_truncate_line_tabs(): line = 'has\ttab' max_width = 9 truncated = cu.truncate_line(line, max_width) assert truncated == 'has t' + HORIZONTAL_ELLIPSIS def test_truncate_with_style(): from cmd2 import ( ansi, ) before_style = ansi.fg.blue + ansi.UNDERLINE_ENABLE after_style = ansi.fg.reset + ansi.UNDERLINE_DISABLE # Style only before truncated text line = before_style + 'long' max_width = 3 truncated = cu.truncate_line(line, max_width) assert truncated == before_style + 'lo' + HORIZONTAL_ELLIPSIS # Style before and after truncated text line = before_style + 'long' + after_style max_width = 3 truncated = cu.truncate_line(line, max_width) assert truncated == before_style + 'lo' + HORIZONTAL_ELLIPSIS + after_style # Style only after truncated text line = 'long' + after_style max_width = 3 truncated = cu.truncate_line(line, max_width) assert truncated == 'lo' + HORIZONTAL_ELLIPSIS + after_style def test_align_text_fill_char_is_tab(): text = 'foo' fill_char = '\t' width = 5 aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) assert aligned == text + ' ' def test_align_text_with_style(): from cmd2 import ( ansi, ) # 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) 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' fill_char = '-' width = 0 with pytest.raises(ValueError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) def test_align_text_fill_char_is_too_long(): text = 'foo' fill_char = 'fill' width = 5 with pytest.raises(TypeError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) def test_align_text_fill_char_is_newline(): text = 'foo' fill_char = '\n' width = 5 with pytest.raises(ValueError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) def test_align_text_has_tabs(): text = '\t\tfoo' fill_char = '-' width = 10 aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, tab_width=2) assert aligned == ' ' + 'foo' + '---' def test_align_text_blank(): text = '' fill_char = '-' width = 5 aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) assert aligned == fill_char * width def test_align_text_wider_than_width(): text = 'long text field' fill_char = '-' width = 8 aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) assert aligned == text def test_align_text_wider_than_width_truncate(): text = 'long text field' fill_char = '-' width = 8 aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, truncate=True) assert aligned == 'long te' + HORIZONTAL_ELLIPSIS def test_align_text_wider_than_width_truncate_add_fill(): """Test when truncation results in a string which is shorter than width and align_text adds filler""" text = '1苹2苹' fill_char = '-' width = 3 aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, truncate=True) assert aligned == '1' + HORIZONTAL_ELLIPSIS + fill_char def test_align_text_has_unprintable(): text = 'foo\x02' fill_char = '-' width = 5 with pytest.raises(ValueError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) def test_align_text_term_width(): import shutil from cmd2 import ( ansi, ) text = 'foo' fill_char = ' ' term_width = shutil.get_terminal_size().columns expected_fill = (term_width - ansi.style_aware_wcswidth(text)) * fill_char aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char) assert aligned == text + expected_fill def test_align_left(): text = 'foo' fill_char = '-' width = 5 aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + fill_char def test_align_left_multiline(): text = "foo\nshoes" fill_char = '-' width = 7 aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == ('foo----\n' 'shoes--') def test_align_left_wide_text(): text = '苹' fill_char = '-' width = 4 aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + fill_char def test_align_left_wide_fill(): text = 'foo' fill_char = '苹' width = 5 aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char def test_align_left_wide_fill_needs_padding(): """Test when fill_char's display width does not divide evenly into gap""" text = 'foo' fill_char = '苹' width = 6 aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + ' ' def test_align_center(): text = 'foo' fill_char = '-' width = 5 aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char def test_align_center_multiline(): text = "foo\nshoes" fill_char = '-' width = 7 aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == ('--foo--\n' '-shoes-') def test_align_center_wide_text(): text = '苹' fill_char = '-' width = 4 aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char def test_align_center_wide_fill(): text = 'foo' fill_char = '苹' width = 7 aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char def test_align_center_wide_fill_needs_right_padding(): """Test when fill_char's display width does not divide evenly into right gap""" text = 'foo' fill_char = '苹' width = 8 aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char + ' ' def test_align_center_wide_fill_needs_left_and_right_padding(): """Test when fill_char's display width does not divide evenly into either gap""" text = 'foo' fill_char = '苹' width = 9 aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + ' ' + text + fill_char + ' ' def test_align_right(): text = 'foo' fill_char = '-' width = 5 aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + fill_char + text def test_align_right_multiline(): text = "foo\nshoes" fill_char = '-' width = 7 aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == ('----foo\n' '--shoes') def test_align_right_wide_text(): text = '苹' fill_char = '-' width = 4 aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + fill_char + text def test_align_right_wide_fill(): text = 'foo' fill_char = '苹' width = 5 aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + text def test_align_right_wide_fill_needs_padding(): """Test when fill_char's display width does not divide evenly into gap""" text = 'foo' fill_char = '苹' width = 6 aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + ' ' + text def test_str_to_bool_true(): assert cu.str_to_bool('true') assert cu.str_to_bool('True') assert cu.str_to_bool('TRUE') assert cu.str_to_bool('tRuE') def test_str_to_bool_false(): assert not cu.str_to_bool('false') assert not cu.str_to_bool('False') assert not cu.str_to_bool('FALSE') assert not cu.str_to_bool('fAlSe') def test_str_to_bool_invalid(): with pytest.raises(ValueError): cu.str_to_bool('other') def test_str_to_bool_bad_input(): with pytest.raises(ValueError): cu.str_to_bool(1) def test_find_editor_specified(): expected_editor = os.path.join('fake_dir', 'editor') with mock.patch.dict(os.environ, {'EDITOR': expected_editor}): editor = cu.find_editor() assert editor == expected_editor def test_find_editor_not_specified(): # Use existing path env setting. Something in the editor list should be found. editor = cu.find_editor() assert editor # Overwrite path env setting with invalid path, clear all other env vars so no editor should be found. with mock.patch.dict(os.environ, {'PATH': 'fake_dir'}, clear=True): editor = cu.find_editor() assert editor is None