diff --git a/src/prompt_toolkit/buffer.py b/src/prompt_toolkit/buffer.py index f5847d4ab..cb41fcc62 100644 --- a/src/prompt_toolkit/buffer.py +++ b/src/prompt_toolkit/buffer.py @@ -39,6 +39,11 @@ from .utils import Event, to_str from .validation import ValidationError, Validator +branch_coverage_insert = { + "insert_1": False, # Branch for copy_margin = True + "insert_2": False, # Branch for copy_margin = False +} + __all__ = [ "EditReadOnlyBuffer", "Buffer", @@ -1179,13 +1184,17 @@ def newline(self, copy_margin: bool = True) -> None: self.insert_text("\n") def insert_line_above(self, copy_margin: bool = True) -> None: - """ - Insert a new line above the current one. - """ + """Insert a new line above the current one.""" + global branch_coverage_insert + if copy_margin: insert = self.document.leading_whitespace_in_current_line + "\n" + branch_coverage_insert["insert_1"] = True + print("Branch 1 was hit!") else: insert = "\n" + branch_coverage_insert["insert_2"] = True + print("Branch 2 was hit!") self.cursor_position += self.document.get_start_of_line_position() self.insert_text(insert) diff --git a/src/prompt_toolkit/completion/base.py b/src/prompt_toolkit/completion/base.py index 3846ef756..2c6c787d2 100644 --- a/src/prompt_toolkit/completion/base.py +++ b/src/prompt_toolkit/completion/base.py @@ -422,17 +422,33 @@ def get_suffix(completion: Completion) -> str: return _commonprefix([get_suffix(c) for c in completions2]) +# Data structures that hold coverage information about the conditional branches +branch_coverage = { + "_commonprefix_1": False, # Branch for checking if not strings + "_commonprefix_2": False, # Branch for comparing characters + "_commonprefix_3": False, # Branch for when the characters do not match + "_commonprefix_3_else": False, # Branch for else case of when the characters match +} + def _commonprefix(strings: Iterable[str]) -> str: - # Similar to os.path.commonprefix if not strings: + branch_coverage["_commonprefix_1"] = True + print("Branch 1 was hit") return "" else: + branch_coverage["_commonprefix_2"] = True + print("Branch 2 was hit") s1 = min(strings) s2 = max(strings) for i, c in enumerate(s1): if c != s2[i]: + branch_coverage["_commonprefix_3"] = True + print("Branch 3 was hit") return s1[:i] + else: + branch_coverage["_commonprefix_3_else"] = True + print("Branch 3 was not hit") return s1 diff --git a/src/prompt_toolkit/document.py b/src/prompt_toolkit/document.py index df5293e62..c4c43f74e 100644 --- a/src/prompt_toolkit/document.py +++ b/src/prompt_toolkit/document.py @@ -14,11 +14,28 @@ from .filters import vi_mode from .selection import PasteMode, SelectionState, SelectionType +branch_coverage_next = { + "find_next_1": False, + "find_next_2": False, +} + +branch_coverage_prev = { + "find_prev_1": False, + "find_prev_2": False +} + +branch_coverage_translate = { + "translate_1": False, # Branch for successful try block execution + "translate_2": False, # Branch for entering except block + "translate_3": False, # Branch for IndexError with row < 0 + "translate_4": False, # Branch for IndexError with row >= number of lines + "translate_5": False, # Branch for ensuring col is within valid range +} + __all__ = [ "Document", ] - # Regex for finding "words" in documents. (We consider a group of alnum # characters a word, but also a group of special characters a word, as long as # it doesn't contain a space.) @@ -323,18 +340,30 @@ def translate_row_col_to_index(self, row: int, col: int) -> int: Negative row/col values are turned into zero. """ + global branch_coverage_translate + try: result = self._line_start_indexes[row] line = self.lines[row] + branch_coverage_translate["translate_1"] = True + print("Try branch was hit!") except IndexError: + branch_coverage_translate["translate_2"] = True + print("Except branch was hit!") if row < 0: result = self._line_start_indexes[0] line = self.lines[0] + branch_coverage_translate["translate_3"] = True + print("Branch 1 was hit!") else: result = self._line_start_indexes[-1] line = self.lines[-1] + branch_coverage_translate["translate_4"] = True + print("Branch 2 was hit!") result += max(0, min(col, len(line))) + branch_coverage_translate["translate_5"] = True + print("Result was hit!") # Keep in range. (len(self.text) is included, because the cursor can be # right after the end of the text as well.) @@ -652,7 +681,7 @@ def find_previous_word_ending( except StopIteration: pass return None - + def find_next_matching_line( self, match_func: Callable[[str], bool], count: int = 1 ) -> int | None: @@ -664,10 +693,12 @@ def find_next_matching_line( for index, line in enumerate(self.lines[self.cursor_position_row + 1 :]): if match_func(line): + branch_coverage_next["find_next_1"] = True result = 1 + index count -= 1 if count == 0: + branch_coverage_next["find_next_2"] = True break return result @@ -683,14 +714,16 @@ def find_previous_matching_line( for index, line in enumerate(self.lines[: self.cursor_position_row][::-1]): if match_func(line): + branch_coverage_prev["find_prev_1"] = True result = -1 - index count -= 1 if count == 0: + branch_coverage_prev["find_prev_2"] = True break return result - + def get_cursor_left_position(self, count: int = 1) -> int: """ Relative position for cursor left. @@ -1180,3 +1213,4 @@ def insert_before(self, text: str) -> Document: cursor_position=self.cursor_position + len(text), selection=selection_state, ) + diff --git a/src/prompt_toolkit/layout/dimension.py b/src/prompt_toolkit/layout/dimension.py index 2e6f5dd4e..9b4bfa3a9 100644 --- a/src/prompt_toolkit/layout/dimension.py +++ b/src/prompt_toolkit/layout/dimension.py @@ -182,37 +182,84 @@ def max_layout_dimensions(dimensions: list[Dimension]) -> Dimension: Callable[[], Any], ] +from typing import Any, Callable, Union + +to_dimension_coverage = { + "to_dimension_1": False, + "to_dimension_2": False, + "to_dimension_3": False, + "to_dimension_4": False, + "to_dimension_5": False +} def to_dimension(value: AnyDimension) -> Dimension: """ Turn the given object into a `Dimension` object. """ if value is None: + to_dimension_coverage["to_dimension_1"] = True + print("Branch 1 was hit") return Dimension() if isinstance(value, int): + to_dimension_coverage["to_dimension_2"] = True + print("Branch 2 was hit") return Dimension.exact(value) if isinstance(value, Dimension): + to_dimension_coverage["to_dimension_3"] = True + print("Branch 3 was hit") return value if callable(value): + to_dimension_coverage["to_dimension_4"] = True + print("Branch 4 was hit") return to_dimension(value()) + to_dimension_coverage["to_dimension_5"] = True + print("Branch 5 was hit") raise ValueError("Not an integer or Dimension object.") -def is_dimension(value: object) -> TypeGuard[AnyDimension]: - """ - Test whether the given value could be a valid dimension. - (For usage in an assertion. It's not guaranteed in case of a callable.) - """ +# Initialize coverage tracking global variable +branch_coverage = { + "is_dimension_1": False, # Branch for checking if value is None + "is_dimension_1_else": False, # Branch for else case of checking if value is None + "is_dimension_2": False, # Branch for checking if value is callable + "is_dimension_2_else": False, # Branch for else case of checking if value is callable + "is_dimension_3": False, # Branch for checking if value is int or Dimension + "is_dimension_3_else": False, # Branch for else case of checking if value is int or Dimension + "is_dimension_4": False # Branch for default case +} + +def is_dimension(value: object) -> bool: if value is None: + branch_coverage["is_dimension_1"] = True + print("Branch 1 was hit") return True + else: + branch_coverage["is_dimension_1_else"] = True + print("Branch 1 was not hit") + if callable(value): - return True # Assume it's a callable that doesn't take arguments. + branch_coverage["is_dimension_2"] = True + print("Branch 2 was hit") + return True + else: + branch_coverage["is_dimension_2_else"] = True + print("Branch 2 was not hit") + if isinstance(value, (int, Dimension)): + branch_coverage["is_dimension_3"] = True + print("Branch 3 was hit") return True + else: + branch_coverage["is_dimension_3_else"] = True + print("Branch 3 was not hit") + + branch_coverage["is_dimension_4"] = True + print("Branch 4 was hit") return False + # Common alias. D = Dimension diff --git a/src/prompt_toolkit/layout/processors.py b/src/prompt_toolkit/layout/processors.py index b10ecf718..280fc1f53 100644 --- a/src/prompt_toolkit/layout/processors.py +++ b/src/prompt_toolkit/layout/processors.py @@ -943,16 +943,25 @@ def apply_transformation(self, ti: TransformationInput) -> Transformation: return processor.apply_transformation(ti) +merge_processors_coverage = { + "merge_processors_1": False, # Branch for empty list of processors + "merge_processors_2": False, # Branch for a single processor + "merge_processors_3": False # Branch for multiple processors +} + def merge_processors(processors: list[Processor]) -> Processor: """ Merge multiple `Processor` objects into one. """ if len(processors) == 0: + merge_processors_coverage["merge_processors_1"] = True return DummyProcessor() if len(processors) == 1: + merge_processors_coverage["merge_processors_2"] = True return processors[0] # Nothing to merge. + merge_processors_coverage["merge_processors_3"] = True return _MergedProcessor(processors) diff --git a/tests/test_commonprefix_coverage.py b/tests/test_commonprefix_coverage.py new file mode 100644 index 000000000..72a595f92 --- /dev/null +++ b/tests/test_commonprefix_coverage.py @@ -0,0 +1,28 @@ +from __future__ import annotations +from typing import Iterable, List, Tuple +from prompt_toolkit.completion.base import branch_coverage,_commonprefix + +def print_coverage(): + print("\nCoverage report:") + hit_branches = sum(branch_coverage.values()) + total_branches = len(branch_coverage) + coverage_percentage = (hit_branches / total_branches) * 100 + for branch, hit in branch_coverage.items(): + print(f"{branch} was {'hit' if hit else 'not hit'}") + print(f"Coverage: {hit_branches}/{total_branches} branches hit ({coverage_percentage:.2f}%)\n") + + +# Tests +test_cases: List[Tuple[List[str], str]] = [ + (["car", "car"], "Test 1: Same words"), + ([], "Test 2: Empty list"), + (["car", "dog"], "Test 2: Different words") +] + +for strings, description in test_cases: + print(f"\nTesting case: {description} - Input: {strings}") + result = _commonprefix(strings) + print("Common prefix:", result) + +print_coverage() + diff --git a/tests/test_find_next_matching_line.py b/tests/test_find_next_matching_line.py new file mode 100644 index 000000000..7795d2b26 --- /dev/null +++ b/tests/test_find_next_matching_line.py @@ -0,0 +1,53 @@ +from prompt_toolkit.document import Document, branch_coverage_next + +def print_coverage(): + print("\nCoverage report:") + hit_branches = sum(branch_coverage_next.values()) + total_branches = len(branch_coverage_next) + coverage_percentage = (hit_branches / total_branches) * 100 + for branch, hit in branch_coverage_next.items(): + print(f"{branch} was {'hit' if hit else 'not hit'}") + print(f"Coverage: {hit_branches}/{total_branches} branches hit ({coverage_percentage:.2f}%)\n") + +test_cases = [ + { + "description": "Find the second next empty line", + "text": "line 1\n\nline 3\n\nline 5", + "cursor_position": 0, + "match_func": lambda line: line.strip() == "", + "count": 2, + "expected_result": 3 + }, + { + "description": "No match found", + "text": "line 1\nline 2\nline 3\nline 4", + "cursor_position": 0, + "match_func": lambda line: line.strip() == "", + "count": 1, + "expected_result": None + }, + { + "description": "Match after cursor position", + "text": "line 1\nline 2\n\nline 4", + "cursor_position": 7, + "match_func": lambda line: line.strip() == "", + "count": 1, + "expected_result": 1 + } +] + +for case in test_cases: + document = Document(text=case["text"], cursor_position=case["cursor_position"]) + result = document.find_next_matching_line(case["match_func"], case["count"]) + assert result == case["expected_result"], f"Test failed for: {case['description']}" + + print("\n") + print(f"Test case: {case['description']}") + print(f"Expected result: {case['expected_result']}") + print(f"Actual result: {result}") + print("Branches hit:") + for branch, hit in branch_coverage_next.items(): + if hit: + print(f" {branch}") + +print_coverage() \ No newline at end of file diff --git a/tests/test_find_prev_matching_line.py b/tests/test_find_prev_matching_line.py new file mode 100644 index 000000000..ba2f875b6 --- /dev/null +++ b/tests/test_find_prev_matching_line.py @@ -0,0 +1,53 @@ +from prompt_toolkit.document import Document, branch_coverage_prev + +def print_coverage(): + print("\nCoverage report:") + hit_branches = sum(branch_coverage_prev.values()) + total_branches = len(branch_coverage_prev) + coverage_percentage = (hit_branches / total_branches) * 100 + for branch, hit in branch_coverage_prev.items(): + print(f"{branch} was {'hit' if hit else 'not hit'}") + print(f"Coverage: {hit_branches}/{total_branches} branches hit ({coverage_percentage:.2f}%)\n") + +test_cases = [ + { + "description": "Find the second previous empty line", + "text": "line 1\n\nline 3\n\nline 5", + "cursor_position": 18, + "match_func": lambda line: line.strip() == "", + "count": 2, + "expected_result": -3 + }, + { + "description": "No match found", + "text": "line 1\nline 2\nline 3\nline 4", + "cursor_position": 10, + "match_func": lambda line: line.strip() == "", + "count": 1, + "expected_result": None + }, + { + "description": "Match before cursor position", + "text": "line 1\n\nline 3\nline 4", + "cursor_position": 18, + "match_func": lambda line: line.strip() == "", + "count": 1, + "expected_result": -2 + } +] + +for case in test_cases: + document = Document(text=case["text"], cursor_position=case["cursor_position"]) + result = document.find_previous_matching_line(case["match_func"], case["count"]) + assert result == case["expected_result"], f"Test failed for: {case['description']}" + + print("\n") + print(f"Test case: {case['description']}") + print(f"Expected result: {case['expected_result']}") + print(f"Actual result: {result}") + print("Branches hit:") + for branch, hit in branch_coverage_prev.items(): + if hit: + print(f" {branch}") + +print_coverage() \ No newline at end of file diff --git a/tests/test_insert_line_above.py b/tests/test_insert_line_above.py new file mode 100644 index 000000000..d2abd7c23 --- /dev/null +++ b/tests/test_insert_line_above.py @@ -0,0 +1,33 @@ +from prompt_toolkit.buffer import Buffer, branch_coverage_insert + + +BUF = Buffer() + +test_cases = [ + (True, "Test 1: Insert line above with copy_margin=True"), + (False, "Test 2: Insert line above with copy_margin=False"), +] + + +def print_coverage(): + print("\nCoverage report:") + hit_branches = sum(branch_coverage_insert.values()) + total_branches = len(branch_coverage_insert) + coverage_percentage = (hit_branches / total_branches) * 100 + for branch, hit in branch_coverage_insert.items(): + print(f"{branch} was {'hit' if hit else 'not hit'}") + print( + f"Coverage: {hit_branches}/{total_branches} branches hit ({coverage_percentage:.2f}%)\n" + ) + + +def test_insert_line_above(_buffer): + for copy_margin, description in test_cases: + try: + _buffer.insert_line_above(copy_margin) + except Exception as e: + print(f"{description}: Failed with exception: {e}") + + +test_insert_line_above(BUF) +print_coverage() diff --git a/tests/test_is_dimension_coverage.py b/tests/test_is_dimension_coverage.py new file mode 100644 index 000000000..4892262b9 --- /dev/null +++ b/tests/test_is_dimension_coverage.py @@ -0,0 +1,27 @@ +from prompt_toolkit.layout.dimension import branch_coverage,is_dimension + + +def print_coverage(): + print("\nCoverage report:") + hit_branches = sum(branch_coverage.values()) + total_branches = len(branch_coverage) + coverage_percentage = (hit_branches / total_branches) * 100 + for branch, hit in branch_coverage.items(): + print(f"{branch} was {'hit' if hit else 'not hit'}") + print(f"Coverage: {hit_branches}/{total_branches} branches hit ({coverage_percentage:.2f}%)\n") + + +# Tests +test_cases = [ + (None, "Test 1: None value"), + (52, "Test 2: Integer value"), + (print_coverage, "Test 3: Regular function"), + ("not a dimension", "Test 4: String value (invalid dimension)") +] + +for value, description in test_cases: + print(f"\nTesting case: {description}") + result = is_dimension(value) + print(f"Result: {result}") + +print_coverage() \ No newline at end of file diff --git a/tests/test_merge_processors.py b/tests/test_merge_processors.py new file mode 100644 index 000000000..31e6f802b --- /dev/null +++ b/tests/test_merge_processors.py @@ -0,0 +1,28 @@ +from prompt_toolkit.layout.processors import merge_processors, merge_processors_coverage +from prompt_toolkit.layout.processors import Processor, DummyProcessor, _MergedProcessor + +def print_coverage(): + print("\nCoverage report:") + hit_branches = sum(merge_processors_coverage.values()) + total_branches = len(merge_processors_coverage) + coverage_percentage = (hit_branches / total_branches) * 100 + for branch, hit in merge_processors_coverage.items(): + print(f"{branch} was {'hit' if hit else 'not hit'}") + print(f"Coverage: {hit_branches}/{total_branches} branches hit ({coverage_percentage:.2f}%)\n") + +class MockProcessor(Processor): + def apply_transformation(self, *args, **kwargs): + pass + +test_cases = [ + ([], "Empty list of processors (should hit Branch 1)"), + ([MockProcessor()], "Single processor (should hit Branch 2)"), + ([MockProcessor(), MockProcessor()], "Multiple processors (should hit Branch 3)") +] + +for processors, description in test_cases: + print(f"\nTesting case: {description}") + result = merge_processors(processors) + print(f"Result: {result}") + +print_coverage() diff --git a/tests/test_to_dimension.py b/tests/test_to_dimension.py new file mode 100644 index 000000000..28fe7d8fa --- /dev/null +++ b/tests/test_to_dimension.py @@ -0,0 +1,29 @@ +from prompt_toolkit.layout.dimension import Dimension, to_dimension, to_dimension_coverage + +def print_coverage(): + print("\nCoverage report:") + hit_branches = sum(to_dimension_coverage.values()) + total_branches = len(to_dimension_coverage) + coverage_percentage = (hit_branches / total_branches) * 100 + for branch, hit in to_dimension_coverage.items(): + print(f"{branch} was {'hit' if hit else 'not hit'}") + print(f"Coverage: {hit_branches}/{total_branches} branches hit ({coverage_percentage:.2f}%)\n") + + +test_cases = [ + (None, "None value (should hit Branch 1)"), + (69, "Integer value (should hit Branch 2)"), + (Dimension(), "Dimension instance (should hit Branch 3)"), + (lambda: 42, "Callable returning an integer (should hit Branch 4)"), + ("Unsupported type", "Unsupported type (should hit Branch 5)") +] + +for value, description in test_cases: + print(f"\nTesting case: {description}") + try: + result = to_dimension(value) + print(f"Result: {result}") + except ValueError as e: + print(f"Exception: {e}") + +print_coverage() diff --git a/tests/test_translate_row_col_to_index.py b/tests/test_translate_row_col_to_index.py new file mode 100644 index 000000000..a55535e99 --- /dev/null +++ b/tests/test_translate_row_col_to_index.py @@ -0,0 +1,44 @@ +from prompt_toolkit.document import Document, branch_coverage_translate + + +DOC = Document( + "line 1\n" + "line 2\n" + "line 3\n" + "line 4\n", len("line 1\n" + "lin") +) + + +test_cases = [ + ((0, 0), "Test 1: Basic case with first row and first column"), + ((0, 5), "Test 2: First row with a valid column index within range"), + ((1, 0), "Test 3: Second row, first column"), + ((-1000, 0), "Test 4: Negative row index"), + ((0, -1), "Test 5: Negative column index"), + ((10, 0), "Test 6: Row index out of range (greater than number of lines)"), + ((0, 100), "Test 7: Column index out of range (greater than line length)"), + ((10, 100), "Test 8: Both row and column indices out of range"), + ((-1, -1), "Test 9: Both row and column indices negative"), +] + + +def print_coverage(): + print("\nCoverage report:") + hit_branches = sum(branch_coverage_translate.values()) + total_branches = len(branch_coverage_translate) + coverage_percentage = (hit_branches / total_branches) * 100 + for branch, hit in branch_coverage_translate.items(): + print(f"{branch} was {'hit' if hit else 'not hit'}") + print( + f"Coverage: {hit_branches}/{total_branches} branches hit ({coverage_percentage:.2f}%)\n" + ) + + +def test(document): + for (row, col), description in test_cases: + try: + result = document.translate_row_col_to_index(row, col) + print(f"{description}: Success, Result = {result}") + except Exception as e: + print(f"{description}: Failed with exception: {e}") + + +test(DOC) +print_coverage()