From 5a7f0553c9567c52219637da3f5be2ff1496cb1a Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Wed, 17 Jul 2024 08:47:39 -0700 Subject: [PATCH] Remove code coverage reporting, due to difficulties supporting Python 3.6 and 3.11 --- .github/PULL_REQUEST_TEMPLATE.md | 1 - .github/workflows/c_extensions.yml | 12 +- docs/stack.rst | 1 - requirements/test.txt | 2 - test/coverage_blame.py | 187 ----------------------------- tox.ini | 8 +- 6 files changed, 10 insertions(+), 201 deletions(-) delete mode 100755 test/coverage_blame.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 42af345e33..549853605b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -51,7 +51,6 @@ ## Reviewer checklist -- Automated test coverage is satisfactory - PR is fully functional - PR has been tested for [accessibility regressions](http://kolibri-dev.readthedocs.io/en/develop/manual_testing.html#accessibility-a11y-testing) - External dependency files were updated if necessary (`yarn` and `pip`) diff --git a/.github/workflows/c_extensions.yml b/.github/workflows/c_extensions.yml index 13eff01917..1534f3a690 100644 --- a/.github/workflows/c_extensions.yml +++ b/.github/workflows/c_extensions.yml @@ -57,10 +57,10 @@ jobs: make staticdeps-cext pip install . # Start and stop kolibri - coverage run -p kolibri start --port=8081 - coverage run -p kolibri stop + kolibri start --port=8081 + kolibri stop # Run just tests in test/ - py.test --cov=kolibri --cov-report= --cov-append --color=no test/ + py.test --color=no test/ no_c_ext: name: No C Extensions needs: pre_job @@ -94,7 +94,7 @@ jobs: make staticdeps pip install . # Start and stop kolibri - coverage run -p kolibri start --port=8081 - coverage run -p kolibri stop + kolibri start --port=8081 + kolibri stop # Run just tests in test/ - py.test --cov=kolibri --cov-report= --cov-append --color=no test/ + py.test --color=no test/ diff --git a/docs/stack.rst b/docs/stack.rst index 4f69109c40..3cbfbf7ce6 100644 --- a/docs/stack.rst +++ b/docs/stack.rst @@ -71,5 +71,4 @@ We use a number of mechanisms to help encourage code quality and consistency. Mo - `pytest `__ runs our Python unit tests. We also leverage the `Django test framework `__. - In addition to building client assets, `webpack `__ runs linters on client-side code: `ESLint `__ for ES6 JavaScript, `Stylelint `__ for SCSS, and `HTMLHint `__ for HTML and Vue.js components. - Client-side code is tested using `Jest `__ -- `codecov `__ reports on the test coverage - We have `Sentry `__ clients integrated (off by default) for automated error reporting diff --git a/requirements/test.txt b/requirements/test.txt index 2f9cee0cdf..872fb52727 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -2,10 +2,8 @@ beautifulsoup4==4.8.2 factory-boy==2.7.0 fake-factory==0.5.7 -coverage==6.2 mock==2.0.0 mixer==6.0.1 -pytest-cov==3.0.0 pytest==6.2.5 pytest-django==4.5.2 pytest-env==0.6.2 diff --git a/test/coverage_blame.py b/test/coverage_blame.py deleted file mode 100755 index a945fdf2b9..0000000000 --- a/test/coverage_blame.py +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Derived from http://scottlobdell.me/2015/04/gamifying-test-coverage-project/ -# Before running this script, first run tests with coverage using: -# tox -e py3.9 -import logging -from collections import Counter -from subprocess import PIPE -from subprocess import Popen - -logger = logging.getLogger(__name__) - -LINE_COUNT_THRESH = 50 - - -class ExcludeLineParser(object): - @classmethod - def get_missing_lines_for_filename(cls, filename): - command = "coverage report -m %s" % filename - process = Popen(command.split(), stdout=PIPE) - output, _ = process.communicate() - return cls._get_excluded_lines(output) - - @classmethod - def _get_excluded_lines(cls, coverage_output): - excluded_line_numbers = [] - ignore_line_count = 2 - ignore_column_count = 6 - lines = [ - line for line in coverage_output.split("\n")[ignore_line_count:] if line - ] - for line in lines: - exclude_line_strings = line.split()[ignore_column_count:] - for exclude_line_string in exclude_line_strings: - exclude_line_string = exclude_line_string.replace(",", "").replace( - " ", "" - ) - exclude_lines = cls._convert_exclude_line_string_to_ints( - exclude_line_string - ) - excluded_line_numbers.extend(exclude_lines) - - return excluded_line_numbers - - @classmethod - def _convert_exclude_line_string_to_ints(cls, exclude_line_string): - if "->" in exclude_line_string: - return [int(exclude_line_string.split("->")[0])] - if "-" in exclude_line_string: - line_start, line_end = exclude_line_string.split("-") - if line_end == "exit": - line_end = line_start - return range(int(line_start), int(line_end) + 1) - else: - try: - line_number = int(exclude_line_string) - except ValueError: - logger.error("Error for values ({})".format(exclude_line_string)) - return [] - return [line_number] - - -def _get_output_from_pipe_command(command_with_pipes): - piped_commands = [command.strip() for command in command_with_pipes.split("|")] - previous_process = None - for command in piped_commands: - process = Popen( - command.split(), - stdin=previous_process and previous_process.stdout, - stdout=PIPE, - ) - previous_process = process - output, err = previous_process.communicate() - return output - - -def get_python_files(): - full_command = ( - "find kolibri | grep py | grep -v pyc | grep -v test | grep -v virtualenv" - ) - output = _get_output_from_pipe_command(full_command) - filenames = [filename for filename in output.split("\n") if filename] - return filenames - - -def git_blame_on_files(file_list): - total_counter = Counter() - miss_counter = Counter() - for index, filename in enumerate(file_list): - full_command = ( - "git blame --line-porcelain %s | grep author | grep -v author-" % filename - ) - output = _get_output_from_pipe_command(full_command) - - git_scorer = GitScorer(output) - counter = git_scorer.get_author_counts() - - line_to_author = git_scorer.get_line_to_author() - non_covered_lines = ExcludeLineParser.get_missing_lines_for_filename(filename) - miss_counter += attribute_missing_coverage_to_author( - line_to_author, non_covered_lines - ) - - total_counter += counter - return total_counter, miss_counter - - -def apply_threshold_to_counter(counter): - for key in counter.keys(): - if counter[key] < LINE_COUNT_THRESH: - del counter[key] - - -def attribute_missing_coverage_to_author(line_to_author, non_covered_lines): - author_to_miss_count = Counter() - for line_number in non_covered_lines: - try: - author = line_to_author[line_number] - except KeyError: - return Counter() - author_to_miss_count[author] += 1 - return author_to_miss_count - - -class GitScorer(object): - def __init__(self, gblame_output): - self.counts_this_file = Counter() - self.line_to_author = {} - self._parse_git_blame_output(gblame_output) - - def _get_author_from_line(self, line): - author = line.replace("author ", "") - if author.startswith(" ") or author.startswith("\t"): - return None - return author - - def _parse_git_blame_output(self, git_blame_output): - lines = git_blame_output.split("\n") - for index, line in enumerate(lines): - if not line: - continue - line_number = index + 1 - author = self._get_author_from_line(line) - if author: - self.counts_this_file[author] += 1 - self.line_to_author[line_number] = author - - def get_author_counts(self): - return self.counts_this_file - - def get_line_to_author(self): - return self.line_to_author - - -def get_test_coverage_percent_per_author(line_counter, miss_counter): - author_to_test_coverage = {} - for author in line_counter.keys(): - line_count = line_counter[author] - miss_count = miss_counter.get(author, 0) - miss_percent = float(miss_count) / line_count - test_coverage_percent = 1.0 - miss_percent - author_to_test_coverage[author] = test_coverage_percent - return author_to_test_coverage - - -if __name__ == "__main__": - python_files = get_python_files() - line_counter, miss_counter = git_blame_on_files(python_files) - apply_threshold_to_counter(line_counter) - author_to_test_coverage = get_test_coverage_percent_per_author( - line_counter, miss_counter - ) - rank = 1 - for author, cov in sorted( - author_to_test_coverage.items(), key=lambda t: t[1], reverse=True - ): - logger.info( - "#%s. %s: %.2d%% coverage (%d out of %d lines)" - % ( - rank, - author, - cov * 100, - line_counter[author] - miss_counter[author], - line_counter[author], - ) - ) - rank += 1 diff --git a/tox.ini b/tox.ini index 585bde51f5..0d2e5ef185 100644 --- a/tox.ini +++ b/tox.ini @@ -27,8 +27,8 @@ deps = commands = sh -c 'kolibri manage makemigrations --check' # Run the actual tests - python -O -m pytest {posargs:kolibri --cov=kolibri --cov-report= --cov-append --color=no} - python -O -m pytest --cov=kolibri --cov-report= --cov-append --color=no -p no:django test + python -O -m pytest {posargs:kolibri --color=no} + python -O -m pytest --color=no -p no:django test # Fail if the log is longer than 200 lines (something erroring or very noisy got added) sh -c "if [ `cat {env:KOLIBRI_HOME}/logs/kolibri.txt | wc -l` -gt 200 ] ; then echo 'Log too long' && echo '' && tail -n 20 {env:KOLIBRI_HOME}/logs/kolibri.txt && exit 1 ; fi" @@ -54,5 +54,5 @@ deps = -r{toxinidir}/requirements/cext.txt -r{toxinidir}/requirements/postgres.txt commands = - python -O -m pytest {posargs:kolibri --cov=kolibri --cov-report= --cov-append --color=no} - python -O -m pytest --cov=kolibri --cov-report= --cov-append --color=no -p no:django test + python -O -m pytest {posargs:kolibri --color=no} + python -O -m pytest --color=no -p no:django test