address test errors

This commit is contained in:
Zhan Li 2025-05-25 00:33:35 -07:00
parent e066b481af
commit 34aa48c0a1
3 changed files with 1489 additions and 92 deletions

1296
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@ authors = [
] ]
description = "Convert a directory structure and its contents into a single text file, including the tree output and file contents in structured XML format. It may be useful to chat with LLM about your code." description = "Convert a directory structure and its contents into a single text file, including the tree output and file contents in structured XML format. It may be useful to chat with LLM about your code."
readme = "README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.9"
license = { text = "MIT" } license = { text = "MIT" }
classifiers = [ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
@ -33,18 +33,29 @@ Repository = "https://github.com/kirill-markin/repo-to-text"
repo-to-text = "repo_to_text.main:main" repo-to-text = "repo_to_text.main:main"
flatten = "repo_to_text.main:main" flatten = "repo_to_text.main:main"
[project.optional-dependencies] #[project.optional-dependencies]
dev = [ #dev = [
"pytest>=8.2.2", # "pytest>=8.2.2",
"black", # "black",
"mypy", # "mypy",
"isort", # "isort",
"build", # "build",
"twine", # "twine",
"pylint", # "pylint",
] #]
[tool.pylint] [tool.pylint]
disable = [ disable = [
"C0303", "C0303",
] ]
[tool.poetry.group.dev.dependencies]
pytest = "^8.3.5"
black = "^25.1.0"
mypy = "^1.15.0"
isort = "^6.0.1"
build = "^1.2.2.post1"
twine = "^6.1.0"
pylint = "^3.3.7"

View file

@ -28,6 +28,47 @@ def temp_dir() -> Generator[str, None, None]:
yield temp_path yield temp_path
shutil.rmtree(temp_path) shutil.rmtree(temp_path)
# Mock tree outputs
# Raw output similar to `tree -a -f --noreport`
MOCK_RAW_TREE_FOR_SAMPLE_REPO = """./
./.gitignore
./.repo-to-text-settings.yaml
./README.md
./src
./src/main.py
./tests
./tests/test_main.py
"""
MOCK_RAW_TREE_SPECIAL_CHARS = """./
./special chars
./special chars/file with spaces.txt
"""
MOCK_RAW_TREE_EMPTY_FILTERING = """./
./src
./src/main.py
./tests
./tests/test_main.py
"""
# Note: ./empty_dir is removed, assuming tree or filter_tree_output would handle it.
# This makes the test focus on the rest of the logic if tree output is as expected.
# Expected output from get_tree_structure (filtered)
MOCK_GTS_OUTPUT_FOR_SAMPLE_REPO = """.
.gitignore
README.md
src
main.py
tests
test_main.py"""
MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO = """.
file1.txt
file2.txt
subdir
file3.txt"""
@pytest.fixture @pytest.fixture
def sample_repo(tmp_path: str) -> str: def sample_repo(tmp_path: str) -> str:
"""Create a sample repository structure for testing.""" """Create a sample repository structure for testing."""
@ -136,9 +177,12 @@ def test_should_ignore_file(sample_repo: str) -> None:
tree_and_content_ignore_spec tree_and_content_ignore_spec
) is False ) is False
def test_get_tree_structure(sample_repo: str) -> None: @patch('repo_to_text.core.core.run_tree_command', return_value=MOCK_RAW_TREE_FOR_SAMPLE_REPO)
@patch('repo_to_text.core.core.check_tree_command', return_value=True)
def test_get_tree_structure(mock_check_tree: MagicMock, mock_run_tree: MagicMock, sample_repo: str) -> None:
"""Test tree structure generation.""" """Test tree structure generation."""
gitignore_spec, _, tree_and_content_ignore_spec = load_ignore_specs(sample_repo) gitignore_spec, _, tree_and_content_ignore_spec = load_ignore_specs(sample_repo)
# The .repo-to-text-settings.yaml in sample_repo ignores itself from tree and content
tree_output = get_tree_structure(sample_repo, gitignore_spec, tree_and_content_ignore_spec) tree_output = get_tree_structure(sample_repo, gitignore_spec, tree_and_content_ignore_spec)
# Basic structure checks # Basic structure checks
@ -147,8 +191,11 @@ def test_get_tree_structure(sample_repo: str) -> None:
assert "main.py" in tree_output assert "main.py" in tree_output
assert "test_main.py" in tree_output assert "test_main.py" in tree_output
assert ".git" not in tree_output assert ".git" not in tree_output
assert ".repo-to-text-settings.yaml" not in tree_output # Should be filtered by tree_and_content_ignore_spec
def test_save_repo_to_text(sample_repo: str) -> None: @patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SAMPLE_REPO)
@patch('repo_to_text.core.core.check_tree_command', return_value=True) # In case any internal call still checks
def test_save_repo_to_text(mock_check_tree: MagicMock, mock_get_tree: MagicMock, sample_repo: str) -> None:
"""Test the main save_repo_to_text function.""" """Test the main save_repo_to_text function."""
# Create output directory # Create output directory
output_dir = os.path.join(sample_repo, "output") output_dir = os.path.join(sample_repo, "output")
@ -162,7 +209,7 @@ def test_save_repo_to_text(sample_repo: str) -> None:
# Test file output # Test file output
output_file = save_repo_to_text(sample_repo, output_dir=output_dir) output_file = save_repo_to_text(sample_repo, output_dir=output_dir)
assert os.path.exists(output_file) assert os.path.exists(output_file)
assert os.path.dirname(output_file) == output_dir assert os.path.abspath(os.path.dirname(output_file)) == os.path.abspath(output_dir)
# Check file contents # Check file contents
with open(output_file, 'r', encoding='utf-8') as f: with open(output_file, 'r', encoding='utf-8') as f:
@ -214,15 +261,20 @@ def test_load_ignore_specs_without_gitignore(temp_dir: str) -> None:
assert content_ignore_spec is None assert content_ignore_spec is None
assert tree_and_content_ignore_spec is not None assert tree_and_content_ignore_spec is not None
def test_get_tree_structure_with_special_chars(temp_dir: str) -> None: @patch('repo_to_text.core.core.run_tree_command', return_value=MOCK_RAW_TREE_SPECIAL_CHARS)
@patch('repo_to_text.core.core.check_tree_command', return_value=True)
def test_get_tree_structure_with_special_chars(mock_check_tree: MagicMock, mock_run_tree: MagicMock, temp_dir: str) -> None:
"""Test tree structure generation with special characters in paths.""" """Test tree structure generation with special characters in paths."""
# Create files with special characters # Create files with special characters
special_dir = os.path.join(temp_dir, "special chars") special_dir = os.path.join(temp_dir, "special chars") # Matches MOCK_RAW_TREE_SPECIAL_CHARS
os.makedirs(special_dir) os.makedirs(special_dir)
with open(os.path.join(special_dir, "file with spaces.txt"), "w", encoding='utf-8') as f: with open(os.path.join(special_dir, "file with spaces.txt"), "w", encoding='utf-8') as f:
f.write("test") f.write("test")
tree_output = get_tree_structure(temp_dir) # load_ignore_specs will be called inside; for temp_dir, they will be None or empty.
gitignore_spec, _, tree_and_content_ignore_spec = load_ignore_specs(temp_dir)
tree_output = get_tree_structure(temp_dir, gitignore_spec, tree_and_content_ignore_spec)
assert "special chars" in tree_output assert "special chars" in tree_output
assert "file with spaces.txt" in tree_output assert "file with spaces.txt" in tree_output
@ -268,7 +320,9 @@ def test_save_repo_to_text_with_binary_files(temp_dir: str) -> None:
expected_content = f"<content full_path=\"binary.bin\">\n{binary_content.decode('latin1')}\n</content>" expected_content = f"<content full_path=\"binary.bin\">\n{binary_content.decode('latin1')}\n</content>"
assert expected_content in output assert expected_content in output
def test_save_repo_to_text_custom_output_dir(temp_dir: str) -> None: @patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO) # Using simple repo tree for generic content
@patch('repo_to_text.core.core.check_tree_command', return_value=True)
def test_save_repo_to_text_custom_output_dir(mock_check_tree: MagicMock, mock_get_tree: MagicMock, temp_dir: str) -> None:
"""Test save_repo_to_text with custom output directory.""" """Test save_repo_to_text with custom output directory."""
# Create a simple file structure # Create a simple file structure
with open(os.path.join(temp_dir, "test.txt"), "w", encoding='utf-8') as f: with open(os.path.join(temp_dir, "test.txt"), "w", encoding='utf-8') as f:
@ -279,8 +333,10 @@ def test_save_repo_to_text_custom_output_dir(temp_dir: str) -> None:
output_file = save_repo_to_text(temp_dir, output_dir=output_dir) output_file = save_repo_to_text(temp_dir, output_dir=output_dir)
assert os.path.exists(output_file) assert os.path.exists(output_file)
assert os.path.dirname(output_file) == output_dir assert os.path.abspath(os.path.dirname(output_file)) == os.path.abspath(output_dir)
assert output_file.startswith(output_dir) # output_file is relative, output_dir is absolute. This assertion needs care.
# Let's assert that the absolute path of output_file starts with absolute output_dir
assert os.path.abspath(output_file).startswith(os.path.abspath(output_dir))
def test_get_tree_structure_empty_directory(temp_dir: str) -> None: def test_get_tree_structure_empty_directory(temp_dir: str) -> None:
"""Test tree structure generation for empty directory.""" """Test tree structure generation for empty directory."""
@ -288,7 +344,9 @@ def test_get_tree_structure_empty_directory(temp_dir: str) -> None:
# Should only contain the directory itself # Should only contain the directory itself
assert tree_output.strip() == "" or tree_output.strip() == temp_dir assert tree_output.strip() == "" or tree_output.strip() == temp_dir
def test_empty_dirs_filtering(tmp_path: str) -> None: @patch('repo_to_text.core.core.run_tree_command', return_value=MOCK_RAW_TREE_EMPTY_FILTERING)
@patch('repo_to_text.core.core.check_tree_command', return_value=True)
def test_empty_dirs_filtering(mock_check_tree: MagicMock, mock_run_tree: MagicMock, tmp_path: str) -> None:
"""Test filtering of empty directories in tree structure generation.""" """Test filtering of empty directories in tree structure generation."""
# Create test directory structure with normalized paths # Create test directory structure with normalized paths
base_path = os.path.normpath(tmp_path) base_path = os.path.normpath(tmp_path)
@ -388,43 +446,47 @@ def test_load_additional_specs_no_settings_file(tmp_path: str) -> None:
assert specs["maximum_word_count_per_file"] is None assert specs["maximum_word_count_per_file"] is None
# Tests for generate_output_content related to splitting # Tests for generate_output_content related to splitting
def test_generate_output_content_no_splitting_max_words_not_set(simple_word_count_repo: str) -> None: @patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO)
def test_generate_output_content_no_splitting_max_words_not_set(mock_get_tree: MagicMock, simple_word_count_repo: str) -> None:
"""Test generate_output_content with no splitting when max_words is not set.""" """Test generate_output_content with no splitting when max_words is not set."""
path = simple_word_count_repo path = simple_word_count_repo
gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path) gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path)
tree_structure = get_tree_structure(path, gitignore_spec, tree_and_content_ignore_spec) # tree_structure is now effectively MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO due to the mock
segments = generate_output_content( segments = generate_output_content(
path, tree_structure, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec, path, MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec,
maximum_word_count_per_file=None maximum_word_count_per_file=None
) )
mock_get_tree.assert_not_called() # We are passing tree_structure directly
assert len(segments) == 1 assert len(segments) == 1
assert "file1.txt" in segments[0] assert "file1.txt" in segments[0]
assert "This is file one." in segments[0] assert "This is file one." in segments[0]
def test_generate_output_content_no_splitting_content_less_than_limit(simple_word_count_repo: str) -> None: @patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO)
def test_generate_output_content_no_splitting_content_less_than_limit(mock_get_tree: MagicMock, simple_word_count_repo: str) -> None:
"""Test generate_output_content with no splitting when content is less than max_words limit.""" """Test generate_output_content with no splitting when content is less than max_words limit."""
path = simple_word_count_repo path = simple_word_count_repo
gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path) gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path)
tree_structure = get_tree_structure(path, gitignore_spec, tree_and_content_ignore_spec)
segments = generate_output_content( segments = generate_output_content(
path, tree_structure, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec, path, MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec,
maximum_word_count_per_file=500 # High limit maximum_word_count_per_file=500 # High limit
) )
mock_get_tree.assert_not_called()
assert len(segments) == 1 assert len(segments) == 1
assert "file1.txt" in segments[0] assert "file1.txt" in segments[0]
def test_generate_output_content_splitting_occurs(simple_word_count_repo: str) -> None: @patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO)
def test_generate_output_content_splitting_occurs(mock_get_tree: MagicMock, simple_word_count_repo: str) -> None:
"""Test generate_output_content when splitting occurs due to max_words limit.""" """Test generate_output_content when splitting occurs due to max_words limit."""
path = simple_word_count_repo path = simple_word_count_repo
gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path) gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path)
tree_structure = get_tree_structure(path, gitignore_spec, tree_and_content_ignore_spec)
max_words = 30 max_words = 30
segments = generate_output_content( segments = generate_output_content(
path, tree_structure, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec, path, MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec,
maximum_word_count_per_file=max_words maximum_word_count_per_file=max_words
) )
mock_get_tree.assert_not_called()
assert len(segments) > 1 assert len(segments) > 1
total_content = "".join(segments) total_content = "".join(segments)
assert "file1.txt" in total_content assert "file1.txt" in total_content
@ -438,33 +500,52 @@ def test_generate_output_content_splitting_occurs(simple_word_count_repo: str) -
else: # Last segment can be smaller else: # Last segment can be smaller
assert segment_word_count > 0 assert segment_word_count > 0
def test_generate_output_content_splitting_very_small_limit(simple_word_count_repo: str) -> None: @patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO)
def test_generate_output_content_splitting_very_small_limit(mock_get_tree: MagicMock, simple_word_count_repo: str) -> None:
"""Test generate_output_content with a very small max_words limit.""" """Test generate_output_content with a very small max_words limit."""
path = simple_word_count_repo path = simple_word_count_repo
gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path) gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path)
tree_structure = get_tree_structure(path, gitignore_spec, tree_and_content_ignore_spec)
max_words = 10 # Very small limit max_words = 10 # Very small limit
segments = generate_output_content( segments = generate_output_content(
path, tree_structure, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec, path, MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec,
maximum_word_count_per_file=max_words maximum_word_count_per_file=max_words
) )
assert len(segments) > 3 # Expect multiple splits mock_get_tree.assert_not_called()
assert len(segments) > 3 # Expect multiple splits due to small limit and multiple chunks
total_content = "".join(segments) total_content = "".join(segments)
assert "file1.txt" in total_content assert "file1.txt" in total_content # Check presence of file name in overall output
# Check if file content (which is a chunk) forms its own segment if it's > max_words
found_file1_content_chunk = False
expected_file1_chunk = "<content full_path=\"file1.txt\">\nThis is file one. It has eight words.\n</content>"
for segment in segments:
if expected_file1_chunk.strip() in segment.strip(): # Check for the core content
# This segment should contain the file1.txt content and its tags
# The chunk itself is ~13 words. If max_words is 10, this chunk will be its own segment.
assert count_words_for_test(segment) == count_words_for_test(expected_file1_chunk)
assert count_words_for_test(segment) > max_words
found_file1_content_chunk = True
break
assert found_file1_content_chunk
def test_generate_output_content_file_header_content_together(tmp_path: str) -> None: raw_file1_content = "This is file one. It has eight words." # 8 words
opening_tag_file1 = '\n<content full_path="file1.txt">\n' # 4 words
closing_tag_file1 = '\n</content>\n' # 2 words
# With max_words = 10:
# Opening tag (4 words) should be in a segment.
# Raw content (8 words) should be in its own segment.
# Closing tag (2 words) should be in a segment (possibly with previous or next small items).
found_raw_content_segment = False
for segment in segments:
if raw_file1_content in segment:
# This segment should ideally contain *only* raw_file1_content if it was split correctly
# or raw_file1_content + closing_tag if they fit together after raw_content forced a split.
# Given max_words=10, raw_content (8 words) + closing_tag (2 words) = 10 words. They *could* be together.
# Let's check if the segment containing raw_file1_content is primarily it.
segment_wc = count_words_for_test(segment)
if raw_file1_content in segment and closing_tag_file1 in segment and opening_tag_file1 not in segment:
assert segment_wc == count_words_for_test(raw_file1_content + closing_tag_file1) # 8 + 2 = 10
found_raw_content_segment = True
break
elif raw_file1_content in segment and closing_tag_file1 not in segment and opening_tag_file1 not in segment:
# This means raw_file_content (8 words) is by itself or with other small parts.
# This case implies the closing tag is in a *subsequent* segment.
assert segment_wc == count_words_for_test(raw_file1_content) # 8 words
found_raw_content_segment = True
break
assert found_raw_content_segment, "Segment with raw file1 content not found or not matching expected structure"
@patch('repo_to_text.core.core.get_tree_structure') # Will use a specific mock inside
def test_generate_output_content_file_header_content_together(mock_get_tree: MagicMock, tmp_path: str) -> None:
"""Test that file header and its content are not split if word count allows.""" """Test that file header and its content are not split if word count allows."""
repo_path = str(tmp_path) repo_path = str(tmp_path)
file_content_str = "word " * 15 # 15 words file_content_str = "word " * 15 # 15 words
@ -477,11 +558,13 @@ def test_generate_output_content_file_header_content_together(tmp_path: str) ->
f.write(content_val) f.write(content_val)
gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(repo_path) gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(repo_path)
tree_structure = get_tree_structure(repo_path, gitignore_spec, tree_and_content_ignore_spec) # Mock the tree structure for this specific test case
mock_tree_for_single_file = ".\n└── single_file.txt"
mock_get_tree.return_value = mock_tree_for_single_file # This mock is for any internal calls if any
max_words_sufficient = 35 # Enough for header + this one file block (around 20 words + initial header) max_words_sufficient = 35 # Enough for header + this one file block (around 20 words + initial header)
segments = generate_output_content( segments = generate_output_content(
repo_path, tree_structure, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec, repo_path, mock_tree_for_single_file, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec,
maximum_word_count_per_file=max_words_sufficient maximum_word_count_per_file=max_words_sufficient
) )
assert len(segments) == 1 # Expect no splitting of this file from its tags assert len(segments) == 1 # Expect no splitting of this file from its tags
@ -491,30 +574,40 @@ def test_generate_output_content_file_header_content_together(tmp_path: str) ->
# Test if it splits if max_words is too small for the file block (20 words) # Test if it splits if max_words is too small for the file block (20 words)
max_words_small = 10 max_words_small = 10
segments_small_limit = generate_output_content( segments_small_limit = generate_output_content(
repo_path, tree_structure, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec, repo_path, mock_tree_for_single_file, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec,
maximum_word_count_per_file=max_words_small maximum_word_count_per_file=max_words_small
) )
# The file block (20 words) is a single chunk. It will form its own segment. # The file block (20 words) is a single chunk. It will form its own segment.
# Header part will be one segment. File block another. Footer another. # Header part will be one segment. File block another. Footer another.
assert len(segments_small_limit) >= 2 assert len(segments_small_limit) >= 2
found_file_block_in_own_segment = False found_raw_content_in_own_segment = False
raw_content_single_file = "word " * 15 # 15 words
# expected_file_block is the whole thing (20 words)
# With max_words_small = 10:
# 1. Opening tag (3 words) -> new segment
# 2. Raw content (15 words) -> new segment (because 0 + 15 > 10)
# 3. Closing tag (2 words) -> new segment (because 0 + 2 <= 10, but follows a large chunk)
for segment in segments_small_limit: for segment in segments_small_limit:
if expected_file_block in segment: if raw_content_single_file.strip() in segment.strip() and \
assert count_words_for_test(segment) == count_words_for_test(expected_file_block) '<content full_path="single_file.txt">' not in segment and \
found_file_block_in_own_segment = True '</content>' not in segment:
# This segment should contain only the raw 15 words
assert count_words_for_test(segment.strip()) == 15
found_raw_content_in_own_segment = True
break break
assert found_file_block_in_own_segment assert found_raw_content_in_own_segment, "Raw content of single_file.txt not found in its own segment"
# Tests for save_repo_to_text related to splitting # Tests for save_repo_to_text related to splitting
@patch('repo_to_text.core.core.load_additional_specs') @patch('repo_to_text.core.core.load_additional_specs')
@patch('repo_to_text.core.core.generate_output_content') @patch('repo_to_text.core.core.generate_output_content')
@patch('repo_to_text.core.core.os.makedirs') @patch('repo_to_text.core.core.os.makedirs')
@patch('builtins.open', new_callable=mock_open) @patch('builtins.open', new_callable=mock_open)
@patch('repo_to_text.core.core.pyperclip.copy') @patch('repo_to_text.core.core.copy_to_clipboard')
def test_save_repo_to_text_no_splitting_mocked( def test_save_repo_to_text_no_splitting_mocked(
mock_pyperclip_copy: MagicMock, mock_copy_to_clipboard: MagicMock,
mock_file_open: MagicMock, # This is the mock_open instance mock_file_open: MagicMock,
mock_makedirs: MagicMock, mock_makedirs: MagicMock,
mock_generate_output: MagicMock, mock_generate_output: MagicMock,
mock_load_specs: MagicMock, mock_load_specs: MagicMock,
@ -531,22 +624,22 @@ def test_save_repo_to_text_no_splitting_mocked(
returned_path = save_repo_to_text(simple_word_count_repo, output_dir=output_dir) returned_path = save_repo_to_text(simple_word_count_repo, output_dir=output_dir)
mock_load_specs.assert_called_once_with(simple_word_count_repo) mock_load_specs.assert_called_once_with(simple_word_count_repo)
mock_generate_output.assert_called_once() # Args are complex, basic check mock_generate_output.assert_called_once()
expected_filename = os.path.join(output_dir, "repo-to-text_mock_timestamp.txt") expected_filename = os.path.join(output_dir, "repo-to-text_mock_timestamp.txt")
assert returned_path == os.path.relpath(expected_filename) assert returned_path == os.path.relpath(expected_filename)
mock_makedirs.assert_called_once_with(output_dir) mock_makedirs.assert_called_once_with(output_dir)
mock_file_open.assert_called_once_with(expected_filename, 'w', encoding='utf-8') mock_file_open.assert_called_once_with(expected_filename, 'w', encoding='utf-8')
mock_file_open().write.assert_called_once_with("Single combined content\nfile1.txt\ncontent1") mock_file_open().write.assert_called_once_with("Single combined content\nfile1.txt\ncontent1")
mock_pyperclip_copy.assert_called_once_with("Single combined content\nfile1.txt\ncontent1") mock_copy_to_clipboard.assert_called_once_with("Single combined content\nfile1.txt\ncontent1")
@patch('repo_to_text.core.core.load_additional_specs') @patch('repo_to_text.core.core.load_additional_specs')
@patch('repo_to_text.core.core.generate_output_content') @patch('repo_to_text.core.core.generate_output_content')
@patch('repo_to_text.core.core.os.makedirs') @patch('repo_to_text.core.core.os.makedirs')
@patch('builtins.open') # Patch builtins.open to get the mock of the function @patch('builtins.open')
@patch('repo_to_text.core.core.pyperclip.copy') @patch('repo_to_text.core.core.copy_to_clipboard')
def test_save_repo_to_text_splitting_occurs_mocked( def test_save_repo_to_text_splitting_occurs_mocked(
mock_pyperclip_copy: MagicMock, mock_copy_to_clipboard: MagicMock,
mock_open_function: MagicMock, # This is the mock for the open function itself mock_open_function: MagicMock,
mock_makedirs: MagicMock, mock_makedirs: MagicMock,
mock_generate_output: MagicMock, mock_generate_output: MagicMock,
mock_load_specs: MagicMock, mock_load_specs: MagicMock,
@ -559,10 +652,8 @@ def test_save_repo_to_text_splitting_occurs_mocked(
mock_generate_output.return_value = segments_content mock_generate_output.return_value = segments_content
output_dir = os.path.join(str(tmp_path), "output_split_adv") output_dir = os.path.join(str(tmp_path), "output_split_adv")
# Mock file handles that 'open' will return when called in a 'with' statement
mock_file_handle1 = MagicMock(spec=IO) mock_file_handle1 = MagicMock(spec=IO)
mock_file_handle2 = MagicMock(spec=IO) mock_file_handle2 = MagicMock(spec=IO)
# Configure the mock_open_function to return these handles sequentially
mock_open_function.side_effect = [mock_file_handle1, mock_file_handle2] mock_open_function.side_effect = [mock_file_handle1, mock_file_handle2]
with patch('repo_to_text.core.core.datetime') as mock_datetime: with patch('repo_to_text.core.core.datetime') as mock_datetime:
@ -571,60 +662,59 @@ def test_save_repo_to_text_splitting_occurs_mocked(
expected_filename_part1 = os.path.join(output_dir, "repo-to-text_mock_ts_split_adv_part_1.txt") expected_filename_part1 = os.path.join(output_dir, "repo-to-text_mock_ts_split_adv_part_1.txt")
expected_filename_part2 = os.path.join(output_dir, "repo-to-text_mock_ts_split_adv_part_2.txt") expected_filename_part2 = os.path.join(output_dir, "repo-to-text_mock_ts_split_adv_part_2.txt")
assert returned_path == os.path.relpath(expected_filename_part1) assert returned_path == os.path.relpath(expected_filename_part1)
mock_makedirs.assert_called_once_with(output_dir) mock_makedirs.assert_called_once_with(output_dir)
# Check calls to the open function
mock_open_function.assert_any_call(expected_filename_part1, 'w', encoding='utf-8') mock_open_function.assert_any_call(expected_filename_part1, 'w', encoding='utf-8')
mock_open_function.assert_any_call(expected_filename_part2, 'w', encoding='utf-8') mock_open_function.assert_any_call(expected_filename_part2, 'w', encoding='utf-8')
assert mock_open_function.call_count == 2 # Exactly two calls for writing output assert mock_open_function.call_count == 2
# Check writes to the mocked file handles (returned by open's side_effect)
# __enter__() is called by the 'with' statement
mock_file_handle1.__enter__().write.assert_called_once_with(segments_content[0]) mock_file_handle1.__enter__().write.assert_called_once_with(segments_content[0])
mock_file_handle2.__enter__().write.assert_called_once_with(segments_content[1]) mock_file_handle2.__enter__().write.assert_called_once_with(segments_content[1])
mock_pyperclip_copy.assert_not_called()
@patch('repo_to_text.core.core.load_additional_specs') mock_copy_to_clipboard.assert_not_called()
@patch('repo_to_text.core.core.generate_output_content')
@patch('repo_to_text.core.core.os.makedirs') @patch('repo_to_text.core.core.copy_to_clipboard')
@patch('builtins.open', new_callable=mock_open) @patch('builtins.open', new_callable=mock_open)
@patch('repo_to_text.core.core.pyperclip.copy') @patch('repo_to_text.core.core.os.makedirs')
@patch('repo_to_text.core.core.generate_output_content') # This is the one that will be used
@patch('repo_to_text.core.core.load_additional_specs') # This is the one that will be used
@patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO)
def test_save_repo_to_text_stdout_with_splitting( def test_save_repo_to_text_stdout_with_splitting(
mock_pyperclip_copy: MagicMock, mock_get_tree: MagicMock, # Order of mock args should match decorator order (bottom-up)
mock_file_open: MagicMock,
mock_os_makedirs: MagicMock,
mock_generate_output: MagicMock,
mock_load_specs: MagicMock, mock_load_specs: MagicMock,
mock_generate_output: MagicMock,
mock_os_makedirs: MagicMock,
mock_file_open: MagicMock,
mock_copy_to_clipboard: MagicMock,
simple_word_count_repo: str, simple_word_count_repo: str,
capsys capsys
) -> None: ) -> None:
"""Test save_repo_to_text with to_stdout=True and content that would split.""" """Test save_repo_to_text with to_stdout=True and content that would split."""
mock_load_specs.return_value = {'maximum_word_count_per_file': 10} # Assume causes splitting mock_load_specs.return_value = {'maximum_word_count_per_file': 10}
mock_generate_output.return_value = ["Segment 1 for stdout.", "Segment 2 for stdout."] mock_generate_output.return_value = ["Segment 1 for stdout.", "Segment 2 for stdout."]
result_string = save_repo_to_text(simple_word_count_repo, to_stdout=True) result_string = save_repo_to_text(simple_word_count_repo, to_stdout=True)
mock_load_specs.assert_called_once_with(simple_word_count_repo) mock_load_specs.assert_called_once_with(simple_word_count_repo)
mock_get_tree.assert_called_once() # Assert that get_tree_structure was called
mock_generate_output.assert_called_once() mock_generate_output.assert_called_once()
mock_os_makedirs.assert_not_called() mock_os_makedirs.assert_not_called()
mock_file_open.assert_not_called() mock_file_open.assert_not_called()
mock_pyperclip_copy.assert_not_called() mock_copy_to_clipboard.assert_not_called()
captured = capsys.readouterr() captured = capsys.readouterr()
# core.py uses print(segment, end=''), so segments are joined directly. assert "Segment 1 for stdout.Segment 2 for stdout." == captured.out.strip() # Added strip() to handle potential newlines from logging
assert "Segment 1 for stdout.Segment 2 for stdout." == captured.out
assert result_string == "Segment 1 for stdout.Segment 2 for stdout." assert result_string == "Segment 1 for stdout.Segment 2 for stdout."
@patch('repo_to_text.core.core.load_additional_specs') @patch('repo_to_text.core.core.load_additional_specs')
@patch('repo_to_text.core.core.generate_output_content') @patch('repo_to_text.core.core.generate_output_content')
@patch('repo_to_text.core.core.os.makedirs') @patch('repo_to_text.core.core.os.makedirs')
@patch('builtins.open', new_callable=mock_open) @patch('builtins.open', new_callable=mock_open)
@patch('repo_to_text.core.core.pyperclip.copy') @patch('repo_to_text.core.core.copy_to_clipboard')
def test_save_repo_to_text_empty_segments( def test_save_repo_to_text_empty_segments(
mock_pyperclip_copy: MagicMock, mock_copy_to_clipboard: MagicMock,
mock_file_open: MagicMock, mock_file_open: MagicMock,
mock_makedirs: MagicMock, mock_makedirs: MagicMock,
mock_generate_output: MagicMock, mock_generate_output: MagicMock,
@ -635,7 +725,7 @@ def test_save_repo_to_text_empty_segments(
) -> None: ) -> None:
"""Test save_repo_to_text when generate_output_content returns no segments.""" """Test save_repo_to_text when generate_output_content returns no segments."""
mock_load_specs.return_value = {'maximum_word_count_per_file': None} mock_load_specs.return_value = {'maximum_word_count_per_file': None}
mock_generate_output.return_value = [] # Empty list mock_generate_output.return_value = []
output_dir = os.path.join(str(tmp_path), "output_empty") output_dir = os.path.join(str(tmp_path), "output_empty")
returned_path = save_repo_to_text(simple_word_count_repo, output_dir=output_dir) returned_path = save_repo_to_text(simple_word_count_repo, output_dir=output_dir)
@ -643,7 +733,7 @@ def test_save_repo_to_text_empty_segments(
assert returned_path == "" assert returned_path == ""
mock_makedirs.assert_not_called() mock_makedirs.assert_not_called()
mock_file_open.assert_not_called() mock_file_open.assert_not_called()
mock_pyperclip_copy.assert_not_called() mock_copy_to_clipboard.assert_not_called()
assert "generate_output_content returned no segments" in caplog.text assert "generate_output_content returned no segments" in caplog.text
if __name__ == "__main__": if __name__ == "__main__":