Skip to content

Commit 0952729

Browse files
committed
feat: complete Phase 1 of version_scanner project
Adds distinct exit codes, --stdout support, workflow file, and fixes 3.10 truncation.
1 parent 59fe7cf commit 0952729

4 files changed

Lines changed: 104 additions & 3 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Version Scanner
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: '0 0 * * *' # Run daily at midnight UTC
7+
pull_request:
8+
9+
jobs:
10+
scan:
11+
name: Run Version Scanner
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: '3.12'
20+
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install pyyaml
25+
26+
- name: Run Version Scanner
27+
run: |
28+
python scripts/version_scanner/version_scanner.py -d python -v 3.7 --stdout

scripts/version_scanner/regex_config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ rules:
8787
- "Python3.7"
8888
rules:
8989
- |
90-
python3\.{minor}
90+
python3\.{minor}(?!\d)
9191
9292
- name: combined_version_string
9393
description: Finds combined version strings often used in class or variable names.
@@ -97,6 +97,6 @@ rules:
9797
- "Python37DeprecationWarning"
9898
rules:
9999
- |
100-
Python{major}{minor}
100+
Python{major}{minor}(?!\d)
101101
102102

scripts/version_scanner/tests/unit/test_version_scanner.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,10 @@ def test_main_loads_ignore_from_script_dir(mock_scan, mock_load_ignore):
376376

377377
with mock.patch('sys.argv', test_args):
378378
from version_scanner import main
379-
main()
379+
try:
380+
main()
381+
except SystemExit:
382+
pass
380383

381384
mock_load_ignore.assert_called_once()
382385
args, kwargs = mock_load_ignore.call_args
@@ -385,6 +388,13 @@ def test_main_loads_ignore_from_script_dir(mock_scan, mock_load_ignore):
385388
assert "scripts/version_scanner" in path
386389

387390

391+
try:
392+
import googleapiclient
393+
HAS_GOOGLE_API = True
394+
except ImportError:
395+
HAS_GOOGLE_API = False
396+
397+
@pytest.mark.skipif(not HAS_GOOGLE_API, reason="Requires googleapiclient")
388398
@mock.patch('googleapiclient.discovery.build')
389399
@mock.patch('google.auth.default')
390400
def test_upload_to_drive(mock_auth, mock_build):
@@ -479,6 +489,48 @@ def test_regex_examples_from_config():
479489
break
480490
assert matched, f"Example '{example}' in group '{name}' did not match any pattern."
481491

492+
def test_main_exit_code_1():
493+
"""Test that main() calls sys.exit(1) when matches are found."""
494+
# We can mock scan_repository to return a dummy match
495+
test_args = ['version_scanner.py', '-d', 'python', '-v', '3.7']
496+
with mock.patch('sys.argv', test_args):
497+
from version_scanner import main
498+
with mock.patch('version_scanner.scan_repository', return_value=[{'file_path': 'test', 'line_number': 1, 'matched_string': '3.7', 'rule_name': 'test'}]):
499+
with pytest.raises(SystemExit) as excinfo:
500+
main()
501+
assert excinfo.value.code == 1
502+
503+
def test_main_stdout(capsys):
504+
"""Test that --stdout prints the CSV output to stdout."""
505+
test_args = ['version_scanner.py', '-d', 'python', '-v', '3.7', '--stdout']
506+
with mock.patch('sys.argv', test_args):
507+
from version_scanner import main
508+
with mock.patch('version_scanner.scan_repository', return_value=[{'file_path': 'test.py', 'line_number': 1, 'matched_string': '3.7', 'rule_name': 'test'}]):
509+
with pytest.raises(SystemExit):
510+
main()
511+
512+
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
517+
518+
def test_scan_file_truncation_bug(tmp_path):
519+
"""Test that searching for 3.1 does NOT match 3.10 (truncation bug)."""
520+
# Create a file with 3.10
521+
test_file = tmp_path / "test_file.py"
522+
test_file.write_text("python_requires = '>=3.10'\npython3.10\nPython310\n")
523+
524+
from version_scanner import ConfigManager, scan_file
525+
526+
# Init config for 3.1
527+
config_manager = ConfigManager("python", "3.1")
528+
config_manager.load_config("regex_config.yaml")
529+
530+
# It should not match anything because all strings are 3.10, not 3.1
531+
matches = scan_file(str(test_file), config_manager.rules)
532+
assert len(matches) == 0, f"Expected 0 matches for 3.1 in 3.10 content, but got {len(matches)}: {matches}"
533+
482534

483535
def test_scan_repository_layout_agnostic(tmp_path):
484536
# Create directories under different roots

scripts/version_scanner/version_scanner.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,12 @@ def main():
601601
help="Upload results to a Google Sheet in Drive"
602602
)
603603

604+
parser.add_argument(
605+
"--stdout",
606+
action="store_true",
607+
help="Print the full CSV report to stdout instead of/in addition to writing to a file"
608+
)
609+
604610
args = parser.parse_args()
605611

606612
# Resolve target packages if filtering is requested
@@ -670,5 +676,20 @@ def main():
670676
if args.upload:
671677
upload_to_drive(output_path, all_matches, github_repo=args.github_repo, branch=args.branch)
672678

679+
if args.stdout:
680+
print("\n=== CSV Output ===")
681+
import io
682+
output = io.StringIO()
683+
write_csv_report(output_path, all_matches, github_repo=args.github_repo, branch=args.branch) # this writes to the file, but we want stdout too
684+
# Let's just read the file and print it
685+
with open(output_path, 'r', encoding='utf-8') as f:
686+
print(f.read(), end='')
687+
688+
# Distinct exit codes for CI/CD
689+
if all_matches:
690+
sys.exit(1)
691+
else:
692+
sys.exit(0)
693+
673694
if __name__ == "__main__":
674695
main()

0 commit comments

Comments
 (0)