Skip to content

Commit 8b64db7

Browse files
committed
feat(scanner): add --soft-fail CLI flag and integrate in GHA workflow
1 parent ae291ad commit 8b64db7

3 files changed

Lines changed: 182 additions & 130 deletions

File tree

.github/workflows/version_scanner.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- name: Run Version Scanner
3434
run: |
3535
# Use -o to output the raw CSV to a file, and --stdout to print the summary to the GitHub Actions UI
36-
python scripts/version_scanner/version_scanner.py -d python -v 3.7 --stdout -o version_scanner_output.csv
36+
python scripts/version_scanner/version_scanner.py -d python -v 3.7 --stdout -o version_scanner_output.csv --soft-fail
3737
3838
- name: Upload CSV Results
3939
if: always()

scripts/version_scanner/tests/unit/test_version_scanner.py

Lines changed: 114 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,17 @@
1919
from unittest.mock import patch
2020
import pytest
2121
import 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

288262
def 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+
342293
def 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+
503466
def 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

518479
def 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

Comments
 (0)