1919from unittest .mock import patch
2020import pytest
2121import yaml
22- from version_scanner import ConfigManager , scan_file , write_csv_report
22+ from version_scanner import (
23+ ConfigManager ,
24+ scan_file ,
25+ write_csv_report ,
26+ _truncate_context ,
27+ _wrap_sheet_hyperlink ,
28+ _wrap_sheet_string ,
29+ format_for_raw_csv ,
30+ format_for_spreadsheet ,
31+ format_for_console
32+ )
2333
2434# Test ConfigManager
2535@pytest .mark .parametrize ("dependency, version, expected" , [
@@ -156,7 +166,7 @@ def test_write_csv_report(tmp_path):
156166 assert rows [0 ]["file_path" ] == "./setup.py"
157167 assert rows [0 ]["rule_name" ] == "python_requires_check"
158168 assert rows [0 ]["line_number" ] == "1"
159- assert rows [0 ]["matched_string" ] == '= "python_requires = \ ' >=3.7\' "'
169+ assert rows [0 ]["matched_string" ] == "python_requires = '>=3.7'"
160170 assert rows [0 ]["context_line" ] == "python_requires = '>=3.7'"
161171
162172
@@ -246,43 +256,7 @@ def test_main_package_file_not_found(capsys):
246256 assert excinfo .value .code == 1
247257 captured = capsys .readouterr ()
248258 assert "Error: Package file not found" in captured .err
249- def test_format_match_for_csv ():
250- from version_scanner import format_match_for_csv
251- match = {
252- "file_path" : "google-cloud-python/main/packages/pkg_a/setup.py" ,
253- "repo_path" : "packages/pkg_a/setup.py" ,
254- "line_number" : 123 ,
255- "rule_name" : "test_rule"
256- }
257-
258- # Test without github_repo
259- formatted = format_match_for_csv (match )
260- assert formatted ["line_number" ] == 123
261-
262- # Test with github_repo
263- formatted = format_match_for_csv (match , github_repo = "https://github.com/user/repo" , branch = "main" )
264- expected_url = "https://github.com/user/repo/blob/main/packages/pkg_a/setup.py#L123"
265- assert formatted ["line_number" ] == f'=HYPERLINK("{ expected_url } ", "123")'
266-
267259
268- def test_format_match_for_csv_truncates_long_line ():
269- from version_scanner import format_match_for_csv
270-
271- long_line = "a" * 1000 + "PY37" + "b" * 1000
272- match = {
273- "file_path" : "test.py" ,
274- "line_number" : 1 ,
275- "rule_name" : "test_rule" ,
276- "matched_string" : "PY37" ,
277- "context_line" : long_line
278- }
279-
280- formatted = format_match_for_csv (match )
281- context = formatted ["context_line" ]
282-
283- assert len (context ) <= 600
284- assert "PY37" in context
285- assert "..." in context
286260
287261
288262def test_get_match_counts ():
@@ -315,30 +289,7 @@ def test_scan_file_removes_newline_from_match(tmp_path):
315289 assert "\n " not in results [0 ]["matched_string" ]
316290
317291
318- def test_write_csv_report_with_links (tmp_path ):
319- output_file = tmp_path / "report.csv"
320- matches = [
321- {
322- "file_path" : "google-cloud-python/main/packages/pkg_a/setup.py" ,
323- "repo_path" : "packages/pkg_a/setup.py" ,
324- "line_number" : 1 ,
325- "rule_name" : "python_requires_check" ,
326- "matched_string" : "python_requires = '>=3.7'" ,
327- "context_line" : "python_requires = '>=3.7'"
328- }
329- ]
330-
331- from version_scanner import write_csv_report
332- write_csv_report (str (output_file ), matches , github_repo = "https://github.com/user/repo" , branch = "main" )
333-
334- assert output_file .exists ()
335-
336- with open (output_file , 'r' , encoding = 'utf-8' , newline = '' ) as f :
337- reader = csv .DictReader (f )
338- rows = list (reader )
339-
340- assert len (rows ) == 1
341- assert "HYPERLINK" in rows [0 ]["line_number" ]
292+
342293def test_scan_repository_ignores_version_scanner (tmp_path ):
343294 vs_dir = tmp_path / "version_scanner"
344295 vs_dir .mkdir ()
@@ -500,6 +451,18 @@ def test_main_exit_code_1():
500451 main ()
501452 assert excinfo .value .code == 1
502453
454+
455+ def test_main_soft_fail_exit_code_0 ():
456+ """Test that main() calls sys.exit(0) when matches are found but --soft-fail is set."""
457+ test_args = ['version_scanner.py' , '-d' , 'python' , '-v' , '3.7' , '--soft-fail' ]
458+ with mock .patch ('sys.argv' , test_args ):
459+ from version_scanner import main
460+ with mock .patch ('version_scanner.scan_repository' , return_value = [{'file_path' : 'test' , 'line_number' : 1 , 'matched_string' : '3.7' , 'rule_name' : 'test' }]):
461+ with pytest .raises (SystemExit ) as excinfo :
462+ main ()
463+ assert excinfo .value .code == 0
464+
465+
503466def test_main_stdout (capsys ):
504467 """Test that --stdout prints the CSV output to stdout."""
505468 test_args = ['version_scanner.py' , '-d' , 'python' , '-v' , '3.7' , '--stdout' ]
@@ -510,10 +473,8 @@ def test_main_stdout(capsys):
510473 main ()
511474
512475 captured = capsys .readouterr ()
513- assert "=== CSV Output ===" in captured .out
514- assert "test.py," in captured .out
515- assert "test," in captured .out
516- assert '"=""3.7"""' in captured .out
476+ assert "=== Scan Results ===" in captured .out
477+ assert "test.py:1 [test] 3.7" in captured .out
517478
518479def test_scan_file_truncation_bug (tmp_path ):
519480 """Test that searching for 3.1 does NOT match 3.10 (truncation bug)."""
@@ -579,3 +540,90 @@ def test_scan_repository_package_name_roots(tmp_path):
579540 assert len (results ) == 1
580541 assert results [0 ]["package_name" ] == "pkg_third"
581542 assert "third_party/pkg_third/setup.py" in results [0 ]["file_path" ]
543+
544+
545+ # --- Decoupled Formatters Tests (TDD) ---
546+
547+ def test_truncate_context ():
548+ # Context shorter than 500 characters shouldn't be truncated
549+ assert _truncate_context ("short context" , "short" ) == "short context"
550+
551+ # Context longer than 500 characters should be truncated around the matched string
552+ matched = "TARGET_VERSION"
553+ long_prefix = "a" * 300
554+ long_suffix = "b" * 300
555+ long_context = long_prefix + matched + long_suffix
556+
557+ truncated = _truncate_context (long_context , matched )
558+ assert len (truncated ) <= 500
559+ assert matched in truncated
560+ assert truncated .startswith ("..." )
561+ assert truncated .endswith ("..." )
562+
563+ def test_wrap_sheet_hyperlink ():
564+ assert _wrap_sheet_hyperlink ("https://github.com/foo" , "12" ) == '=HYPERLINK("https://github.com/foo", "12")'
565+
566+ def test_wrap_sheet_string ():
567+ assert _wrap_sheet_string ("3.10" ) == '="3.10"'
568+ assert _wrap_sheet_string ("" ) == ""
569+ assert _wrap_sheet_string (None ) == ""
570+
571+ def test_format_for_raw_csv ():
572+ match = {
573+ "file_path" : "google-cloud-python/main/packages/pkg_a/setup.py" ,
574+ "repo_path" : "packages/pkg_a/setup.py" ,
575+ "package_name" : "pkg_a" ,
576+ "rule_name" : "python_requires_check" ,
577+ "line_number" : "123" ,
578+ "matched_string" : "3.7" ,
579+ "context_line" : "python_requires = '>=3.7'"
580+ }
581+
582+ formatted = format_for_raw_csv (match )
583+
584+ assert formatted ["file_path" ] == "google-cloud-python/main/packages/pkg_a/setup.py"
585+ assert formatted ["package_name" ] == "pkg_a"
586+ assert formatted ["rule_name" ] == "python_requires_check"
587+ assert formatted ["line_number" ] == 123 # Int conversion
588+ assert formatted ["matched_string" ] == "3.7" # No formula wrapping
589+ assert formatted ["context_line" ] == "python_requires = '>=3.7'"
590+
591+ def test_format_for_spreadsheet ():
592+ match = {
593+ "file_path" : "google-cloud-python/main/packages/pkg_a/setup.py" ,
594+ "repo_path" : "packages/pkg_a/setup.py" ,
595+ "package_name" : "pkg_a" ,
596+ "rule_name" : "python_requires_check" ,
597+ "line_number" : 123 ,
598+ "matched_string" : "3.7" ,
599+ "context_line" : "python_requires = '>=3.7'"
600+ }
601+
602+ # Without github_repo
603+ formatted_no_repo = format_for_spreadsheet (match )
604+ assert formatted_no_repo ["line_number" ] == 123
605+ assert formatted_no_repo ["matched_string" ] == '="3.7"' # Decimal protection formula
606+
607+ # With github_repo
608+ formatted_repo = format_for_spreadsheet (match , github_repo = "https://github.com/user/repo" , branch = "main" )
609+ expected_url = "https://github.com/user/repo/blob/main/packages/pkg_a/setup.py#L123"
610+ assert formatted_repo ["line_number" ] == f'=HYPERLINK("{ expected_url } ", "123")'
611+ assert formatted_repo ["matched_string" ] == '="3.7"'
612+
613+ def test_format_for_console ():
614+ match = {
615+ "file_path" : "google-cloud-python/main/packages/pkg_a/setup.py" ,
616+ "repo_path" : "packages/pkg_a/setup.py" ,
617+ "package_name" : "pkg_a" ,
618+ "rule_name" : "python_requires_check" ,
619+ "line_number" : 123 ,
620+ "matched_string" : "3.7" ,
621+ "context_line" : "python_requires = '>=3.7'"
622+ }
623+
624+ log_str = format_for_console (match )
625+ assert "google-cloud-python/main/packages/pkg_a/setup.py:123" in log_str
626+ assert "[python_requires_check]" in log_str
627+ assert "3.7" in log_str
628+ assert "python_requires = " not in log_str # Slim format doesn't print context line
629+
0 commit comments