Spaces:
Build error
Build error
| """Comprehensive tests for diff_compressor.py. | |
| Tests cover: | |
| 1. Parsing of unified diff format | |
| 2. Context line reduction | |
| 3. Hunk selection and limiting | |
| 4. Compression ratios | |
| 5. Edge cases | |
| """ | |
| from headroom.transforms.diff_compressor import ( | |
| DiffCompressionResult, | |
| DiffCompressor, | |
| DiffCompressorConfig, | |
| DiffFile, | |
| DiffHunk, | |
| ) | |
| class TestDiffParsing: | |
| """Tests for parsing unified diff format.""" | |
| def test_parse_simple_diff(self): | |
| """Simple single-file diff is parsed correctly.""" | |
| content = """diff --git a/src/main.py b/src/main.py | |
| --- a/src/main.py | |
| +++ b/src/main.py | |
| @@ -10,6 +10,7 @@ def main(): | |
| print("hello") | |
| + print("world") | |
| return 0 | |
| """ | |
| compressor = DiffCompressor() | |
| diff_files = compressor._parse_diff(content.split("\n")) | |
| assert len(diff_files) == 1 | |
| assert diff_files[0].header == "diff --git a/src/main.py b/src/main.py" | |
| assert diff_files[0].old_file == "--- a/src/main.py" | |
| assert diff_files[0].new_file == "+++ b/src/main.py" | |
| assert len(diff_files[0].hunks) == 1 | |
| assert diff_files[0].hunks[0].additions == 1 | |
| assert diff_files[0].hunks[0].deletions == 0 | |
| def test_parse_multi_file_diff(self): | |
| """Multi-file diff is parsed into separate DiffFile objects.""" | |
| content = """diff --git a/file1.py b/file1.py | |
| --- a/file1.py | |
| +++ b/file1.py | |
| @@ -1,3 +1,4 @@ | |
| line1 | |
| +added line | |
| line2 | |
| diff --git a/file2.py b/file2.py | |
| --- a/file2.py | |
| +++ b/file2.py | |
| @@ -5,4 +5,3 @@ | |
| keep | |
| -removed | |
| keep2 | |
| """ | |
| compressor = DiffCompressor() | |
| diff_files = compressor._parse_diff(content.split("\n")) | |
| assert len(diff_files) == 2 | |
| assert "file1.py" in diff_files[0].header | |
| assert "file2.py" in diff_files[1].header | |
| assert diff_files[0].hunks[0].additions == 1 | |
| assert diff_files[0].hunks[0].deletions == 0 | |
| assert diff_files[1].hunks[0].additions == 0 | |
| assert diff_files[1].hunks[0].deletions == 1 | |
| def test_parse_multi_hunk_file(self): | |
| """File with multiple hunks is parsed correctly.""" | |
| content = """diff --git a/src/utils.py b/src/utils.py | |
| --- a/src/utils.py | |
| +++ b/src/utils.py | |
| @@ -10,4 +10,5 @@ def helper(): | |
| pass | |
| + # added comment | |
| return True | |
| @@ -50,3 +51,4 @@ def other(): | |
| x = 1 | |
| + y = 2 | |
| return x | |
| """ | |
| compressor = DiffCompressor() | |
| diff_files = compressor._parse_diff(content.split("\n")) | |
| assert len(diff_files) == 1 | |
| assert len(diff_files[0].hunks) == 2 | |
| assert diff_files[0].total_additions == 2 | |
| def test_parse_new_file(self): | |
| """New file diff is detected.""" | |
| content = """diff --git a/newfile.py b/newfile.py | |
| new file mode 100644 | |
| --- /dev/null | |
| +++ b/newfile.py | |
| @@ -0,0 +1,3 @@ | |
| +def new_func(): | |
| + pass | |
| + return None | |
| """ | |
| compressor = DiffCompressor() | |
| diff_files = compressor._parse_diff(content.split("\n")) | |
| assert len(diff_files) == 1 | |
| assert diff_files[0].is_new_file is True | |
| assert diff_files[0].hunks[0].additions == 3 | |
| def test_parse_deleted_file(self): | |
| """Deleted file diff is detected.""" | |
| content = """diff --git a/oldfile.py b/oldfile.py | |
| deleted file mode 100644 | |
| --- a/oldfile.py | |
| +++ /dev/null | |
| @@ -1,2 +0,0 @@ | |
| -def old_func(): | |
| - pass | |
| """ | |
| compressor = DiffCompressor() | |
| diff_files = compressor._parse_diff(content.split("\n")) | |
| assert len(diff_files) == 1 | |
| assert diff_files[0].is_deleted_file is True | |
| assert diff_files[0].hunks[0].deletions == 2 | |
| def test_parse_binary_file(self): | |
| """Binary file diff is detected.""" | |
| content = """diff --git a/image.png b/image.png | |
| Binary files a/image.png and b/image.png differ | |
| """ | |
| compressor = DiffCompressor() | |
| diff_files = compressor._parse_diff(content.split("\n")) | |
| assert len(diff_files) == 1 | |
| assert diff_files[0].is_binary is True | |
| class TestContextReduction: | |
| """Tests for context line reduction.""" | |
| def test_reduce_context_lines(self): | |
| """Context lines are reduced to configured maximum.""" | |
| content = """diff --git a/file.py b/file.py | |
| --- a/file.py | |
| +++ b/file.py | |
| @@ -1,10 +1,11 @@ | |
| context1 | |
| context2 | |
| context3 | |
| context4 | |
| +added | |
| context5 | |
| context6 | |
| context7 | |
| context8 | |
| """ | |
| # Default max_context_lines is 2 | |
| compressor = DiffCompressor( | |
| config=DiffCompressorConfig( | |
| max_context_lines=2, | |
| min_lines_for_ccr=5, | |
| enable_ccr=False, | |
| ) | |
| ) | |
| result = compressor.compress(content) | |
| # Should keep 2 context before and 2 after the +added line | |
| # Plus the added line itself | |
| lines = result.compressed.split("\n") | |
| context_count = sum(1 for line in lines if line.startswith(" ")) | |
| # At most 4 context lines (2 before + 2 after) | |
| assert context_count <= 4 | |
| def test_preserve_all_changes(self): | |
| """All addition and deletion lines are preserved.""" | |
| content = """diff --git a/file.py b/file.py | |
| --- a/file.py | |
| +++ b/file.py | |
| @@ -1,10 +1,10 @@ | |
| ctx1 | |
| ctx2 | |
| -removed1 | |
| +added1 | |
| ctx3 | |
| ctx4 | |
| -removed2 | |
| +added2 | |
| ctx5 | |
| ctx6 | |
| """ | |
| compressor = DiffCompressor( | |
| config=DiffCompressorConfig( | |
| min_lines_for_ccr=5, | |
| enable_ccr=False, | |
| ) | |
| ) | |
| result = compressor.compress(content) | |
| assert "-removed1" in result.compressed | |
| assert "-removed2" in result.compressed | |
| assert "+added1" in result.compressed | |
| assert "+added2" in result.compressed | |
| class TestHunkSelection: | |
| """Tests for hunk selection when limiting.""" | |
| def test_max_hunks_per_file(self): | |
| """Hunks are limited to max_hunks_per_file.""" | |
| # Create a diff with many hunks | |
| hunks = [] | |
| for i in range(20): | |
| hunks.append(f"""@@ -{i * 10},3 +{i * 10},4 @@ | |
| context | |
| +added_{i} | |
| more | |
| """) | |
| content = f"""diff --git a/bigfile.py b/bigfile.py | |
| --- a/bigfile.py | |
| +++ b/bigfile.py | |
| {"".join(hunks)}""" | |
| compressor = DiffCompressor( | |
| config=DiffCompressorConfig( | |
| max_hunks_per_file=5, | |
| min_lines_for_ccr=10, | |
| enable_ccr=False, | |
| ) | |
| ) | |
| result = compressor.compress(content) | |
| # Should have at most 5 hunks | |
| hunk_count = result.compressed.count("@@") | |
| # Each hunk has one @@ header (we count full hunk headers) | |
| assert hunk_count <= 10 # Each hunk header appears twice @@...@@ | |
| def test_keeps_first_and_last_hunk(self): | |
| """First and last hunks are preserved when limiting.""" | |
| hunks = [] | |
| for i in range(10): | |
| hunks.append(f"""@@ -{i * 10},3 +{i * 10},4 @@ | |
| context | |
| +added_{i} | |
| more | |
| """) | |
| content = f"""diff --git a/file.py b/file.py | |
| --- a/file.py | |
| +++ b/file.py | |
| {"".join(hunks)}""" | |
| compressor = DiffCompressor( | |
| config=DiffCompressorConfig( | |
| max_hunks_per_file=3, | |
| min_lines_for_ccr=10, | |
| enable_ccr=False, | |
| ) | |
| ) | |
| result = compressor.compress(content) | |
| # First hunk (added_0) should be present | |
| assert "+added_0" in result.compressed | |
| # Last hunk (added_9) should be present | |
| assert "+added_9" in result.compressed | |
| class TestFileSelection: | |
| """Tests for file selection when limiting.""" | |
| def test_max_files(self): | |
| """Files are limited to max_files.""" | |
| # Create diff with many files | |
| files = [] | |
| for i in range(30): | |
| files.append(f"""diff --git a/file{i}.py b/file{i}.py | |
| --- a/file{i}.py | |
| +++ b/file{i}.py | |
| @@ -1,2 +1,3 @@ | |
| ctx | |
| +added | |
| ctx2 | |
| """) | |
| content = "\n".join(files) | |
| compressor = DiffCompressor( | |
| config=DiffCompressorConfig( | |
| max_files=10, | |
| min_lines_for_ccr=20, | |
| enable_ccr=False, | |
| ) | |
| ) | |
| result = compressor.compress(content) | |
| # Count diff --git headers | |
| file_count = result.compressed.count("diff --git") | |
| assert file_count <= 10 | |
| class TestCompressionResult: | |
| """Tests for DiffCompressionResult properties.""" | |
| def test_compression_ratio_calculation(self): | |
| """Compression ratio is calculated correctly.""" | |
| result = DiffCompressionResult( | |
| compressed="a\nb\nc", | |
| original_line_count=100, | |
| compressed_line_count=10, | |
| files_affected=2, | |
| additions=5, | |
| deletions=3, | |
| hunks_kept=2, | |
| hunks_removed=5, | |
| ) | |
| assert result.compression_ratio == 0.1 | |
| def test_tokens_saved_estimate(self): | |
| """Token savings estimation works correctly.""" | |
| result = DiffCompressionResult( | |
| compressed="short", | |
| original_line_count=100, | |
| compressed_line_count=10, | |
| files_affected=1, | |
| additions=10, | |
| deletions=5, | |
| hunks_kept=1, | |
| hunks_removed=0, | |
| ) | |
| # 90 lines saved * 40 chars/line / 4 chars/token = 900 tokens | |
| assert result.tokens_saved_estimate == 900 | |
| class TestHunkScoring: | |
| """Tests for context-aware hunk scoring.""" | |
| def test_score_by_context_keywords(self): | |
| """Hunks containing context keywords get higher scores.""" | |
| content = """diff --git a/file.py b/file.py | |
| --- a/file.py | |
| +++ b/file.py | |
| @@ -1,3 +1,4 @@ | |
| normal context | |
| +normal change | |
| more context | |
| @@ -10,3 +11,4 @@ | |
| error handling | |
| +fix the bug here | |
| return result | |
| """ | |
| compressor = DiffCompressor() | |
| diff_files = compressor._parse_diff(content.split("\n")) | |
| compressor._score_hunks(diff_files, "fix error bug") | |
| # Second hunk should have higher score (contains "fix" and "bug") | |
| assert len(diff_files[0].hunks) == 2 | |
| assert diff_files[0].hunks[1].score > diff_files[0].hunks[0].score | |
| def test_score_priority_patterns(self): | |
| """Hunks with priority patterns (error, security) score higher.""" | |
| compressor = DiffCompressor() | |
| hunk_normal = DiffHunk( | |
| header="@@ -1,1 +1,2 @@", | |
| lines=["+normal change"], | |
| additions=1, | |
| ) | |
| hunk_error = DiffHunk( | |
| header="@@ -10,1 +10,2 @@", | |
| lines=["+fix critical error"], | |
| additions=1, | |
| ) | |
| diff_file = DiffFile( | |
| header="diff --git a/f.py b/f.py", | |
| old_file="--- a/f.py", | |
| new_file="+++ b/f.py", | |
| hunks=[hunk_normal, hunk_error], | |
| ) | |
| compressor._score_hunks([diff_file], "") | |
| assert hunk_error.score > hunk_normal.score | |
| class TestSmallDiffPassthrough: | |
| """Tests for small diff passthrough behavior.""" | |
| def test_small_diff_unchanged(self): | |
| """Diffs smaller than threshold pass through unchanged.""" | |
| content = """diff --git a/small.py b/small.py | |
| --- a/small.py | |
| +++ b/small.py | |
| @@ -1,2 +1,3 @@ | |
| line1 | |
| +added | |
| line2 | |
| """ | |
| compressor = DiffCompressor( | |
| config=DiffCompressorConfig( | |
| min_lines_for_ccr=100, # High threshold | |
| ) | |
| ) | |
| result = compressor.compress(content) | |
| # Should be unchanged | |
| assert result.compressed == content | |
| assert result.compression_ratio == 1.0 | |
| class TestOutputFormatting: | |
| """Tests for output formatting.""" | |
| def test_summary_line_added(self): | |
| """Summary line is added at end of compressed diff.""" | |
| # Large diff that will be compressed | |
| hunks = [] | |
| for i in range(15): | |
| hunks.append(f"""@@ -{i * 10},5 +{i * 10},6 @@ | |
| ctx1 | |
| ctx2 | |
| +added_{i} | |
| ctx3 | |
| ctx4 | |
| """) | |
| content = f"""diff --git a/file.py b/file.py | |
| --- a/file.py | |
| +++ b/file.py | |
| {"".join(hunks)}""" | |
| compressor = DiffCompressor( | |
| config=DiffCompressorConfig( | |
| max_hunks_per_file=5, | |
| min_lines_for_ccr=10, | |
| enable_ccr=False, | |
| ) | |
| ) | |
| result = compressor.compress(content) | |
| # Should have summary at end | |
| assert "files changed" in result.compressed | |
| assert "hunks omitted" in result.compressed | |
| def test_preserves_diff_format(self): | |
| """Output preserves valid unified diff format.""" | |
| content = """diff --git a/test.py b/test.py | |
| --- a/test.py | |
| +++ b/test.py | |
| @@ -1,3 +1,4 @@ | |
| def test(): | |
| + # new comment | |
| pass | |
| return True | |
| """ | |
| compressor = DiffCompressor( | |
| config=DiffCompressorConfig( | |
| min_lines_for_ccr=5, | |
| enable_ccr=False, | |
| ) | |
| ) | |
| result = compressor.compress(content) | |
| # Should have all standard diff markers | |
| assert "diff --git" in result.compressed | |
| assert "---" in result.compressed | |
| assert "+++" in result.compressed | |
| assert "@@" in result.compressed | |
| class TestEdgeCases: | |
| """Tests for edge cases and boundary conditions.""" | |
| def test_empty_input(self): | |
| """Empty input is handled gracefully.""" | |
| compressor = DiffCompressor() | |
| result = compressor.compress("") | |
| assert result.compressed == "" | |
| assert result.compression_ratio == 1.0 | |
| def test_non_diff_input(self): | |
| """Non-diff input passes through unchanged.""" | |
| content = "This is not a diff\nJust regular text" | |
| compressor = DiffCompressor() | |
| result = compressor.compress(content) | |
| # Should pass through (no diff --git found) | |
| assert result.compressed == content | |
| def test_unicode_content(self): | |
| """Unicode characters in diff are handled.""" | |
| content = """diff --git a/i18n.py b/i18n.py | |
| --- a/i18n.py | |
| +++ b/i18n.py | |
| @@ -1,2 +1,3 @@ | |
| msg = "hello" | |
| +msg_ja = "こんにちは" | |
| return msg | |
| """ | |
| compressor = DiffCompressor() | |
| result = compressor.compress(content) | |
| assert "こんにちは" in result.compressed | |
| def test_no_newline_at_eof(self): | |
| """Handles 'No newline at end of file' indicator.""" | |
| content = """diff --git a/file.py b/file.py | |
| --- a/file.py | |
| +++ b/file.py | |
| @@ -1,2 +1,2 @@ | |
| line1 | |
| -line2 | |
| \\ No newline at end of file | |
| +line2_modified | |
| \\ No newline at end of file | |
| """ | |
| compressor = DiffCompressor() | |
| result = compressor.compress(content) | |
| # Should not crash and preserve the indicator | |
| assert "No newline" in result.compressed or "-line2" in result.compressed | |
| def test_empty_hunks(self): | |
| """Files with no actual hunks are handled.""" | |
| content = """diff --git a/file.py b/file.py | |
| --- a/file.py | |
| +++ b/file.py | |
| """ | |
| compressor = DiffCompressor() | |
| result = compressor.compress(content) | |
| # Should not crash | |
| assert result.compressed is not None | |
| class TestDiffHunkDataclass: | |
| """Tests for DiffHunk dataclass.""" | |
| def test_change_count_property(self): | |
| """change_count returns sum of additions and deletions.""" | |
| hunk = DiffHunk( | |
| header="@@ -1,5 +1,6 @@", | |
| lines=["+a", "+b", "-c", " ctx"], | |
| additions=2, | |
| deletions=1, | |
| ) | |
| assert hunk.change_count == 3 | |
| def test_default_values(self): | |
| """DiffHunk default values are correct.""" | |
| hunk = DiffHunk(header="@@", lines=[]) | |
| assert hunk.additions == 0 | |
| assert hunk.deletions == 0 | |
| assert hunk.context_lines == 0 | |
| assert hunk.score == 0.0 | |
| class TestDiffFileDataclass: | |
| """Tests for DiffFile dataclass.""" | |
| def test_total_additions_property(self): | |
| """total_additions sums across all hunks.""" | |
| hunk1 = DiffHunk(header="@@", lines=[], additions=3) | |
| hunk2 = DiffHunk(header="@@", lines=[], additions=5) | |
| diff_file = DiffFile( | |
| header="diff --git", | |
| old_file="---", | |
| new_file="+++", | |
| hunks=[hunk1, hunk2], | |
| ) | |
| assert diff_file.total_additions == 8 | |
| def test_total_deletions_property(self): | |
| """total_deletions sums across all hunks.""" | |
| hunk1 = DiffHunk(header="@@", lines=[], deletions=2) | |
| hunk2 = DiffHunk(header="@@", lines=[], deletions=4) | |
| diff_file = DiffFile( | |
| header="diff --git", | |
| old_file="---", | |
| new_file="+++", | |
| hunks=[hunk1, hunk2], | |
| ) | |
| assert diff_file.total_deletions == 6 | |
| class TestConfigOptions: | |
| """Tests for configuration options.""" | |
| def test_max_context_lines_config(self): | |
| """max_context_lines configuration controls context reduction.""" | |
| content = """diff --git a/file.py b/file.py | |
| --- a/file.py | |
| +++ b/file.py | |
| @@ -1,10 +1,11 @@ | |
| c1 | |
| c2 | |
| c3 | |
| c4 | |
| c5 | |
| +added | |
| c6 | |
| c7 | |
| c8 | |
| c9 | |
| c10 | |
| """ | |
| # With max_context_lines=1 | |
| compressor = DiffCompressor( | |
| config=DiffCompressorConfig( | |
| max_context_lines=1, | |
| min_lines_for_ccr=5, | |
| enable_ccr=False, | |
| ) | |
| ) | |
| result = compressor.compress(content) | |
| # Count context lines (lines starting with space) | |
| context_count = sum(1 for line in result.compressed.split("\n") if line.startswith(" ")) | |
| # Should have at most 2 context lines (1 before + 1 after) | |
| assert context_count <= 2 | |
| def test_always_keep_additions_default(self): | |
| """Additions are always kept by default.""" | |
| content = """diff --git a/file.py b/file.py | |
| --- a/file.py | |
| +++ b/file.py | |
| @@ -1,3 +1,5 @@ | |
| ctx | |
| +add1 | |
| +add2 | |
| ctx | |
| """ | |
| compressor = DiffCompressor( | |
| config=DiffCompressorConfig( | |
| always_keep_additions=True, | |
| min_lines_for_ccr=2, | |
| enable_ccr=False, | |
| ) | |
| ) | |
| result = compressor.compress(content) | |
| assert "+add1" in result.compressed | |
| assert "+add2" in result.compressed | |
| def test_always_keep_deletions_default(self): | |
| """Deletions are always kept by default.""" | |
| content = """diff --git a/file.py b/file.py | |
| --- a/file.py | |
| +++ b/file.py | |
| @@ -1,5 +1,3 @@ | |
| ctx | |
| -del1 | |
| -del2 | |
| ctx | |
| """ | |
| compressor = DiffCompressor( | |
| config=DiffCompressorConfig( | |
| always_keep_deletions=True, | |
| min_lines_for_ccr=2, | |
| enable_ccr=False, | |
| ) | |
| ) | |
| result = compressor.compress(content) | |
| assert "-del1" in result.compressed | |
| assert "-del2" in result.compressed | |