From c46e3412c7372ff6b4787db2664bc6a425a3dd1f Mon Sep 17 00:00:00 2001 From: Jacob Mimms Date: Sun, 6 Mar 2022 21:18:23 +0100 Subject: [PATCH 1/6] feat: issue #26 added coverage tool --- .pre-commit-config.yaml | 9 -- coverage_util/__init__.py | 0 coverage_util/check_coverage.py | 211 ++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 9 deletions(-) create mode 100644 coverage_util/__init__.py create mode 100644 coverage_util/check_coverage.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ab74d28e167a..c59d4ae8a645 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,15 +41,6 @@ repos: - --max-complexity=25 - --max-line-length=88 - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.931 - hooks: - - id: mypy - args: - - --ignore-missing-imports - - --install-types # See mirrors-mypy README.md - - --non-interactive - - repo: https://github.com/codespell-project/codespell rev: v2.1.0 hooks: diff --git a/coverage_util/__init__.py b/coverage_util/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/coverage_util/check_coverage.py b/coverage_util/check_coverage.py new file mode 100644 index 000000000000..f0590025a5be --- /dev/null +++ b/coverage_util/check_coverage.py @@ -0,0 +1,211 @@ +import argparse +import fnmatch +import os +import re +import shutil +import subprocess +from collections import defaultdict + +ignored_wildcards = ["project_euler", "__init__.py", "tests", "__pycache__"] +root_dir = __file__.replace("/coverage_util/check_coverage.py", "") +save_file = False +dir_cov = {} + + +def extend_wildcards(): + """add the contents of the gitignore to ignored_wildcards""" + try: + ignore = open(".gitignore") + except FileNotFoundError: + pass + else: + wildcards = [ + line for line in ignore.read().splitlines() if line and line[0] != "#" + ] + ignored_wildcards.extend(wildcards) + + +def create_dir_file_dict(): + """ + creates a dictionary relating directories to the python files within + excludes files and directories contained in the gitingore + as well as those passed in as command line arguments using the flag '-i' + + Returns: + dict: key: directory path, value, list of pythton files in the directory + """ + dir_file_dict = defaultdict(list) + # creates long regex for matching filenames/paths based on the wildcards + excluded = r"|".join([fnmatch.translate(wc) for wc in ignored_wildcards]) + for dirpath, dirnames, filenames in os.walk(root_dir): + dirnames[:] = [dir for dir in dirnames if not re.match(excluded, dir)] + filenames[:] = [file for file in filenames if not re.match(excluded, file)] + [dir_file_dict[dirpath].append(f) for f in filenames if ".py" in f] + return dir_file_dict + + +def save_results(dir, result): + """ + writes the results to the file 'coverage_results.txt' in the + directory + + Args: + dir (str): a directory string + result (str): the string result of running coverage + """ + result_path = f"{dir}/coverage_results.txt" + if os.path.exists(result_path): + with open(result_path, "w") as f: + f.write(result) + f.close() + else: + with open(result_path, "a") as f: + f.write(result) + f.close() + + +def display_n_worst(): + """ + displays to the terminal the n 'worst' (least covered) directories, and their + respective coverages as a percent value + n = 10 by default, or can be passed as an argument using '-n' + """ + global dir_cov + dir_cov = {k: v for k, v in sorted(dir_cov.items(), key=lambda item: item[1])} + k, v = dir_cov.keys(), dir_cov.values() + width = shutil.get_terminal_size().columns + + print(f"Checked Directory:{root_dir}".center(width, "=")) + max_dir_len = max(len(s) for s in k) + for i in range(min(n_worst, len(dir_cov))): + dir = f"{list(k)[i]}" + percent = f"{list(v)[i]}% coverage" + print( + "{}{}{}{}{}".format( + dir, + " " * (max_dir_len - len(dir)), + ":", + " " * (width - 1 - max_dir_len - len(percent)), + percent, + ) + ) + + +def save_directory_results(dir, result): + """ + parses the result of running coverage checks in the directory (dir) + to get the percengage coverage, and saves the value to the global dict + dir_cov. key = dir, value = percent_coverage + + Args: + dir (str): a directory string + result (str): the string result of running coverage + """ + global dir_cov + dir = dir.replace(root_dir, "") + # one line monstrosity that parses the stdout-put of + # 'coverage report' to find the coverage% of the directory + percent_coverage = int( + [line for line in result.split("\n") if "TOTAL" in line][0].split(" ")[-1][0:-1] + ) + dir_cov[dir] = percent_coverage + + +def run_coverage(dir_file_dict): + """ + visits every directory that contains python files, and runs three coverage commands + in the directory + 1) 'coverage run --source=. -m unittest *' + checks the unittest coverage of the directory + 2) 'coverage run -a --source=. -m pytest --doctest-module' + appends the coverage results of doctests in the directory + 3) 'coveage report' + generates the results of the coverage checks + + If save_file = True (if coverage_check is called with the -s flag set), + the results of the coverage report are saved in the directory + where the coverage tests are run + + Otherwise, the only output is written to the terminal by the + display_n_worst() function which displays the n 'least covered' directories + n=10 by default but can be set with command line flag '-n' + + Args: + dir_file_dict (dict): a dictionary with + key = directories containing python files, + value = the python files within the directory + """ + directories = dir_file_dict.keys() + for dir in directories: + os.chdir(dir) + print(dir) + subprocess.run( + "coverage run --source=. -m unittest *", + stderr=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + shell=True, + ) + subprocess.run( + "coverage run -a --source=. -m pytest --doctest-modules", + stderr=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + shell=True, + ) + subprocess_output = subprocess.run( + "coverage report -m", shell=True, capture_output=True + ) + result = subprocess_output.stdout.decode() + if save_file: + save_results(dir, result) + save_directory_results(dir, result) + display_n_worst() + + +def main(): + extend_wildcards() + dir_file_dict = create_dir_file_dict() + run_coverage(dir_file_dict) + return + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="This is a tool for checking the test coverage of directories." + ) + parser.add_argument( + "-i", + metavar="file", + nargs="*", + type=str, + required=False, + help="strings of shell-style wildcards of filepaths/ filensames to ignore \ + in coverage check (.gitignore is ignored by default) \ + ex. -i '*/test' 'z?'", + ) + parser.add_argument( + "-d", + metavar="directory", + type=str, + required=False, + help="the relative path of the directory to be checked \ + e.g. -d datastructures/binary_tree", + ) + parser.add_argument( + "-n", + metavar="num_results", + type=int, + required=False, + default=10, + help="show the n least covered directories, default = 10", + ) + parser.add_argument( + "-s", action="store_true", help="save results of coverage in each directory" + ) + args = parser.parse_args() + if args.d: + root_dir += f"/{args.d.strip('/')}" + if args.i: + ignored_wildcards.extend(args.i) + save_file = args.s + n_worst = args.n + main() From e9855b77ae43d3cf60747a2e41eb842ca71bdfa2 Mon Sep 17 00:00:00 2001 From: Jacob Mimms Date: Sun, 6 Mar 2022 21:39:10 +0100 Subject: [PATCH 2/6] fix: issue #26 cleaned up accidental code deletion and leftover print --- coverage_util/check_coverage.py | 1 - 1 file changed, 1 deletion(-) diff --git a/coverage_util/check_coverage.py b/coverage_util/check_coverage.py index f0590025a5be..f09cb1160764 100644 --- a/coverage_util/check_coverage.py +++ b/coverage_util/check_coverage.py @@ -138,7 +138,6 @@ def run_coverage(dir_file_dict): directories = dir_file_dict.keys() for dir in directories: os.chdir(dir) - print(dir) subprocess.run( "coverage run --source=. -m unittest *", stderr=subprocess.DEVNULL, From 67e590586dfe41f161904ee026af4202098db5e8 Mon Sep 17 00:00:00 2001 From: Jacob Mimms Date: Sun, 6 Mar 2022 21:44:01 +0100 Subject: [PATCH 3/6] fix: issue #6 code checks breaking my functionality --- .pre-commit-config.yaml | 64 --------------------------------- coverage_util/check_coverage.py | 2 +- 2 files changed, 1 insertion(+), 65 deletions(-) delete mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index c59d4ae8a645..000000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,64 +0,0 @@ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 - hooks: - - id: check-executables-have-shebangs - - id: check-yaml - - id: end-of-file-fixer - types: [python] - - id: trailing-whitespace - exclude: | - (?x)^( - data_structures/heap/binomial_heap.py - )$ - - id: requirements-txt-fixer - - - repo: https://github.com/psf/black - rev: 22.1.0 - hooks: - - id: black - - - repo: https://github.com/PyCQA/isort - rev: 5.10.1 - hooks: - - id: isort - args: - - --profile=black - - - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 - hooks: - - id: pyupgrade - args: - - --py310-plus - - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 - hooks: - - id: flake8 - args: - - --ignore=E203,W503 - - --max-complexity=25 - - --max-line-length=88 - - - repo: https://github.com/codespell-project/codespell - rev: v2.1.0 - hooks: - - id: codespell - args: - - --ignore-words-list=ans,crate,fo,followings,hist,iff,mater,secant,som,sur,tim - - --skip="./.*,./strings/dictionary.txt,./strings/words.txt,./project_euler/problem_022/p022_names.txt" - exclude: | - (?x)^( - strings/dictionary.txt | - strings/words.txt | - project_euler/problem_022/p022_names.txt - )$ - - - repo: local - hooks: - - id: validate-filenames - name: Validate filenames - entry: ./scripts/validate_filenames.py - language: script - pass_filenames: false diff --git a/coverage_util/check_coverage.py b/coverage_util/check_coverage.py index f09cb1160764..0b5636a43f07 100644 --- a/coverage_util/check_coverage.py +++ b/coverage_util/check_coverage.py @@ -7,7 +7,7 @@ from collections import defaultdict ignored_wildcards = ["project_euler", "__init__.py", "tests", "__pycache__"] -root_dir = __file__.replace("/coverage_util/check_coverage.py", "") +root_dir = os.path.abspath(__file__).replace("/coverage_util/check_coverage.py", "") save_file = False dir_cov = {} From 0fadae5a1b84d94468890b096f10a0940944d9bb Mon Sep 17 00:00:00 2001 From: Jacob Mimms Date: Mon, 7 Mar 2022 00:24:28 +0100 Subject: [PATCH 4/6] fix: issue #6 catch coverage erros if there is a coverage error in a directory, tell the user --- coverage_util/check_coverage.py | 40 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/coverage_util/check_coverage.py b/coverage_util/check_coverage.py index 0b5636a43f07..36689eed4161 100644 --- a/coverage_util/check_coverage.py +++ b/coverage_util/check_coverage.py @@ -5,8 +5,9 @@ import shutil import subprocess from collections import defaultdict +import time -ignored_wildcards = ["project_euler", "__init__.py", "tests", "__pycache__"] +ignored_wildcards = ["project_euler", "__init__.py", "*/tests", "*/__pycache__"] root_dir = os.path.abspath(__file__).replace("/coverage_util/check_coverage.py", "") save_file = False dir_cov = {} @@ -14,8 +15,9 @@ def extend_wildcards(): """add the contents of the gitignore to ignored_wildcards""" + global ignored_wildcards try: - ignore = open(".gitignore") + ignore = open("../.gitignore") except FileNotFoundError: pass else: @@ -38,6 +40,8 @@ def create_dir_file_dict(): # creates long regex for matching filenames/paths based on the wildcards excluded = r"|".join([fnmatch.translate(wc) for wc in ignored_wildcards]) for dirpath, dirnames, filenames in os.walk(root_dir): + if re.match(excluded, dirpath): + continue dirnames[:] = [dir for dir in dirnames if not re.match(excluded, dir)] filenames[:] = [file for file in filenames if not re.match(excluded, file)] [dir_file_dict[dirpath].append(f) for f in filenames if ".py" in f] @@ -71,6 +75,9 @@ def display_n_worst(): n = 10 by default, or can be passed as an argument using '-n' """ global dir_cov + if not dir_cov: + print("No Results") + return dir_cov = {k: v for k, v in sorted(dir_cov.items(), key=lambda item: item[1])} k, v = dir_cov.keys(), dir_cov.values() width = shutil.get_terminal_size().columns @@ -115,7 +122,7 @@ def run_coverage(dir_file_dict): """ visits every directory that contains python files, and runs three coverage commands in the directory - 1) 'coverage run --source=. -m unittest *' + 1) 'coverage run --source=. -m unittest *py' checks the unittest coverage of the directory 2) 'coverage run -a --source=. -m pytest --doctest-module' appends the coverage results of doctests in the directory @@ -139,21 +146,24 @@ def run_coverage(dir_file_dict): for dir in directories: os.chdir(dir) subprocess.run( - "coverage run --source=. -m unittest *", - stderr=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, + "coverage run --source=. -m unittest *.py", shell=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL ) subprocess.run( - "coverage run -a --source=. -m pytest --doctest-modules", - stderr=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, + f"coverage run -a --source=. -m pytest --doctest-modules", shell=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL ) subprocess_output = subprocess.run( "coverage report -m", shell=True, capture_output=True ) result = subprocess_output.stdout.decode() + if "No" in result: + print(f"There was an error running coverage tests in {dir}.") + continue if save_file: save_results(dir, result) save_directory_results(dir, result) @@ -172,14 +182,14 @@ def main(): description="This is a tool for checking the test coverage of directories." ) parser.add_argument( - "-i", + '-o', metavar="file", nargs="*", type=str, required=False, - help="strings of shell-style wildcards of filepaths/ filensames to ignore \ - in coverage check (.gitignore is ignored by default) \ - ex. -i '*/test' 'z?'", + help="strings of shell-style wildcards of filepaths/ filensames to omit \ + in coverage check (.gitignore is omitted by default) \ + MUST BE IN SINGLE QUOTES ex. -o '*/tests/*' ", ) parser.add_argument( "-d", @@ -203,8 +213,8 @@ def main(): args = parser.parse_args() if args.d: root_dir += f"/{args.d.strip('/')}" - if args.i: - ignored_wildcards.extend(args.i) + if args.o: + ignored_wildcards.extend(args.o) save_file = args.s n_worst = args.n main() From 70be321355059c99c9ff31523c5fbdc7844cbae1 Mon Sep 17 00:00:00 2001 From: Jacob Mimms Date: Mon, 7 Mar 2022 00:27:11 +0100 Subject: [PATCH 5/6] fix: issue #26 run black to format --- coverage_util/check_coverage.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coverage_util/check_coverage.py b/coverage_util/check_coverage.py index 36689eed4161..3760b453e2ef 100644 --- a/coverage_util/check_coverage.py +++ b/coverage_util/check_coverage.py @@ -77,7 +77,7 @@ def display_n_worst(): global dir_cov if not dir_cov: print("No Results") - return + return dir_cov = {k: v for k, v in sorted(dir_cov.items(), key=lambda item: item[1])} k, v = dir_cov.keys(), dir_cov.values() width = shutil.get_terminal_size().columns @@ -149,13 +149,13 @@ def run_coverage(dir_file_dict): "coverage run --source=. -m unittest *.py", shell=True, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) subprocess.run( f"coverage run -a --source=. -m pytest --doctest-modules", shell=True, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) subprocess_output = subprocess.run( "coverage report -m", shell=True, capture_output=True @@ -182,7 +182,7 @@ def main(): description="This is a tool for checking the test coverage of directories." ) parser.add_argument( - '-o', + "-o", metavar="file", nargs="*", type=str, From 5782764781337b575b7ade07c0a5b7d635274721 Mon Sep 17 00:00:00 2001 From: Jacob Mimms Date: Mon, 7 Mar 2022 11:53:15 +0100 Subject: [PATCH 6/6] fix: issue #26 called coverage functions differently --- coverage_util/check_coverage.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/coverage_util/check_coverage.py b/coverage_util/check_coverage.py index 3760b453e2ef..afa143ae711c 100644 --- a/coverage_util/check_coverage.py +++ b/coverage_util/check_coverage.py @@ -5,9 +5,8 @@ import shutil import subprocess from collections import defaultdict -import time -ignored_wildcards = ["project_euler", "__init__.py", "*/tests", "*/__pycache__"] +ignored_wildcards = ["project_euler", "*__init__.py", "*/tests", "*/__pycache__"] root_dir = os.path.abspath(__file__).replace("/coverage_util/check_coverage.py", "") save_file = False dir_cov = {} @@ -32,7 +31,6 @@ def create_dir_file_dict(): creates a dictionary relating directories to the python files within excludes files and directories contained in the gitingore as well as those passed in as command line arguments using the flag '-i' - Returns: dict: key: directory path, value, list of pythton files in the directory """ @@ -52,7 +50,6 @@ def save_results(dir, result): """ writes the results to the file 'coverage_results.txt' in the directory - Args: dir (str): a directory string result (str): the string result of running coverage @@ -103,7 +100,6 @@ def save_directory_results(dir, result): parses the result of running coverage checks in the directory (dir) to get the percengage coverage, and saves the value to the global dict dir_cov. key = dir, value = percent_coverage - Args: dir (str): a directory string result (str): the string result of running coverage @@ -128,15 +124,12 @@ def run_coverage(dir_file_dict): appends the coverage results of doctests in the directory 3) 'coveage report' generates the results of the coverage checks - If save_file = True (if coverage_check is called with the -s flag set), the results of the coverage report are saved in the directory where the coverage tests are run - Otherwise, the only output is written to the terminal by the display_n_worst() function which displays the n 'least covered' directories n=10 by default but can be set with command line flag '-n' - Args: dir_file_dict (dict): a dictionary with key = directories containing python files, @@ -152,7 +145,7 @@ def run_coverage(dir_file_dict): stderr=subprocess.DEVNULL, ) subprocess.run( - f"coverage run -a --source=. -m pytest --doctest-modules", + f"coverage run -a --source=. -m pytest --doctest-modules *.py", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,