testinggame/__init__.py (201 lines of code) (raw):

# !/usr/bin/python # -*- coding: utf-8 -*- ''' * Copyright (c) 2015 Spotify AB. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. ''' import argparse import os import subprocess def _find_name_from_blame(blame_line): """ Finds the name of the committer of code given a blame line from git Args: blame_line: A string from the git output of the blame for a file. Returns: The username as a string of the user to blame """ blame_info = blame_line[blame_line.find('(')+1:] blame_info = blame_info[:blame_info.find(')')] blame_components = blame_info.split() name_components = blame_components[:len(blame_components)-4] return ' '.join(name_components) def _find_xctest_tests(blame_lines, names, source, xctestsuperclasses): """ Finds the number of XCTest cases per user. Args: blame_lines: An array where each index is a string containing the git blame line. names: The current dictionary containing the usernames as a key and the number of tests as a value. source: A string containing the raw source code for the file. xctestsuperclasses: An array containing alternative superclasses for the xctest framework. Returns: A dictionary built off the names argument containing the usernames as a key and the number of tests as a value. """ xctest_identifiers = ['XCTestCase'] xctest_identifiers.extend(xctestsuperclasses) contains_test_case = False for xctest_identifier in xctest_identifiers: contains_test_case |= source.find(xctest_identifier) != -1 if contains_test_case: break if contains_test_case: for blame_line in blame_lines: if blame_line.replace(' ', '').find('-(void)test') != -1: name = _find_name_from_blame(blame_line) name_count = names.get(name, 0) names[name] = name_count + 1 return names def _find_java_tests(blame_lines, names): """ Finds the number of Java test cases per user. This will find tests both with the @Test annotation and the standard test methods. Args: blame_lines: An array where each index is a string containing the git blame line. names: The current dictionary containing the usernames as a key and the number of tests as a value. Returns: A dictionary built off the names argument containing the usernames as a key and the number of tests as a value. """ next_is_test = False for blame_line in blame_lines: separator = blame_line.find(')') blame_code_nospaces = blame_line[separator+1:] blame_code_nospaces = blame_code_nospaces.replace(' ', '') blame_code_nospaces = blame_code_nospaces.replace('\t', '') if next_is_test or blame_code_nospaces.startswith('publicvoidtest'): name = _find_name_from_blame(blame_line) name_count = names.get(name, 0) names[name] = name_count + 1 next_is_test = False else: next_is_test = blame_code_nospaces.startswith('@Test') return names def _find_cs_tests(blame_lines, names): """ Finds the number of C# test cases per user. This will find nUnit tests with the [Test] attribute Args: blame_lines: An array where each index is a string containing the git blame line. names: The current dictionary containing the usernames as a key and the number of tests as a value. Returns: A dictionary built off the names argument containing the usernames as a key and the number of tests as a value. """ next_is_test = False for blame_line in blame_lines: separator = blame_line.find(')') blame_code_nospaces = blame_line[separator+1:] blame_code_nospaces = blame_code_nospaces.replace(' ', '') blame_code_nospaces = blame_code_nospaces.replace('\t', '') if next_is_test and blame_code_nospaces.find('{') != -1: next_is_test = False elif next_is_test and blame_code_nospaces.startswith('public'): name = _find_name_from_blame(blame_line) name_count = names.get(name, 0) names[name] = name_count + 1 next_is_test = False else: next_is_test = next_is_test or blame_code_nospaces.startswith('[Test]') return names def _find_boost_tests(blame_lines, names): """ Finds the number of Boost test cases per user. Args: blame_lines: An array where each index is a string containing the git blame line. names: The current dictionary containing the usernames as a key and the number of tests as a value. Returns: A dictionary built off the names argument containing the usernames as a key and the number of tests as a value. """ test_cases = ['BOOST_AUTO_TEST_CASE', 'BOOST_FIXTURE_TEST_CASE'] for blame_line in blame_lines: contains_test_case = False for test_case in test_cases: contains_test_case |= blame_line.find(test_case) != -1 if contains_test_case: break if contains_test_case: name = _find_name_from_blame(blame_line) name_count = names.get(name, 0) names[name] = name_count + 1 return names def _find_python_tests(blame_lines, names, source): """ Finds the number of python test cases per user. Args: blame_lines: An array where each index is a string containing the git blame line. names: The current dictionary containing the usernames as a key and the number of tests as a value. Returns: A dictionary built off the names argument containing the usernames as a key and the number of tests as a value. """ for blame_line in blame_lines: separator = blame_line.find(')') blame_code_nospaces = blame_line[separator+1:] blame_code_nospaces = blame_code_nospaces.replace(' ', '') blame_code_nospaces = blame_code_nospaces.replace('\t', '') if blame_code_nospaces.startswith('deftest'): name = _find_name_from_blame(blame_line) name_count = names.get(name, 0) names[name] = name_count + 1 return names def _find_php_tests(blame_lines, names): """ Finds the number of php test cases per user. Does not consider data providers Args: blame_lines: An array where each index is a string containing the git blame line. names: The current dictionary containing the usernames as a key and the number of tests as a value. Returns: A dictionary built off the names argument containing the usernames as a key and the number of tests as a value. """ for blame_line in blame_lines: separator = blame_line.find(')') blame_code_nospaces = blame_line[separator+1:] blame_code_nospaces = blame_code_nospaces.replace(' ', '') blame_code_nospaces = blame_code_nospaces.replace('\t', '') if blame_code_nospaces.startswith('publicfunctiontest'): name = _find_name_from_blame(blame_line) name_count = names.get(name, 0) names[name] = name_count + 1 return names def _find_git_status(directory, xctestsuperclasses): """ Finds the number of tests per user within a given directory. Note that this will only work on the root git subdirectory, submodules will not be counted. Args: directory: The path to the directory to scan. xctestsuperclasses: An array of strings containing names for xctest superclasses. Returns: A dictionary built off the names argument containing the usernames as a key and the number of tests as a value. >>> _find_git_status('tests', 'SPTTestCase') {'Will Sackfield': 6} """ names = {} objc_extensions = ['.m', '.mm'] java_extensions = ['.java', '.kt'] cpp_extensions = ['.cpp', '.mm'] python_extensions = ['.py'] cs_extensions = ['.cs'] php_extensions = ['.php'] valid_extensions = objc_extensions valid_extensions.extend(java_extensions) valid_extensions.extend(cpp_extensions) valid_extensions.extend(python_extensions) valid_extensions.extend(cs_extensions) valid_extensions.extend(php_extensions) for root, dirs, files in os.walk(directory): for name in files: filename, fileextension = os.path.splitext(name) absfile = os.path.join(root, name) if fileextension in valid_extensions: try: with open(absfile) as sourcefile: source = sourcefile.read() p = subprocess.Popen(['git', 'blame', absfile], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() blame_lines = out.splitlines() if fileextension in objc_extensions: names = _find_xctest_tests(blame_lines, names, source, xctestsuperclasses) if fileextension in java_extensions: names = _find_java_tests(blame_lines, names) if fileextension in cpp_extensions: names = _find_boost_tests(blame_lines, names) if fileextension in python_extensions: names = _find_python_tests(blame_lines, names, source) if fileextension in cs_extensions: names = _find_cs_tests(blame_lines, names) if fileextension in php_extensions: names = _find_php_tests(blame_lines, names) except: 'Could not open file: ' + absfile return names def _main(): parser = argparse.ArgumentParser() parser.add_argument('-d', '--directory', help='The directory to search for files in', required=False, default=os.getcwd()) parser.add_argument('-x', '--xctestsuperclasses', help='A comma separated list of XCTest super classes', required=False, default='') parser.add_argument('-v', '--version', help='Prints the version of testing game', required=False, default=False, action='store_true') args = parser.parse_args() if args.version: print 'testing game version 1.0.0' return xctest_superclasses = args.xctestsuperclasses.replace(' ', '').split(',') names = _find_git_status(args.directory, xctest_superclasses) total_tests = 0 for name in names: total_tests += names[name] print "Total Tests: %(t)d" % {'t': total_tests} print "-------------------------------------------" sorted_list = sorted(names.items(), key=lambda x: x[1], reverse=True) for t in sorted_list: percentage = (float(t[1]) / float(total_tests)) * 100.0 t_index = sorted_list.index(t) + 1 print "%(i)d. %(n)s, %(t)d (%(p).2f%%)" % {'i': t_index, 'n': t[0], 't': t[1], 'p': percentage} if __name__ == "__main__": _main()