Created
July 1, 2016 11:01
-
-
Save jontwo/27ecb6da05eb05ecd050ff4aebf2076b to your computer and use it in GitHub Desktop.
C# test coverage
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
# -*- coding: utf-8 -*- | |
""" | |
Name: get_test_coverage.py | |
Purpose: Analyze C# project to check each class has at least one unit test | |
Author: jpm | |
Created: 18/09/2015 | |
Copyright: (c) jpm 2015 | |
Licence: GPL v3. See http://www.gnu.org/licenses/ | |
""" | |
import json | |
import os | |
import re | |
import sys | |
pattern = re.compile( | |
r"\s*(public|private|internal|protected)?\s*(static|virtual|abstract)?" | |
"\s*([a-zA-Z\<\>_1-9]+)\s(?P<method>[a-zA-Z\<\>_1-9]+\([\w,=\s\<\>]*\))" | |
) | |
ui_methods = [ | |
'AfterCheck', 'AfterCollapse', 'AfterExpand', 'AfterSelect', 'Click', | |
'CheckChanged', 'CheckedChanged', 'Closed', 'DropDown', 'FormClosed', | |
'FormClosing', 'ItemDrag', 'ItemSelectionChanged', 'KeyDown', 'KeyUp', | |
'Leave', 'Load', 'MouseDown', 'MouseEnter', 'MouseMove', 'MouseUp', | |
'Paint', 'SelectedIndexChanged', 'SelectionChangeCommitted', 'Selecting', | |
'Shown', 'SizeChanged', 'Tick' | |
] | |
def file_read(filename): | |
""" | |
Read from a file | |
:param filename: Full path of file | |
:return: File contents as string or empty string if there was an error | |
""" | |
try: | |
with open(filename, 'r') as f: | |
return f.read() | |
except (OSError, IOError): | |
return '' | |
def get_methods(filename): | |
""" | |
Read C# file and use regex to find method names | |
:param filename: Full path of file | |
:return: Array of method names in alphabetical order | |
""" | |
found = [] | |
for line in file_read(filename).split('\n'): | |
result = pattern.match(line) | |
if result: | |
found.append(result.group('method')) | |
return sorted(found) | |
def get_coverage_for_class(methods, tests, missing_only=False): | |
""" | |
Compare method names and test names and print the names of methods that | |
do not have at least one test | |
:param methods: List of method names | |
:param tests: List of test names | |
:param missing_only: If true, only print missing names | |
""" | |
if not missing_only: | |
print 'Methods:' | |
print '\n'.join(methods) | |
print 'Tests:' | |
print '\n'.join(tests) | |
missing = [] | |
for method in methods: | |
method_name = method.split('(')[0] | |
has_test = False | |
for test in tests: | |
if test.startswith(method_name): | |
has_test = True | |
continue | |
if not has_test and method_name not in missing and \ | |
method_name.split('_')[-1] not in ui_methods: | |
missing.append(method_name) | |
if missing: | |
print 'Missing:' | |
print '\n'.join(missing) | |
else: | |
print 'OK' | |
def get_coverage_for_config(config): | |
""" | |
Use config file to examine a whole project for missing tests | |
:param config: Project configuration as JSON in the following format: | |
{ "class_path": "<project folder>", "test_path": "<test folder>", | |
"tests": { "<filename>": { "test": "<testfilename>", "missing": | |
[<method names>] }, ... }, "skip": [<filenames>] } | |
"missing" and "skip" refer to methods/files that do not need to be tested | |
""" | |
# get absolute paths for classes and tests | |
class_path = os.path.abspath( | |
os.path.join(os.path.dirname(config_file), config['class_path']) | |
) | |
test_path = os.path.abspath( | |
os.path.join(os.path.dirname(config_file), config['test_path']) | |
) | |
# find project files | |
for proj_file in os.listdir(class_path): | |
if os.path.splitext(proj_file.lower())[1] != '.cs': | |
continue | |
if proj_file.lower().endswith('.designer.cs'): | |
continue | |
if proj_file in config['tests'].keys(): | |
methods = get_methods(os.path.join(class_path, proj_file)) | |
tests = get_methods( | |
os.path.join(test_path, config['tests'][proj_file]['test']) | |
) | |
# remove methods we know are missing | |
methods = filter( | |
lambda x: x not in config['tests'][proj_file]['missing'], | |
map(lambda y: y.split('(')[0],methods) | |
) | |
print 'Getting coverage for {0} -> {1}...'.format( | |
proj_file, config['tests'][proj_file]['test'] | |
) | |
get_coverage_for_class(methods, tests, True) | |
print '' | |
elif proj_file not in config['skip']: | |
print 'Class {0} not found in config'.format(proj_file) | |
def show_usage(): | |
print 'Usage: get_test_coverage.py <class file> <test file>' | |
print 'Checks each method in the class file has at least one test' | |
print ' or get_test_coverage.py <config file>' | |
print 'Specify class and test files and any methods that can be skipped' | |
sys.exit(1) | |
if __name__ == '__main__': | |
if len(sys.argv) == 2 and (sys.argv[1] == '-?' or sys.argv[1] == '-h'): | |
show_usage() | |
config_file = '' | |
methods = [] | |
tests = [] | |
if len(sys.argv) == 1: | |
# no args - look for coverage.json | |
if not os.path.exists('coverage.json'): | |
show_usage() | |
config_file = 'coverage.json' | |
elif len(sys.argv) == 2: | |
# single arg is coverage.json | |
if not os.path.exists(sys.argv[1]): | |
print '{0} not found'.format(sys.argv[1]) | |
show_usage() | |
config_file = sys.argv[1] | |
elif len(sys.argv) == 3: | |
# class/test | |
methods = get_methods(sys.argv[1]) | |
tests = get_methods(sys.argv[2]) | |
else: | |
show_usage() | |
if config_file: | |
try: | |
get_coverage_for_config(json.loads(file_read(config_file))) | |
except KeyError: | |
print 'Warning: missing key in config file' | |
sys.exit(2) | |
else: | |
get_coverage_for_class(methods, tests) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is a very basic test coverage tool that just looks at the method names in your project and checks that each xxx method has a corresponding xxxTest method. Can be run on individual classes or, using a JSON config file, for the whole project at once.