-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
155 additions
and
0 deletions.
There are no files selected for viewing
153 changes: 153 additions & 0 deletions
153
coverage-metrics/bin/utils/unit-test-coverage/coverage_guard.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import os | ||
import re | ||
import subprocess | ||
|
||
from argparse import ArgumentParser | ||
from prettytable import PrettyTable | ||
import yaml | ||
|
||
ATTR_PACKAGES = "packages" | ||
CONFIG = "config.yaml" | ||
|
||
|
||
class Colour: | ||
RED = "\033[91m" | ||
GREEN = "\033[92m" | ||
BLUE = "\033[94m" | ||
# Resets the colour to the default one | ||
RESET = "\x1b[0m" | ||
# Dims the current colour | ||
DIM = '\033[2m' | ||
|
||
@staticmethod | ||
# Highlights the text with a specified colour. | ||
def highlight(text, colour): | ||
return f'{colour}{text}{Colour.RESET}' | ||
|
||
|
||
# Validates the test coverage config file. | ||
def validate_coverage_config(config): | ||
if ATTR_PACKAGES not in coverage_cfg: | ||
raise AttributeError('The coverage config file is malformed. The "%s" attribute is missing.' % ATTR_PACKAGES) | ||
|
||
if len(config[ATTR_PACKAGES]) == 0: | ||
return config | ||
|
||
for pkg, pkg_coverage in config[ATTR_PACKAGES].items(): | ||
if not type(pkg_coverage) in (int, float): | ||
raise AttributeError('A non-numeric coverage setting for package: "%s".' % pkg) | ||
|
||
return config | ||
|
||
|
||
# Ensures that every package under evaluation exist in the target repository. | ||
def ensure_packages_exist(packages, repo_path): | ||
for pkg in packages: | ||
pkg_path = os.path.join(repo_path, pkg) | ||
if not os.path.exists(pkg_path): | ||
raise FileNotFoundError('Cannot find a package "%s" under "%s".' % (pkg, repo_path)) | ||
|
||
|
||
def fetch_raw_coverage(packages, path): | ||
if len(packages) == 0: | ||
return "" | ||
|
||
# Normalise the package path to be relative to the project. | ||
packages = ["./%s" % p for p in packages] | ||
try: | ||
raw_coverage = subprocess.check_output(['go', 'test', '-cover', *packages], cwd=os.path.realpath(path)) | ||
return raw_coverage.decode("UTF-8") | ||
except Exception as e: | ||
return str(e.output).replace('\\n', '\n').replace('\\t', '\t') | ||
|
||
|
||
def parse_coverage(raw_coverage): | ||
failed_test_suites = re.findall(r"^FAIL\s*" + re.escape(args.module) + r"/([\w/]*)", | ||
raw_coverage, | ||
flags=re.MULTILINE) | ||
if len(failed_test_suites) > 0: | ||
raise AssertionError("Unit tests failed for packages: %s" % ", ".join(failed_test_suites)) | ||
|
||
res = {} | ||
# Extracts all coverage percentages from the unit test report. | ||
# https://regex101.com/r/xPIx8n/1 | ||
coverages = re.findall(r"^ok\s*" + re.escape(args.module) + r"/([\w/]*)\s*(?:\(cached\))?\s*coverage: (\d*\.\d*)%", | ||
raw_coverage, | ||
flags=re.MULTILINE) | ||
for package, package_coverage in coverages: | ||
res[package] = float(package_coverage) | ||
|
||
return res | ||
|
||
|
||
def print_report(cfg, coverage): | ||
table = PrettyTable(("Package", "Desired coverage", "Actual coverage")) | ||
is_undertested = False | ||
for package, desired_coverage in cfg[ATTR_PACKAGES].items(): | ||
if package not in coverage: | ||
raise AttributeError('A package "%s" is not in the list of tested packages.' % package) | ||
actual_coverage = coverage[package] | ||
is_covered = actual_coverage >= desired_coverage | ||
if not is_covered: | ||
is_undertested = True | ||
colour = Colour.GREEN if is_covered else Colour.RED | ||
table.add_row(( | ||
package, | ||
Colour.highlight(desired_coverage, colour), | ||
Colour.highlight(actual_coverage, colour), | ||
)) | ||
|
||
table.align["Package"] = "l" | ||
table.align["Desired coverage"] = "r" | ||
table.align["Actual coverage"] = "r" | ||
print(table) | ||
|
||
return is_undertested | ||
|
||
|
||
# Validates and normalises the CLI arguments. | ||
def normalise(args): | ||
if not args.repo_path: | ||
raise "the --repo parameter must be point to the Go project source code" | ||
|
||
if not args.module: | ||
raise "the --module parameter contain a fully qualified module name" | ||
|
||
if not args.config: | ||
args.config = CONFIG | ||
|
||
return args | ||
|
||
|
||
if "__main__" == __name__: | ||
parser = ArgumentParser() | ||
parser.add_argument("-r", "--repo", dest="repo_path", help="A path to the Go project source code") | ||
parser.add_argument("-m", "--module", dest="module", help="A Go module name") | ||
parser.add_argument("-c", "--config", dest="config", help="A coverage file config") | ||
|
||
args = parser.parse_args() | ||
normalise(args) | ||
|
||
try: | ||
with open(os.path.join(args.repo_path, args.config), 'r') as config_file: | ||
coverage_cfg = yaml.safe_load(config_file) | ||
|
||
validate_coverage_config(coverage_cfg) | ||
|
||
# Validate the coverage prerequisites. | ||
packages_with_coverage = list(coverage_cfg[ATTR_PACKAGES].keys()) | ||
ensure_packages_exist(packages_with_coverage, args.repo_path) | ||
|
||
# Calculate the coverage. | ||
raw_coverage = fetch_raw_coverage(packages_with_coverage, args.repo_path) | ||
base_coverage = parse_coverage(raw_coverage) | ||
|
||
is_undertested = print_report(coverage_cfg, base_coverage) | ||
if is_undertested: | ||
exit(os.EX_DATAERR) | ||
|
||
except (AttributeError, AssertionError, FileNotFoundError) as e: | ||
print(e) | ||
exit(os.EX_IOERR) |
2 changes: 2 additions & 0 deletions
2
coverage-metrics/bin/utils/unit-test-coverage/requirements.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
PyYAML==6.0.1 | ||
prettytable==3.7.0 |