Skip to content

Commit a002eb1

Browse files
committed
chore(scanner): address PR review comments on limit, safe-int, and string escaping
1 parent cc8cd2d commit a002eb1

2 files changed

Lines changed: 73 additions & 4 deletions

File tree

scripts/version_scanner/tests/unit/test_version_scanner.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
_truncate_context,
2727
_wrap_sheet_hyperlink,
2828
_wrap_sheet_string,
29+
_safe_int,
2930
format_for_raw_csv,
3031
format_for_spreadsheet,
3132
format_for_console
@@ -475,6 +476,42 @@ def test_main_stdout(capsys):
475476
assert "test.py:1 [test] 3.7" in captured.out
476477

477478

479+
def test_main_without_stdout_limits_output(capsys):
480+
"""Test that main() without --stdout prints only 10 matches and shows a suffix."""
481+
test_args = ['version_scanner.py', '-d', 'python', '-v', '3.7']
482+
matches = [{'file_path': f'test_{i}.py', 'line_number': i, 'matched_string': '3.7', 'rule_name': 'test'} for i in range(15)]
483+
with mock.patch('sys.argv', test_args):
484+
from version_scanner import main
485+
with mock.patch('version_scanner.scan_repository', return_value=matches):
486+
with pytest.raises(SystemExit):
487+
main()
488+
489+
captured = capsys.readouterr()
490+
# Should only print first 10 matches
491+
for i in range(10):
492+
assert f"test_{i}.py:{i} [test] 3.7" in captured.out
493+
for i in range(10, 15):
494+
assert f"test_{i}.py:{i} [test] 3.7" not in captured.out
495+
assert "... and 5 more matches." in captured.out
496+
497+
498+
def test_main_with_stdout_prints_all(capsys):
499+
"""Test that main() with --stdout prints all matches without limiting."""
500+
test_args = ['version_scanner.py', '-d', 'python', '-v', '3.7', '--stdout']
501+
matches = [{'file_path': f'test_{i}.py', 'line_number': i, 'matched_string': '3.7', 'rule_name': 'test'} for i in range(15)]
502+
with mock.patch('sys.argv', test_args):
503+
from version_scanner import main
504+
with mock.patch('version_scanner.scan_repository', return_value=matches):
505+
with pytest.raises(SystemExit):
506+
main()
507+
508+
captured = capsys.readouterr()
509+
# Should print all 15 matches
510+
for i in range(15):
511+
assert f"test_{i}.py:{i} [test] 3.7" in captured.out
512+
assert "... and 5 more matches." not in captured.out
513+
514+
478515
def test_main_does_not_print_rules(capsys):
479516
"""Test that main() does not print the list of loaded rules to stdout."""
480517
test_args = ['version_scanner.py', '-d', 'python', '-v', '3.7']
@@ -576,9 +613,29 @@ def test_wrap_sheet_hyperlink():
576613

577614
def test_wrap_sheet_string():
578615
assert _wrap_sheet_string("3.10") == '="3.10"'
616+
assert _wrap_sheet_string('python_requires = ">=3.7"') == '="python_requires = "">=3.7"""'
579617
assert _wrap_sheet_string("") == ""
580618
assert _wrap_sheet_string(None) == ""
581619

620+
def test_safe_int():
621+
assert _safe_int("123") == 123
622+
assert _safe_int("") == 0
623+
assert _safe_int(None) == 0
624+
assert _safe_int("abc") == 0
625+
626+
def test_format_for_raw_csv_handles_empty_line_number():
627+
match = {
628+
"file_path": "google-cloud-python/main/packages/pkg_a/setup.py",
629+
"repo_path": "packages/pkg_a/setup.py",
630+
"package_name": "pkg_a",
631+
"rule_name": "python_requires_check",
632+
"line_number": "",
633+
"matched_string": "3.7",
634+
"context_line": "python_requires = '>=3.7'"
635+
}
636+
formatted = format_for_raw_csv(match)
637+
assert formatted["line_number"] == 0
638+
582639
def test_format_for_raw_csv():
583640
match = {
584641
"file_path": "google-cloud-python/main/packages/pkg_a/setup.py",

scripts/version_scanner/version_scanner.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import os
2424
import re
2525
import sys
26-
from typing import Dict, List, Tuple
26+
from typing import Dict, List, Tuple, Any
2727
import yaml
2828

2929
class ConfigManager:
@@ -208,7 +208,15 @@ def _wrap_sheet_hyperlink(url: str, label: str) -> str:
208208
def _wrap_sheet_string(value: str) -> str:
209209
if value is None:
210210
return ""
211-
return f'="{value}"' if value else ""
211+
escaped_value = value.replace('"', '""')
212+
return f'="{escaped_value}"' if value else ""
213+
214+
215+
def _safe_int(value: Any, default: int = 0) -> int:
216+
try:
217+
return int(value)
218+
except (ValueError, TypeError):
219+
return default
212220

213221

214222
def format_for_raw_csv(match: Dict[str, str]) -> Dict[str, str]:
@@ -217,7 +225,7 @@ def format_for_raw_csv(match: Dict[str, str]) -> Dict[str, str]:
217225
"file_path": match.get("file_path", ""),
218226
"package_name": match.get("package_name", ""),
219227
"rule_name": match.get("rule_name", ""),
220-
"line_number": int(match.get("line_number", 0)) if match.get("line_number") is not None else 0,
228+
"line_number": _safe_int(match.get("line_number")),
221229
"matched_string": match.get("matched_string", ""),
222230
"context_line": _truncate_context(match.get("context_line", ""), match.get("matched_string", ""))
223231
}
@@ -661,9 +669,13 @@ def main():
661669
all_matches = scan_repository(args.path, rules, target_packages, ignore_dirs, version_string=args.version)
662670

663671
print(f"\nFound {len(all_matches)} matches.")
664-
for m in all_matches:
672+
display_matches = all_matches if args.stdout else all_matches[:10]
673+
for m in display_matches:
665674
print(format_for_console(m))
666675

676+
if not args.stdout and len(all_matches) > 10:
677+
print(f" ... and {len(all_matches) - 10} more matches.")
678+
667679
# Get and print summary counts
668680
rule_counts, package_counts = get_match_counts(all_matches)
669681
print_summary_table(rule_counts, package_counts)

0 commit comments

Comments
 (0)