diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 41cd8d6..c98f4f8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,8 @@ jobs: run: tox -e pep8 - name: Check covering run: tox -e cover + - name: Run unit tests + run: tox -e unit - name: Test with tox run: tox diff --git a/.gitignore b/.gitignore index 71bfe8a..50b6eaa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ doc/build .coverage build/ .venv/ -*/__pycache__/ +*__pycache__/ diff --git a/beagle/search.py b/beagle/search.py index 486c4e4..fc0c68f 100644 --- a/beagle/search.py +++ b/beagle/search.py @@ -55,6 +55,13 @@ def get_parser(self, prog_name): dest='file_pattern', help='file name pattern', ) + parser.add_argument( + '--exclude', + dest='exclude', + action='append', + default=[], + help='exclude files or paths matching glob pattern (repeatable)', + ) parser.add_argument( '--ignore-case', default=False, @@ -96,6 +103,12 @@ def check_repo(repo): for repo, repo_matches in sorted(interesting_repos): for repo_match in repo_matches['Matches']: + filename = repo_match['Filename'] + # Exclude files matching any exclude pattern + if parsed_args.exclude: + if any(fnmatch.fnmatch(filename, pat) + for pat in parsed_args.exclude): + continue for file_match in repo_match['Matches']: if (parsed_args.ignore_comments and file_match['Line'].lstrip().startswith( diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_search.py b/tests/test_search.py new file mode 100644 index 0000000..84e4f20 --- /dev/null +++ b/tests/test_search.py @@ -0,0 +1,63 @@ +import unittest + + +class DummyArgs: + def __init__(self, exclude=None, repo_pattern=None, + ignore_comments=False, comment_marker='#', context_lines=0): + self.exclude = exclude or [] + self.repo_pattern = repo_pattern or '' + self.ignore_comments = ignore_comments + self.comment_marker = comment_marker + self.context_lines = context_lines + + +class TestBeagleSearchExclude(unittest.TestCase): + def setUp(self): + # Simulate results as returned by hound.query + self.results = { + 'repo1': { + 'Matches': [ + {'Filename': 'src/main.py', + 'Matches': [{'LineNumber': 1, + 'Line': 'foo', 'Before': [], 'After': []}]}, + {'Filename': 'src/test_utils.py', + 'Matches': [{'LineNumber': 2, + 'Line': 'bar', 'Before': [], 'After': []}]}, + {'Filename': 'docs/readme.py', + 'Matches': [{'LineNumber': 3, 'Line': 'baz', + 'Before': [], 'After': []}]}, + ] + } + } + + def test_exclude_pattern(self): + from beagle.search import Search + search = Search(app=None, app_args=None) + args = DummyArgs(exclude=['*test*', '*doc*']) + found = list(search._flatten_results(self.results, args)) + # Only src/main.py should remain + self.assertEqual(len(found), 1) + self.assertIn('src/main.py', found[0]) + + def test_no_exclude(self): + from beagle.search import Search + search = Search(app=None, app_args=None) + args = DummyArgs(exclude=[]) + found = list(search._flatten_results(self.results, args)) + self.assertEqual(len(found), 3) + self.assertIn('src/main.py', found[0]) + self.assertIn('src/test_utils.py', found[1]) + self.assertIn('docs/readme.py', found[2]) + + def test_partial_exclude(self): + from beagle.search import Search + search = Search(app=None, app_args=None) + args = DummyArgs(exclude=['*test*']) + found = list(search._flatten_results(self.results, args)) + self.assertEqual(len(found), 2) + self.assertIn('src/main.py', found[0]) + self.assertIn('docs/readme.py', found[1]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tox.ini b/tox.ini index 6f293a4..b6053b6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.2.0 -envlist = py39,py310,py311,py312,py313,pep8 +envlist = py39,py310,py311,py312,py313,pep8,unit [testenv] usedevelop = True @@ -30,6 +30,14 @@ deps = -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -b html doc/source doc/build/html +[testenv:unit] +description = Run pytest-based unit tests +deps = + pytest + -rrequirements.txt +commands = + pytest tests + [flake8] # E123, E125 skipped as they are invalid PEP-8. # W504 skipped to give priority to W503