diff --git a/tests/test_ext.py b/tests/test_ext.py index 1066753..90e2ff5 100644 --- a/tests/test_ext.py +++ b/tests/test_ext.py @@ -1,11 +1,12 @@ # -*- coding:utf-8 -*- import pytest import doctest +import time from unittest.mock import patch from pprint import pprint from pigit.ext.utils import traceback_info, confirm -from pigit.ext.func import dynamic_default_attrs +from pigit.ext.func import dynamic_default_attrs, time_it def test_doctest(): @@ -53,3 +54,32 @@ def bp(a, b, c: int, d: int = 10): return (a, b, c, d) assert dynamic_default_attrs(bp, **da)(1, 2, 0) == (1, 2, 0, 4) + + @pytest.mark.parametrize( + "test_input, expected_output, expected_time_unit, msg", + [ + (lambda x: x + 1, 2, "second", ""), + (lambda _: time.sleep(1.2), None, "second", ""), + (lambda _: time.sleep(61), None, "minute", ""), + # (lambda _: time.sleep(3600), None, "hour", ""), + (lambda x, y: x * y, 20, "second", "multiplication"), + ], + ) + # `capsys ` is a builtin fixture,like sys.stdout and sys.stderr. + def test_time_it_happy_path( + self, monkeypatch, capsys, test_input, expected_output, expected_time_unit, msg + ): + # Arrange + decorated_function = time_it(test_input) + + # Act + result = ( + decorated_function(2, 10) + if "multiplication" in msg + else decorated_function(1) + ) + + # Assert + captured = capsys.readouterr() + assert expected_output == result + assert expected_time_unit in captured.out diff --git a/tests/test_ext_executor.py b/tests/test_ext_executor.py index e958482..a9e2eb8 100644 --- a/tests/test_ext_executor.py +++ b/tests/test_ext_executor.py @@ -121,9 +121,11 @@ def test_exec_parallel(self): """ ) - cmd = 'python3' - if self.executor.exec('python -V', flags=REPLY)[0] == 0 : - cmd ='python' + cmd = ( + "python" + if self.executor.exec("python -V", flags=REPLY)[0] == 0 + else "python3" + ) cmds = [[cmd, "-c", code.format(i)] for i in range(3, 0, -1)] # pprint(cmds) diff --git a/tests/test_git_ignore.py b/tests/test_git_ignore.py index 6aaa9b9..8d25133 100644 --- a/tests/test_git_ignore.py +++ b/tests/test_git_ignore.py @@ -1,9 +1,9 @@ import pytest -from .conftest import TEST_PATH - from pigit.git.ignore import get_ignore_source, create_gitignore, IGNORE_TEMPLATE +from .conftest import TEST_PATH + def test_iter_ignore(): for t in IGNORE_TEMPLATE: diff --git a/tests/test_tui_components.py b/tests/test_tui_components.py index 25755c9..c298e10 100644 --- a/tests/test_tui_components.py +++ b/tests/test_tui_components.py @@ -1,6 +1,13 @@ -from typing import Callable, Dict, Tuple import pytest -from pigit.tui.components import Container, Component, ComponentError, _Namespace +from pigit.tui.components import ( + _Namespace, + Container, + Component, + ComponentError, + ItemSelector, + LineTextBrowser, +) +from pigit.tui.console import Render # Mock Component to use in tests @@ -21,54 +28,263 @@ def _handle_event(self, key): pass -class TestContainer(Container): - NAME = "test-container" +class MockContainer(Container): + NAME = "mock-container" def update(self, action: str, **data): pass -@pytest.mark.parametrize( - "start_name, switch_key, expected_active", - [ - ("main", None, "main"), # Happy path: default start_name - ("secondary", None, "secondary"), # Happy path: specified start_name - ("main", "secondary", "secondary"), # Edge case: switch child after init - ], - ids=["default-start", "specified-start", "switch-after-init"], -) -def test_container_init_and_switch(start_name, switch_key, expected_active): - # Arrange - _Namespace.clear() - children = {"main": MockComponent("main"), "secondary": MockComponent("secondary")} - switch_handle = lambda key: switch_key or start_name - - # Act - container = TestContainer( - children=children, start_name=start_name, switch_handle=switch_handle +class TestContainer: + @pytest.mark.parametrize( + "start_name, switch_key, expected_active", + [ + ("main", None, "main"), # Happy path: default start_name + ("secondary", None, "secondary"), # Happy path: specified start_name + ("main", "secondary", "secondary"), # Edge case: switch child after init + ], + ids=["default-start", "specified-start", "switch-after-init"], ) - if switch_key: - container._handle_event(switch_key) + def test_container_init_and_switch(self, start_name, switch_key, expected_active): + # Arrange + _Namespace.clear() + children = { + "main": MockComponent("main"), + "secondary": MockComponent("secondary"), + } + switch_handle = lambda key: switch_key or start_name - # Assert - assert children[ - expected_active - ].is_activated(), f"{expected_active} should be activated" + # Act + container = MockContainer( + children=children, start_name=start_name, switch_handle=switch_handle + ) + if switch_key: + container._handle_event(switch_key) + # Assert + assert children[ + expected_active + ].is_activated(), f"{expected_active} should be activated" -@pytest.mark.parametrize( - "action, data, expected_exception", - [ - ("unsupported", {}, ComponentError), # Error case: unsupported action - ], - ids=["unsupported-action"], -) -def test_container_accept_errors(action, data, expected_exception): - # Arrange - _Namespace.clear() - children = {"main": MockComponent("main")} - container = TestContainer(children=children) - - # Act / Assert - with pytest.raises(expected_exception): - container.accept(action, **data) + @pytest.mark.parametrize( + "action, data, expected_exception", + [ + ("unsupported", {}, ComponentError), # Error case: unsupported action + ], + ids=["unsupported-action"], + ) + def test_container_accept_errors(self, action, data, expected_exception): + # Arrange + _Namespace.clear() + children = {"main": MockComponent("main")} + container = MockContainer(children=children) + + # Act / Assert + with pytest.raises(expected_exception): + container.accept(action, **data) + + +class MockLineTextBrowser(LineTextBrowser): + NAME = "mock-line-text-browser" + + +class TestLineTextBrowser: + @pytest.mark.parametrize( + "x, y, size, content, expected_position, expected_content", + [ + ( + 1, + 1, + (10, 2), + ["line1", "line2", "line3"], + (1, 1), + ["line1", "line2"], + ), # ID: Test-1 + (2, 3, (5, 1), ["a", "b", "c", "d"], (2, 3), ["a"]), # ID: Test-2 + ( + 0, + 0, + None, + None, + (0, 0), + [], + ), # ID: Test-3, edge case with no size and content + ], + ) + def test_LineTextBrowser_init( + self, mocker, x, y, size, content, expected_position, expected_content + ): + _Namespace.clear() + # Arrange + mocker.patch("pigit.tui.components.Render.draw") + + # Act + browser = MockLineTextBrowser(x, y, size, content) + + # Assert + assert browser.x == expected_position[0] + assert browser.y == expected_position[1] + if content: + browser._render() + Render.draw.assert_called_with(expected_content, *expected_position, size) + + @pytest.mark.parametrize( + "initial_size, new_size, expected_size", + [ + ((10, 2), (5, 3), (5, 3)), # ID: Test-4 + ((5, 1), (10, 5), (10, 5)), # ID: Test-5 + ], + ) + def test_resize(self, mocker, initial_size, new_size, expected_size): + # Arrange + _Namespace.clear() + browser = MockLineTextBrowser(size=initial_size) + mocker.patch.object(browser, "fresh") + + # Act + browser.resize(new_size) + + # Assert + assert browser._size == expected_size + browser.fresh.assert_called_once() + + @pytest.mark.parametrize( + "content, initial_index, scroll_lines, expected_index", + [ + (["line1", "line2", "line3"], 0, 1, 1), # ID: Test-6 + (["line1", "line2", "line3"], 1, 1, 2), # ID: Test-7 + (["line1", "line2", "line3"], 2, 1, 2), # ID: Test-8, edge case at bottom + ], + ) + def test_scroll_down( + self, mocker, content, initial_index, scroll_lines, expected_index + ): + # Arrange + _Namespace.clear() + browser = MockLineTextBrowser(content=content, size=[0, 1]) + browser._i = initial_index + mocker.patch.object(browser, "_render") + + # Act + browser.scroll_down(scroll_lines) + + # Assert + assert browser._i == expected_index + browser._render.assert_called_once() + + @pytest.mark.parametrize( + "content, initial_index, scroll_lines, expected_index", + [ + (["line1", "line2", "line3"], 2, 1, 1), # ID: Test-9 + (["line1", "line2", "line3"], 1, 1, 0), # ID: Test-10 + (["line1", "line2", "line3"], 0, 1, 0), # ID: Test-11, edge case at top + ], + ) + def test_scroll_up( + self, mocker, content, initial_index, scroll_lines, expected_index + ): + # Arrange + _Namespace.clear() + browser = MockLineTextBrowser(content=content) + browser._i = initial_index + mocker.patch.object(browser, "_render") + + # Act + browser.scroll_up(scroll_lines) + + # Assert + assert browser._i == expected_index + browser._render.assert_called_once() + + +class MockItemSelector(ItemSelector): + NAME = "test-item-selector" + + def fresh(self): + pass + + +class TestItemSelector: + def test_ItemSelector_init_error(self): + ItemSelector.NAME = "**" + ItemSelector.CURSOR = "**" + + with pytest.raises(ComponentError): + ItemSelector() + + # Test initialization of ItemSelector + @pytest.mark.parametrize( + "x, y, size, content", + [ + (2, 2, (10, 5), ["Item 1", "Item 2"]), + (0, 0, (5, 5), []), + ], + ) + def test_ItemSelector_init(self, x, y, size, content): + # Arrange + _Namespace.clear() + MockItemSelector.CURSOR = "*" + + # Act + selector = MockItemSelector(x=x, y=y, size=size, content=content) + + # Assert + assert selector.x == x + assert selector.y == y + assert selector._size == size + if content: + assert selector.content == content + else: + assert selector.content == [""] + + # Test resize method + @pytest.mark.parametrize( + "initial_size, new_size", + [ + ((10, 5), (20, 10)), + ((20, 10), (5, 2)), + ], + ids=["resize_larger", "resize_smaller"], + ) + def test_ItemSelector_resize(self, initial_size, new_size): + _Namespace.clear() + selector = MockItemSelector(size=initial_size) + + selector.resize(new_size) + assert selector._size == new_size + + # Test next method with various steps + @pytest.mark.parametrize( + "content, initial_pos, step, expected_pos", + [ + (["Item 1", "Item 2", "Item 3"], 0, 1, 1), + (["Item 1", "Item 2", "Item 3"], 0, 2, 2), + (["Item 1", "Item 2", "Item 3"], 2, 1, 2), + ], + ids=["next_single_step", "next_multiple_steps", "next_beyond_end"], + ) + def test_ItemSelector_next(self, content, initial_pos, step, expected_pos): + _Namespace.clear() + selector = MockItemSelector(content=content) + selector.curr_no = initial_pos + + selector.next(step=step) + assert selector.curr_no == expected_pos + + # Test forward method with various steps + @pytest.mark.parametrize( + "content, initial_pos, step, expected_pos", + [ + (["Item 1", "Item 2", "Item 3"], 2, 1, 1), + (["Item 1", "Item 2", "Item 3"], 2, 2, 0), + (["Item 1", "Item 2", "Item 3"], 0, 1, 0), + ], + ids=["forward_single_step", "forward_multiple_steps", "forward_beyond_start"], + ) + def test_ItemSelector_forward(self, content, initial_pos, step, expected_pos): + _Namespace.clear() + selector = MockItemSelector(content=content) + selector.curr_no = initial_pos + + selector.forward(step=step) + assert selector.curr_no == expected_pos diff --git a/tests/test_tui_input.py b/tests/test_tui_input.py index 9278a00..1b6bcd3 100644 --- a/tests/test_tui_input.py +++ b/tests/test_tui_input.py @@ -2,6 +2,8 @@ import termios import pytest from unittest.mock import Mock, patch + +import pigit.tui from pigit.tui.input import ( PosixInput, KeyQueueTrie, @@ -9,7 +11,43 @@ process_key_queue, process_one_code, set_byte_encoding, + set_encoding, +) + + +@pytest.mark.parametrize( + "encoding, expected_byte_encoding, expected_dec_special, expected_target_encoding", + [ + # Happy path tests + ("utf-8", "utf8", False, "utf-8"), + ("UTF8", "utf8", False, "utf8"), + ("utf", "utf8", False, "utf"), + ("ascii", "narrow", True, "ascii"), + ("ISO-8859-1", "narrow", True, "iso-8859-1"), + # Edge cases + ("", "narrow", True, "ascii"), + # Error cases are not applicable as the function handles all strings without raising exceptions + ], ) +def test_set_encoding( + encoding, + expected_byte_encoding, + expected_dec_special, + expected_target_encoding, + mocker, +): + # Arrange + mocker.patch("pigit.tui.input.set_byte_encoding") + mocker.patch("pigit.tui.input._target_encoding", "ascii") + mocker.patch("pigit.tui.input._use_dec_special", True) + + # Act + set_encoding(encoding) + + # Assert + pigit.tui.input.set_byte_encoding.assert_called_with(expected_byte_encoding) + assert pigit.tui.input._use_dec_special == expected_dec_special + assert pigit.tui.input._target_encoding == expected_target_encoding class TestKeyQueueTrie: @@ -49,6 +87,15 @@ def test_init_and_add(self, sequences, expected_data): False, ("value", []), ), # Test case ID: 5 + ], + ) + def test_get(self, sequences, codes, more_available, expected): + kqt = KeyQueueTrie(sequences) + assert kqt.get(codes, more_available) == expected + + @pytest.mark.parametrize( + "sequences, codes, more_available, expected", + [ ( (("abc", "value"),), [ord("a"), ord("b")], @@ -57,16 +104,11 @@ def test_init_and_add(self, sequences, expected_data): ), # Test case ID: 6 ], ) - def test_get(self, sequences, codes, more_available, expected): - # Arrange + def test_get_with_more(self, sequences, codes, more_available, expected): kqt = KeyQueueTrie(sequences) - # Act & Assert - if isinstance(expected, type) and issubclass(expected, Exception): - with pytest.raises(expected): - kqt.get(codes, more_available) - else: - assert kqt.get(codes, more_available) == expected + with pytest.raises(expected): + kqt.get(codes, more_available) # Test for get_recurse method @pytest.mark.parametrize( @@ -180,6 +222,7 @@ def test_read_cursor_position(self, codes, more_available, expected): (31, "ctrl _"), # Upper bound of second if condition (32, " "), # Lower bound of third if condition (126, "~"), # Upper bound of third if condition + (10, "enter"), # Code in _key_conv (127, "backspace"), # Code in _key_conv (128, None), # Code not in _key_conv (0, None), # Code less than 1 @@ -191,19 +234,14 @@ def test_read_cursor_position(self, codes, more_available, expected): "ctrl_E", "space", "tilde", + "enter", "backspace", - "none", - "zero", + "128", + "0", ], ) def test_process_one_code(code, expected): - # Arrange - global _key_conv - - # Act result = process_one_code(code) - - # Assert assert result == expected @@ -215,44 +253,34 @@ def more_input_required(): # Define the test function @pytest.mark.parametrize( - "codes, more_available, expected_output, expected_remaining_codes, raises", + "codes, more_available, expected_output, expected_remaining_codes", [ # Happy path tests - ([65, 66, 67], False, ["A"], [66, 67], None), # ID: ASCII codes - ([27, 65], False, ["meta A"], [], None), # ID: ESC code - ([27, 27, 65], False, ["esc", "meta A"], [], None), # ID: Multiple ESC codes + ([65, 66, 67], False, ["A"], [66, 67]), # ID: ASCII codes + ([27, 65], False, ["meta A"], []), # ID: ESC code + ([27, 27, 65], False, ["esc", "meta A"], []), # ID: Multiple ESC codes # Edge cases - ([], False, [], [], None), # ID: Empty codes - ([32, 126], False, [" "], [126], None), # ID: Boundary ASCII codes - # Error cases - ( - [240, 201], - True, - [], - [], - more_input_required, - ), # ID: MoreInputRequired exception + ([], False, [], []), # ID: Empty codes + ([32, 126], False, [" "], [126]), # ID: Boundary ASCII codes ], ) def test_process_key_queue( - codes, more_available, expected_output, expected_remaining_codes, raises + codes, more_available, expected_output, expected_remaining_codes ): - # Arrange + output, remaining_codes = process_key_queue(codes, more_available) - # Act - if raises: - with pytest.raises(MoreInputRequired): - set_byte_encoding("utf8") - output, remaining_codes = process_key_queue(codes, more_available) - else: - output, remaining_codes = process_key_queue(codes, more_available) + # Assert + assert output == expected_output + assert remaining_codes == expected_remaining_codes - # Assert - assert output == expected_output - assert remaining_codes == expected_remaining_codes + +def test_process_key_queue_with_exception(): + with pytest.raises(MoreInputRequired): + set_byte_encoding("utf8") + output, remaining_codes = process_key_queue([240, 201], True) -class TestInput: +class TestPosixInput: # Test for PosixInput.write method @pytest.mark.parametrize( "data, id", diff --git a/tools/gitignore.py b/tools/gitignore_util.py similarity index 100% rename from tools/gitignore.py rename to tools/gitignore_util.py