532 lines
18 KiB
Python
532 lines
18 KiB
Python
"""
|
|
Tests for repomix_batch.py
|
|
|
|
Run with: pytest test_repomix_batch.py -v --cov=repomix_batch --cov-report=term-missing
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch, mock_open, MagicMock
|
|
import pytest
|
|
|
|
# Add parent directory to path to import the module
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from repomix_batch import (
|
|
RepomixConfig,
|
|
EnvLoader,
|
|
RepomixBatchProcessor,
|
|
load_repositories_from_file,
|
|
main
|
|
)
|
|
|
|
|
|
class TestRepomixConfig:
|
|
"""Test RepomixConfig dataclass."""
|
|
|
|
def test_default_values(self):
|
|
"""Test default configuration values."""
|
|
config = RepomixConfig()
|
|
assert config.style == "xml"
|
|
assert config.output_dir == "repomix-output"
|
|
assert config.remove_comments is False
|
|
assert config.include_pattern is None
|
|
assert config.ignore_pattern is None
|
|
assert config.no_security_check is False
|
|
assert config.verbose is False
|
|
|
|
def test_custom_values(self):
|
|
"""Test custom configuration values."""
|
|
config = RepomixConfig(
|
|
style="markdown",
|
|
output_dir="custom-output",
|
|
remove_comments=True,
|
|
include_pattern="src/**",
|
|
ignore_pattern="tests/**",
|
|
no_security_check=True,
|
|
verbose=True
|
|
)
|
|
assert config.style == "markdown"
|
|
assert config.output_dir == "custom-output"
|
|
assert config.remove_comments is True
|
|
assert config.include_pattern == "src/**"
|
|
assert config.ignore_pattern == "tests/**"
|
|
assert config.no_security_check is True
|
|
assert config.verbose is True
|
|
|
|
|
|
class TestEnvLoader:
|
|
"""Test EnvLoader class."""
|
|
|
|
def test_parse_env_file_basic(self, tmp_path):
|
|
"""Test parsing basic .env file."""
|
|
env_file = tmp_path / ".env"
|
|
env_file.write_text("KEY1=value1\nKEY2=value2\n")
|
|
|
|
result = EnvLoader._parse_env_file(env_file)
|
|
assert result == {"KEY1": "value1", "KEY2": "value2"}
|
|
|
|
def test_parse_env_file_with_quotes(self, tmp_path):
|
|
"""Test parsing .env file with quoted values."""
|
|
env_file = tmp_path / ".env"
|
|
env_file.write_text('KEY1="value with spaces"\nKEY2=\'single quotes\'\n')
|
|
|
|
result = EnvLoader._parse_env_file(env_file)
|
|
assert result == {"KEY1": "value with spaces", "KEY2": "single quotes"}
|
|
|
|
def test_parse_env_file_with_comments(self, tmp_path):
|
|
"""Test parsing .env file with comments."""
|
|
env_file = tmp_path / ".env"
|
|
env_file.write_text("# Comment\nKEY1=value1\n\n# Another comment\nKEY2=value2\n")
|
|
|
|
result = EnvLoader._parse_env_file(env_file)
|
|
assert result == {"KEY1": "value1", "KEY2": "value2"}
|
|
|
|
def test_parse_env_file_with_empty_lines(self, tmp_path):
|
|
"""Test parsing .env file with empty lines."""
|
|
env_file = tmp_path / ".env"
|
|
env_file.write_text("KEY1=value1\n\n\nKEY2=value2\n")
|
|
|
|
result = EnvLoader._parse_env_file(env_file)
|
|
assert result == {"KEY1": "value1", "KEY2": "value2"}
|
|
|
|
def test_parse_env_file_with_equals_in_value(self, tmp_path):
|
|
"""Test parsing .env file with equals sign in value."""
|
|
env_file = tmp_path / ".env"
|
|
env_file.write_text("KEY1=value=with=equals\n")
|
|
|
|
result = EnvLoader._parse_env_file(env_file)
|
|
assert result == {"KEY1": "value=with=equals"}
|
|
|
|
def test_parse_env_file_invalid(self, tmp_path):
|
|
"""Test parsing invalid .env file."""
|
|
env_file = tmp_path / ".env"
|
|
env_file.write_text("INVALID LINE WITHOUT EQUALS\n")
|
|
|
|
result = EnvLoader._parse_env_file(env_file)
|
|
assert result == {}
|
|
|
|
def test_parse_env_file_not_found(self, tmp_path):
|
|
"""Test parsing non-existent .env file."""
|
|
env_file = tmp_path / "nonexistent.env"
|
|
result = EnvLoader._parse_env_file(env_file)
|
|
assert result == {}
|
|
|
|
@patch.dict(os.environ, {"PROCESS_VAR": "from_process"}, clear=True)
|
|
def test_load_env_files_process_env_priority(self):
|
|
"""Test that process environment has highest priority."""
|
|
with patch.object(Path, 'exists', return_value=False):
|
|
env_vars = EnvLoader.load_env_files()
|
|
assert env_vars.get("PROCESS_VAR") == "from_process"
|
|
|
|
|
|
class TestRepomixBatchProcessor:
|
|
"""Test RepomixBatchProcessor class."""
|
|
|
|
def test_init(self):
|
|
"""Test processor initialization."""
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
assert processor.config == config
|
|
assert isinstance(processor.env_vars, dict)
|
|
|
|
@patch("subprocess.run")
|
|
def test_check_repomix_installed_success(self, mock_run):
|
|
"""Test checking if repomix is installed (success)."""
|
|
mock_run.return_value = Mock(returncode=0)
|
|
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
assert processor.check_repomix_installed() is True
|
|
|
|
mock_run.assert_called_once()
|
|
args = mock_run.call_args
|
|
assert args[0][0] == ["repomix", "--version"]
|
|
|
|
@patch("subprocess.run")
|
|
def test_check_repomix_installed_failure(self, mock_run):
|
|
"""Test checking if repomix is installed (failure)."""
|
|
mock_run.return_value = Mock(returncode=1)
|
|
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
assert processor.check_repomix_installed() is False
|
|
|
|
@patch("subprocess.run")
|
|
def test_check_repomix_installed_not_found(self, mock_run):
|
|
"""Test checking if repomix is not found."""
|
|
mock_run.side_effect = FileNotFoundError()
|
|
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
assert processor.check_repomix_installed() is False
|
|
|
|
def test_get_extension(self):
|
|
"""Test getting file extension for style."""
|
|
assert RepomixBatchProcessor._get_extension("xml") == "xml"
|
|
assert RepomixBatchProcessor._get_extension("markdown") == "md"
|
|
assert RepomixBatchProcessor._get_extension("json") == "json"
|
|
assert RepomixBatchProcessor._get_extension("plain") == "txt"
|
|
assert RepomixBatchProcessor._get_extension("unknown") == "xml"
|
|
|
|
def test_build_command_local(self):
|
|
"""Test building command for local repository."""
|
|
config = RepomixConfig(style="markdown", remove_comments=True)
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
output_file = Path("output.md")
|
|
cmd = processor._build_command("/path/to/repo", output_file, is_remote=False)
|
|
|
|
assert cmd[0] == "repomix"
|
|
assert "/path/to/repo" in cmd
|
|
assert "--style" in cmd
|
|
assert "markdown" in cmd
|
|
assert "--remove-comments" in cmd
|
|
assert "-o" in cmd
|
|
|
|
def test_build_command_remote(self):
|
|
"""Test building command for remote repository."""
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
output_file = Path("output.xml")
|
|
cmd = processor._build_command("owner/repo", output_file, is_remote=True)
|
|
|
|
assert cmd[0] == "npx"
|
|
assert "repomix" in cmd
|
|
assert "--remote" in cmd
|
|
assert "owner/repo" in cmd
|
|
|
|
def test_build_command_with_patterns(self):
|
|
"""Test building command with include/ignore patterns."""
|
|
config = RepomixConfig(
|
|
include_pattern="src/**/*.ts",
|
|
ignore_pattern="tests/**"
|
|
)
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
output_file = Path("output.xml")
|
|
cmd = processor._build_command("/path/to/repo", output_file, is_remote=False)
|
|
|
|
assert "--include" in cmd
|
|
assert "src/**/*.ts" in cmd
|
|
assert "-i" in cmd
|
|
assert "tests/**" in cmd
|
|
|
|
def test_build_command_verbose(self):
|
|
"""Test building command with verbose flag."""
|
|
config = RepomixConfig(verbose=True)
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
output_file = Path("output.xml")
|
|
cmd = processor._build_command("/path/to/repo", output_file, is_remote=False)
|
|
|
|
assert "--verbose" in cmd
|
|
|
|
def test_build_command_no_security_check(self):
|
|
"""Test building command with security check disabled."""
|
|
config = RepomixConfig(no_security_check=True)
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
output_file = Path("output.xml")
|
|
cmd = processor._build_command("/path/to/repo", output_file, is_remote=False)
|
|
|
|
assert "--no-security-check" in cmd
|
|
|
|
@patch("subprocess.run")
|
|
@patch("pathlib.Path.mkdir")
|
|
def test_process_repository_success(self, mock_mkdir, mock_run):
|
|
"""Test processing repository successfully."""
|
|
mock_run.return_value = Mock(returncode=0)
|
|
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
success, message = processor.process_repository("/path/to/repo")
|
|
|
|
assert success is True
|
|
assert "Successfully processed" in message
|
|
mock_mkdir.assert_called_once()
|
|
mock_run.assert_called_once()
|
|
|
|
@patch("subprocess.run")
|
|
@patch("pathlib.Path.mkdir")
|
|
def test_process_repository_failure(self, mock_mkdir, mock_run):
|
|
"""Test processing repository with failure."""
|
|
mock_run.return_value = Mock(
|
|
returncode=1,
|
|
stderr="Error message",
|
|
stdout=""
|
|
)
|
|
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
success, message = processor.process_repository("/path/to/repo")
|
|
|
|
assert success is False
|
|
assert "Failed to process" in message
|
|
assert "Error message" in message
|
|
|
|
@patch("subprocess.run")
|
|
@patch("pathlib.Path.mkdir")
|
|
def test_process_repository_timeout(self, mock_mkdir, mock_run):
|
|
"""Test processing repository with timeout."""
|
|
mock_run.side_effect = subprocess.TimeoutExpired(cmd=[], timeout=300)
|
|
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
success, message = processor.process_repository("/path/to/repo")
|
|
|
|
assert success is False
|
|
assert "Timeout" in message
|
|
|
|
@patch("subprocess.run")
|
|
@patch("pathlib.Path.mkdir")
|
|
def test_process_repository_exception(self, mock_mkdir, mock_run):
|
|
"""Test processing repository with exception."""
|
|
mock_run.side_effect = Exception("Unexpected error")
|
|
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
success, message = processor.process_repository("/path/to/repo")
|
|
|
|
assert success is False
|
|
assert "Error processing" in message
|
|
assert "Unexpected error" in message
|
|
|
|
@patch("subprocess.run")
|
|
@patch("pathlib.Path.mkdir")
|
|
def test_process_repository_with_custom_output(self, mock_mkdir, mock_run):
|
|
"""Test processing repository with custom output name."""
|
|
mock_run.return_value = Mock(returncode=0)
|
|
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
success, message = processor.process_repository(
|
|
"/path/to/repo",
|
|
output_name="custom-output.xml"
|
|
)
|
|
|
|
assert success is True
|
|
assert "custom-output.xml" in message
|
|
|
|
@patch("subprocess.run")
|
|
@patch("pathlib.Path.mkdir")
|
|
def test_process_repository_remote(self, mock_mkdir, mock_run):
|
|
"""Test processing remote repository."""
|
|
mock_run.return_value = Mock(returncode=0)
|
|
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
success, message = processor.process_repository(
|
|
"owner/repo",
|
|
is_remote=True
|
|
)
|
|
|
|
assert success is True
|
|
cmd = mock_run.call_args[0][0]
|
|
assert "npx" in cmd
|
|
assert "--remote" in cmd
|
|
|
|
@patch.object(RepomixBatchProcessor, "process_repository")
|
|
def test_process_batch_success(self, mock_process):
|
|
"""Test processing batch of repositories."""
|
|
mock_process.return_value = (True, "Success")
|
|
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
repositories = [
|
|
{"path": "/repo1"},
|
|
{"path": "/repo2", "output": "custom.xml"},
|
|
{"path": "owner/repo", "remote": True}
|
|
]
|
|
|
|
results = processor.process_batch(repositories)
|
|
|
|
assert len(results["success"]) == 3
|
|
assert len(results["failed"]) == 0
|
|
assert mock_process.call_count == 3
|
|
|
|
@patch.object(RepomixBatchProcessor, "process_repository")
|
|
def test_process_batch_with_failures(self, mock_process):
|
|
"""Test processing batch with some failures."""
|
|
mock_process.side_effect = [
|
|
(True, "Success 1"),
|
|
(False, "Failed"),
|
|
(True, "Success 2")
|
|
]
|
|
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
repositories = [
|
|
{"path": "/repo1"},
|
|
{"path": "/repo2"},
|
|
{"path": "/repo3"}
|
|
]
|
|
|
|
results = processor.process_batch(repositories)
|
|
|
|
assert len(results["success"]) == 2
|
|
assert len(results["failed"]) == 1
|
|
|
|
def test_process_batch_missing_path(self):
|
|
"""Test processing batch with missing path."""
|
|
config = RepomixConfig()
|
|
processor = RepomixBatchProcessor(config)
|
|
|
|
repositories = [
|
|
{"output": "custom.xml"} # Missing 'path'
|
|
]
|
|
|
|
results = processor.process_batch(repositories)
|
|
|
|
assert len(results["success"]) == 0
|
|
assert len(results["failed"]) == 1
|
|
assert "Missing 'path'" in results["failed"][0]
|
|
|
|
|
|
class TestLoadRepositoriesFromFile:
|
|
"""Test load_repositories_from_file function."""
|
|
|
|
def test_load_valid_json(self, tmp_path):
|
|
"""Test loading valid JSON file."""
|
|
json_file = tmp_path / "repos.json"
|
|
repos = [
|
|
{"path": "/repo1"},
|
|
{"path": "owner/repo", "remote": True}
|
|
]
|
|
json_file.write_text(json.dumps(repos))
|
|
|
|
result = load_repositories_from_file(str(json_file))
|
|
assert result == repos
|
|
|
|
def test_load_invalid_json(self, tmp_path):
|
|
"""Test loading invalid JSON file."""
|
|
json_file = tmp_path / "invalid.json"
|
|
json_file.write_text("not valid json {")
|
|
|
|
result = load_repositories_from_file(str(json_file))
|
|
assert result == []
|
|
|
|
def test_load_non_array_json(self, tmp_path):
|
|
"""Test loading JSON file with non-array content."""
|
|
json_file = tmp_path / "object.json"
|
|
json_file.write_text('{"path": "/repo"}')
|
|
|
|
result = load_repositories_from_file(str(json_file))
|
|
assert result == []
|
|
|
|
def test_load_nonexistent_file(self):
|
|
"""Test loading non-existent file."""
|
|
result = load_repositories_from_file("/nonexistent/file.json")
|
|
assert result == []
|
|
|
|
|
|
class TestMain:
|
|
"""Test main function."""
|
|
|
|
@patch("sys.argv", ["repomix_batch.py", "/repo1", "/repo2"])
|
|
@patch.object(RepomixBatchProcessor, "check_repomix_installed", return_value=True)
|
|
@patch.object(RepomixBatchProcessor, "process_batch")
|
|
def test_main_with_repos(self, mock_process_batch, mock_check):
|
|
"""Test main function with repository arguments."""
|
|
mock_process_batch.return_value = {"success": ["msg1", "msg2"], "failed": []}
|
|
|
|
result = main()
|
|
|
|
assert result == 0
|
|
mock_check.assert_called_once()
|
|
mock_process_batch.assert_called_once()
|
|
|
|
# Verify repositories passed
|
|
call_args = mock_process_batch.call_args[0][0]
|
|
assert len(call_args) == 2
|
|
assert call_args[0]["path"] == "/repo1"
|
|
assert call_args[1]["path"] == "/repo2"
|
|
|
|
@patch("sys.argv", ["repomix_batch.py", "-f", "repos.json"])
|
|
@patch.object(RepomixBatchProcessor, "check_repomix_installed", return_value=True)
|
|
@patch.object(RepomixBatchProcessor, "process_batch")
|
|
@patch("repomix_batch.load_repositories_from_file")
|
|
def test_main_with_file(self, mock_load, mock_process_batch, mock_check):
|
|
"""Test main function with file argument."""
|
|
mock_load.return_value = [{"path": "/repo1"}]
|
|
mock_process_batch.return_value = {"success": ["msg1"], "failed": []}
|
|
|
|
result = main()
|
|
|
|
assert result == 0
|
|
mock_load.assert_called_once_with("repos.json")
|
|
mock_process_batch.assert_called_once()
|
|
|
|
@patch("sys.argv", ["repomix_batch.py"])
|
|
@patch.object(RepomixBatchProcessor, "check_repomix_installed", return_value=True)
|
|
def test_main_no_repos(self, mock_check):
|
|
"""Test main function with no repositories."""
|
|
result = main()
|
|
assert result == 1
|
|
|
|
@patch("sys.argv", ["repomix_batch.py", "/repo1"])
|
|
@patch.object(RepomixBatchProcessor, "check_repomix_installed", return_value=False)
|
|
def test_main_repomix_not_installed(self, mock_check):
|
|
"""Test main function when repomix is not installed."""
|
|
result = main()
|
|
assert result == 1
|
|
|
|
@patch("sys.argv", ["repomix_batch.py", "/repo1"])
|
|
@patch.object(RepomixBatchProcessor, "check_repomix_installed", return_value=True)
|
|
@patch.object(RepomixBatchProcessor, "process_batch")
|
|
def test_main_with_failures(self, mock_process_batch, mock_check):
|
|
"""Test main function with processing failures."""
|
|
mock_process_batch.return_value = {
|
|
"success": ["msg1"],
|
|
"failed": ["error1"]
|
|
}
|
|
|
|
result = main()
|
|
assert result == 1
|
|
|
|
@patch("sys.argv", [
|
|
"repomix_batch.py",
|
|
"/repo1",
|
|
"--style", "markdown",
|
|
"--remove-comments",
|
|
"--verbose"
|
|
])
|
|
@patch.object(RepomixBatchProcessor, "check_repomix_installed", return_value=True)
|
|
@patch.object(RepomixBatchProcessor, "process_batch")
|
|
def test_main_with_options(self, mock_process_batch, mock_check):
|
|
"""Test main function with various options."""
|
|
mock_process_batch.return_value = {"success": ["msg1"], "failed": []}
|
|
|
|
result = main()
|
|
assert result == 0
|
|
|
|
# Verify config passed to processor
|
|
# The processor is created inside main, so we check it was called
|
|
mock_process_batch.assert_called_once()
|
|
|
|
@patch("sys.argv", ["repomix_batch.py", "/repo1", "--remote"])
|
|
@patch.object(RepomixBatchProcessor, "check_repomix_installed", return_value=True)
|
|
@patch.object(RepomixBatchProcessor, "process_batch")
|
|
def test_main_with_remote_flag(self, mock_process_batch, mock_check):
|
|
"""Test main function with --remote flag."""
|
|
mock_process_batch.return_value = {"success": ["msg1"], "failed": []}
|
|
|
|
result = main()
|
|
assert result == 0
|
|
|
|
# Verify remote flag is set
|
|
call_args = mock_process_batch.call_args[0][0]
|
|
assert call_args[0]["remote"] is True
|