ratchets

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 300607df051bcb3c90a51892b3524792fca02603
parent 672ec99e971a4f43e14e82cafb0feeb3095bb0a0
Author: Andrew D. Laack <andrew@laack.co>
Date:   Sat, 21 Jun 2025 05:19:59 -0500

Added Ratchets tests to Ratchets (#6)

* Updated tests to specify outputs and toml files to decouple the tests from the repo config

* Made tests self-contained so I can use ratchets with ratchets

* Fixed ratchet issues based on current .toml.

* Fixed OS dependent path issue

* Added a few more regex and shell tests

* Revised no TODO regex

* Added current ratchet_values

* Removed reference to top level db.
Diffstat:
M.gitignore | 2++
A.temp_ratchet_blame.db | 0
MREADME.md | 2+-
Rexamples/example_test_ratchet.py -> examples/test_ratchet.py | 0
Mratchet_excluded.txt | 3+--
Dratchet_values.json | 2--
Msrc/ratchets/run_tests.py | 41+++++++++++++++++++++++++++++------------
Msrc/ratchets/validate.py | 2+-
Mtests.toml | 60+++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Cratchet_excluded.txt -> tests/exclusion_files/ratchet_excluded.txt | 0
Mtests/test_files/test_caching.py | 2+-
Mtests/test_files/test_exclusion.py | 32+++++++++++++++++---------------
Mtests/test_files/test_toml_configs.py | 35++++++++++++++++++++++++++++-------
13 files changed, 133 insertions(+), 48 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -196,3 +196,5 @@ notes/ data/ testing.db .ratchet_blame.db +temp/ +temp_* diff --git a/.temp_ratchet_blame.db b/.temp_ratchet_blame.db Binary files differ. diff --git a/README.md b/README.md @@ -118,7 +118,7 @@ Once the update command has been executed, the `ratchet_excluded.txt` file is cr ## Running as part of PyTest -To set up tests, we provide an example file at [examples/example_test_ratchet.py](https://github.com/andrewlaack/ratchets/blob/main/examples/example_test_ratchet.py), which defines tests to be ran with PyTest. In this file there are two uncommented methods that runs one test per rule in both sections (regex and shell). +To set up tests, we provide an example file at [examples/example_test_ratchet.py](https://github.com/andrewlaack/ratchets/blob/main/examples/test_ratchet.py), which defines tests to be ran with PyTest. In this file there are two uncommented methods that runs one test per rule in both sections (regex and shell). The commented methods aggregate these tests together into two total tests (regex and shell). diff --git a/examples/example_test_ratchet.py b/examples/test_ratchet.py diff --git a/ratchet_excluded.txt b/ratchet_excluded.txt @@ -1,2 +1 @@ -*1* -ex_dir/ +file_spec_files diff --git a/ratchet_values.json b/ratchet_values.json @@ -1 +0,0 @@ -{}- \ No newline at end of file diff --git a/src/ratchets/run_tests.py b/src/ratchets/run_tests.py @@ -95,7 +95,7 @@ def get_python_files( def filter_excluded_files( files: List[Path], excluded_path: str, ignore_path: str ) -> List[Path]: - """Returns a new list of file paths that consists of all 'files' paths not excluded in the 'excluded_path' or 'ignore_path'.""" + """Get a list of paths not excluded by the 'excluded_path' or 'ignore_path'.""" patterns = [] if os.path.isfile(excluded_path): with open(excluded_path, "r") as f: @@ -162,9 +162,13 @@ def print_issues(issues: Dict[str, List[Dict[str, Any]]]) -> None: print(f"\n{test_name} — no issues found.") -def load_ratchet_results() -> Dict[str, Any]: +def load_ratchet_results(file_location: Optional[str] = None) -> Dict[str, Any]: """Load and return current ratchet values..""" - path = get_ratchet_path() + + if file_location is None: + path = get_ratchet_path() + else: + path = file_location if not os.path.isfile(path): return {} @@ -323,12 +327,21 @@ def results_to_json( def update_ratchets( - test_path: str, cmd_mode: bool, regex_mode: bool, paths: Optional[List[str]] + test_path: str, + cmd_mode: bool, + regex_mode: bool, + paths: Optional[List[str]], + override_ratchet_path: Optional[str] = None, ) -> None: - """Update the current ratchets based on the outcome of the tests defined in 'test_path'.""" + """Update the current ratchets based on 'test_path'.""" results = evaluate_tests(test_path, cmd_mode, regex_mode, paths) results_json = results_to_json(results) - path = get_ratchet_path() + + if override_ratchet_path is None: + path = get_ratchet_path() + else: + path = override_ratchet_path + with open(path, "w") as file: file.writelines(results_json) @@ -337,7 +350,7 @@ def print_issues_with_blames( results: Tuple[Dict[str, List[Dict[str, Any]]], Dict[str, List[Dict[str, Any]]]], max_count: int, ) -> None: - """For the results in 'results', get blame results for each result and then print the results in a human readable format.""" + """Get blame results for each result and print in human readable format.""" enriched_test_issues, enriched_shell_issues = add_blames(results) def _parse_time(ts: Optional[str]) -> datetime: @@ -362,7 +375,8 @@ def print_issues_with_blames( ) print() print( - f"{section_name} — {test_name} ({len(sorted_matches)} issue{'s' if len(sorted_matches) != 1 else ''}):" + f"{section_name} — {test_name} ({len(sorted_matches)}" + + f"issue{'s' if len(sorted_matches) != 1 else ''}):" ) print() count = 0 @@ -398,7 +412,7 @@ def print_issues_with_blames( def add_blames( results: Tuple[Dict[str, List[Dict[str, Any]]], Dict[str, List[Dict[str, Any]]]], ) -> Tuple[Dict[str, List[Dict[str, Any]]], Dict[str, List[Dict[str, Any]]]]: - """Add blame information: check cache in series, then run git blame in parallel for misses.""" + """Add blame information to input results.""" test_issues, shell_issues = results try: @@ -572,7 +586,8 @@ def cli(): "-b", "--blame", action="store_true", - help="run an additional git-blame for each infraction, ordering results by timestamp", + help="run an additional git-blame for" + + "each infraction, ordering results by timestamp", ) parser.add_argument( @@ -583,14 +598,16 @@ def cli(): "-m", "--max-count", type=int, - help="maximum infractions to display per test (only applies with --blame; default is 10)", + help="maximum infractions to display per test" + + "(only applies with --blame; default is 10)", ) parser.add_argument( "-c", "--compare-counts", action="store_true", - help="show only the differences in infraction counts between the current and last saved tests", + help="show only the differences in infraction " + + "counts between the current and last saved tests", ) parser.add_argument( diff --git a/src/ratchets/validate.py b/src/ratchets/validate.py @@ -24,7 +24,7 @@ def check_valid(regex_tests: Dict[str, Dict[str, Any]]) -> None: def check_invalid(regex_tests: Dict[str, Dict[str, Any]]) -> None: - """Given a dict of regex test and strings, returns if all of the regexps match all of their strings.""" + """Check if all of the regexps match all of their strings.""" for test in regex_tests: regex: str = regex_tests[test]["regex"] for validation in regex_tests[test]["invalid"]: diff --git a/tests.toml b/tests.toml @@ -1,6 +1,3 @@ -# these will be ran in *parallel -# (gthreads) - [ratchet.regex.exceptions] regex = "except:" valid = [ @@ -34,6 +31,20 @@ invalid = ["import pytorch_lightning", "from pytorch_lightning import LightningM description = "The standard PyTorch library should be used in lieu of the PyTorch Lightning package." +[ratchet.regex.no_wildcard_imports] +regex = "from\\s+[^\\s]+\\s+import\\s+\\*" +valid = [ + "from module import name1, name2", + "import module", + "from package.subpackage import ClassA" +] +invalid = [ + "from math import *", + "from mypkg.utils import *" +] +description = "Wildcard imports make it unclear which names are in scope and can cause conflicts. Always import only the explicit names you need." + + [ratchet.regex.tabs] regex = "\\t" valid = [ @@ -48,10 +59,45 @@ invalid = [ ] description = "As per the PEP 8 style guide for Python code, spaces are to be used instead of tabs for indentation. To mitigate this issue, run 'black FILENAME' to reformat the file with black, or manually fix the issue and update your editor to replace tabs with space." -# printed text is assumed to be failures -# each evaluation **must** accept a file path as input -# these can be tested by running echo FILEPATH | COMMAND -# these will be ran in parallel + +[ratchet.regex.trailing_whitespace] +regex = "[ \\t]+$" +valid = [ + "def foo():\n return 42", + "x = 1 # comment" +] +invalid = [ + "def foo(): \n return 42\t" +] +description = "Trailing whitespace is invisible but can cause noise in diffs and editors; trim trailing spaces/tabs." + + +[ratchet.regex.ensure_trailing_newline] +regex = "\\Z(?<!\\n)\\Z" +valid = [ + "# some code\n\n" +] +invalid = [ + "# code without trailing newline" +] +description = "Text files should end with a newline character to satisfy POSIX tools and avoid diff noise." + + +[ratchet.regex.no_todo_comments] +regex = "(?i)#.*\\b(?:TODO|FIXME)\\b" +valid = [ + "# This is a regular comment without keywords", + "print('TODO in string literal is not caught as a comment')", + "x = 42 # calculation complete" +] +invalid = [ + "# TODO: implement this function", + " # FIXME handle edge cases", + "def foo():\n # todo: remove debug code", + "some_var = 'value' # FIXME: adjust value later" +] +description = "Ensure no TODO or FIXME comments remain in code; address or file an issue instead of leaving these markers." + [ratchet.shell.line_too_long] command = "xargs -n1 awk 'length($0) > 88'" diff --git a/ratchet_excluded.txt b/tests/exclusion_files/ratchet_excluded.txt diff --git a/tests/test_files/test_caching.py b/tests/test_files/test_caching.py @@ -3,7 +3,7 @@ from ratchets.abstracted_tests import find_project_root import os from datetime import datetime -CACHING_FILENAME = ".ratchet_blame.db" +CACHING_FILENAME = "tests/test_files/temp_ratchet_blame.db" def test_create_new_db(): diff --git a/tests/test_files/test_exclusion.py b/tests/test_files/test_exclusion.py @@ -1,30 +1,29 @@ from ratchets import run_tests from ratchets import abstracted_tests import os -import toml -from typing import Dict, Any -import json import shutil def test_config(): - test_path = run_tests.get_file_path(None) + test_path = os.path.join( + run_tests.find_project_root(), "tests/toml_files/default.toml" + ) - assert os.path.isfile(test_path), "tests.toml not found" + assert os.path.isfile(test_path), "default.toml not found" try: issues = run_tests.evaluate_tests(test_path, True, True, None) - run_tests.update_ratchets(test_path, True, True, None) + run_tests.update_ratchets( + test_path, + True, + True, + None, + run_tests.find_project_root() + "/tests/test_files/temp_ratchet1.json", + ) except Exception as e: assert False, f"Unable to update ratchets using 'tests.toml': {e}" -# TODO: -# add gitignore checks. -# gitignore is handled the same way as -# the excluded file, but should be checked too - - def test_exclusion(): current_file_directory = os.path.dirname(os.path.abspath(__file__)) @@ -34,7 +33,10 @@ def test_exclusion(): test_py_dir = os.path.abspath( os.path.join(current_file_directory, "..", "python_files") ) - exclusion_path = run_tests.get_excludes_path() + exclusion_path = ( + run_tests.find_project_root() + "/tests/exclusion_files/ratchet_excluded.txt" + ) + root = run_tests.find_project_root() ignore_path = os.path.join(root, ".gitignore") @@ -63,7 +65,7 @@ def test_exclusion(): assert ( filename in expected_results - ), "An additional excluded.txt file was added, but the corresponding expected count was not added to the dict" + ), "An additional excluded.txt file was added, but not reflected" assert expected_results[filename] == len( filtered @@ -71,7 +73,7 @@ def test_exclusion(): assert count == len( expected_results - ), "There is an entry in the expected_results dictionary that does not correspond with a file tested" + ), "There is an extra entry in the expected_results dictionary" if __name__ == "__main__": diff --git a/tests/test_files/test_toml_configs.py b/tests/test_files/test_toml_configs.py @@ -17,13 +17,21 @@ import json def test_config(): - test_path = run_tests.get_file_path(None) + test_path = os.path.join( + run_tests.find_project_root(), "tests/toml_files/default.toml" + ) - assert os.path.isfile(test_path), "tests.toml not found" + assert os.path.isfile(test_path), "default.toml not found" try: issues = run_tests.evaluate_tests(test_path, True, True, None) - run_tests.update_ratchets(test_path, True, True, None) + run_tests.update_ratchets( + test_path, + True, + True, + None, + run_tests.find_project_root() + "/tests/test_files/temp_ratchet1.json", + ) except Exception as e: assert False, f"Unable to update ratchets using 'tests.toml': {e}" @@ -58,8 +66,15 @@ def test_formatting(): # ensure updated values match subsequent runs. def verify_updating(): - test_path = run_tests.get_file_path(None) - run_tests.update_ratchets(test_path, True, True, None) + test_path = run_tests.find_project_root() + "/tests/toml_files/default.toml" + + run_tests.update_ratchets( + test_path, + True, + True, + None, + run_tests.find_project_root() + "/tests/test_files/temp_ratchet2.json", + ) # if one is false then the results are guaranteed # to be either the same or lower. @@ -84,7 +99,7 @@ def test_ratchet_excluded_missing(): except Exception as e: assert False, "Unable to delete ratchet_values.json" - test_path = run_tests.get_file_path(None) + test_path = run_tests.find_project_root() + "/tests/toml_files/default.toml" try: previous = run_tests.load_ratchet_results() @@ -96,7 +111,13 @@ def test_ratchet_excluded_missing(): issues = run_tests.evaluate_tests(test_path, True, True, None) # writes back json file - run_tests.update_ratchets(test_path, True, True, None) + run_tests.update_ratchets( + test_path, + True, + True, + None, + run_tests.find_project_root() + "/tests/test_files/temp_ratchet3.json", + ) return