headroom/tests/test_transforms/test_diff_compressor.py
chopratejas 5c740ea427 Add DiffCompressor and fix hnswlib SIGILL crash on CI
DiffCompressor:
- Parse unified diff format and compress by reducing context lines
- Preserve file headers and all +/- change lines
- Score hunks by relevance (error keywords, query matches)
- Add summary line: [N files, +X -Y lines]
- Expected 30-50% savings on typical git diffs
- Wire into content router for CompressionStrategy.DIFF
- 30 tests covering parsing, compression, edge cases

hnswlib SIGILL fix:
- Move hnswlib import from module level to lazy loading
- hnswlib crashes with SIGILL (Illegal Instruction) on CPUs
  without AVX support, before Python can catch the error
- Now imports only when HNSWVectorIndex is actually used
- HNSW_AVAILABLE is checked lazily via __getattr__

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 08:09:55 -08:00

673 lines
18 KiB
Python

"""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