diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7fd95c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pyc +/*.egg-info +build/ +dist/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d07ecb3..59207ba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1,5 @@ Contributions are welcomed. Open a pull-request or an issue. + +This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code. + +[code-of-conduct]: https://github.com/spotify/code-of-conduct/blob/master/code-of-conduct.md diff --git a/README.md b/README.md index fb773d8..f962ae3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# :star: The Testing Game :star: +# :star: The Testing Game 1.0 :star: Welcome to the Testing Game! A simple script that counts the number of Objective-C, Java, C++ or python unit tests in the current working directory within a git repository, and showcases a ranking based on the percentage each developer has written. @@ -24,12 +24,19 @@ This script was made to “gameify” testing at Spotify and to continue encoura The script uses the current working directory to find files it could possibly read (such as `.m`, `.mm` and `.java` files) and performs a `git blame` on these files in order to match tests written to the developers that wrote them. The owner of the method name of the test is considered the developer that wrote it. +## Dependencies + +* [python 2.7.10](https://www.python.org/downloads/release/python-2710/) (for running the script) +* [git 2.6.1](https://git-scm.com/) (for finding the blame information for a given file) + +The script should run on any operating system containing these two dependencies. + ## Usage 1. Run the Python script from your repository: ```shell - > python testing-game.py + > python testinggame.py ``` 2. Mention that you write most unit units of your project on every meeting (no, don’t do that). diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..706c90e --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + +setup(name='testinggame', + version='1.0.0', + description='A script for counting the number of tests per developer', + url='http://github.com/spotify/testing-game', + author='Will Sackfield', + author_email='sackfield@spotify.com', + license='Apache', + packages=['testinggame'], + zip_safe=False, + entry_points={ + 'console_scripts': [ + 'testinggame = testinggame:_main' + ] + }) \ No newline at end of file diff --git a/testing-game.py b/testinggame/__init__.py similarity index 53% rename from testing-game.py rename to testinggame/__init__.py index 2821504..2d54632 100755 --- a/testing-game.py +++ b/testinggame/__init__.py @@ -1,3 +1,5 @@ +# !/usr/bin/python +# -*- coding: utf-8 -*- ''' * Copyright (c) 2015 Spotify AB. * @@ -18,16 +20,43 @@ * specific language governing permissions and limitations * under the License. ''' - -# !/usr/bin/python -# -*- coding: utf-8 -*- - import argparse import os import subprocess -def find_xctest_tests(blame_lines, names, source, xctestsuperclasses): +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 @@ -38,17 +67,27 @@ def find_xctest_tests(blame_lines, names, source, xctestsuperclasses): if contains_test_case: for blame_line in blame_lines: if blame_line.replace(' ', '').find('-(void)test') != -1: - 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] - name = ' '.join(name_components) + 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, source): +def _find_java_tests(blame_lines, names, source): + """ + 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. + source: A string containing the raw source code for the file. + 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(')') @@ -56,8 +95,7 @@ def find_java_tests(blame_lines, names, source): 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'): - blame_info = blame_line[:separator] - name = blame_info[blame_info.find('<')+1:blame_info.find('@')] + name = _find_name_from_blame(blame_line) name_count = names.get(name, 0) names[name] = name_count + 1 next_is_test = False @@ -66,7 +104,20 @@ def find_java_tests(blame_lines, names, source): return names -def find_boost_tests(blame_lines, names, source): +def _find_boost_tests(blame_lines, names, source): + """ + 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. + source: A string containing the raw source code for the file. + 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 @@ -75,31 +126,55 @@ def find_boost_tests(blame_lines, names, source): if contains_test_case: break if contains_test_case: - 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] - name = ' '.join(name_components) + name = _find_name_from_blame(blame_line) name_count = names.get(name, 0) names[name] = name_count + 1 return names -def find_nose_tests(blame_lines, names, source): +def _find_nose_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. + source: A string containing the raw source code for the file. + 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_'): - blame_info = blame_line[:separator] - name = blame_info[blame_info.find('<')+1:blame_info.find('@')] + 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_git_status(directory, xctestsuperclasses): +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'] @@ -123,23 +198,28 @@ def find_git_status(directory, xctestsuperclasses): out, err = p.communicate() blame_lines = out.splitlines() if fileextension in objc_extensions: - names = find_xctest_tests(blame_lines, - names, - source, - xctestsuperclasses) + names = _find_xctest_tests(blame_lines, + names, + source, + xctestsuperclasses) if fileextension in java_extensions: - names = find_java_tests(blame_lines, names, source) - if fileextension in cpp_extensions: - names = find_boost_tests(blame_lines, + names = _find_java_tests(blame_lines, names, source) + if fileextension in cpp_extensions: + names = _find_boost_tests(blame_lines, + names, + source) if fileextension in python_extensions: - names = find_nose_tests(blame_lines, names, source) + names = _find_nose_tests(blame_lines, + names, + source) except: 'Could not open file: ' + absfile return names -if __name__ == "__main__": + +def _main(): parser = argparse.ArgumentParser() parser.add_argument('-d', '--directory', @@ -151,9 +231,18 @@ def find_git_status(directory, 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) + names = _find_git_status(args.directory, xctest_superclasses) total_tests = 0 for name in names: total_tests += names[name] @@ -167,3 +256,6 @@ def find_git_status(directory, xctestsuperclasses): 'n': t[0], 't': t[1], 'p': percentage} + +if __name__ == "__main__": + _main() diff --git a/testinggame/tests/ExampleTest.java b/testinggame/tests/ExampleTest.java new file mode 100644 index 0000000..48675ad --- /dev/null +++ b/testinggame/tests/ExampleTest.java @@ -0,0 +1,12 @@ +package com.spotify.thing; + +public class ExampleTest { + @Before + public void setUp() throws Exception { + } + + @Test + public void testExample() { + Assert.assertEquals(0, 0); + } +} \ No newline at end of file diff --git a/testinggame/tests/SPTTestExampleTest.m b/testinggame/tests/SPTTestExampleTest.m new file mode 100644 index 0000000..6c8630a --- /dev/null +++ b/testinggame/tests/SPTTestExampleTest.m @@ -0,0 +1,24 @@ +#import + +@interface SPTTestExampleTest : SPTTestCase + +@end + +@implementation SPTTestExampleTest + +- (void)setUp +{ + [super setUp]; +} + +- (void)tearDown +{ + [super tearDown]; +} + +- (void)testExample +{ + XCTAssertEqual(0, 0); +} + +@end diff --git a/testinggame/tests/XCTestExampleTest.mm b/testinggame/tests/XCTestExampleTest.mm new file mode 100644 index 0000000..4cb33c3 --- /dev/null +++ b/testinggame/tests/XCTestExampleTest.mm @@ -0,0 +1,28 @@ +#import + +#include + +@interface XCTestExampleTest : XCTestCase + +@end + +@implementation XCTestExampleTest + +- (void)setUp +{ + [super setUp]; +} + +- (void)tearDown +{ + [super tearDown]; +} + +- (void)testExample +{ + std::string string1("thing"); + std::string string2("thing"); + XCTAssertEqual(string1, string2) +} + +@end diff --git a/testinggame/tests/boost_test.cpp b/testinggame/tests/boost_test.cpp new file mode 100644 index 0000000..92c5840 --- /dev/null +++ b/testinggame/tests/boost_test.cpp @@ -0,0 +1,12 @@ +#include +#include + +BOOST_AUTO_TEST_SUITE(spotify) +BOOST_AUTO_TEST_SUITE(testinggame) + +BOOST_AUTO_TEST_CASE(BoostTestExample) { + BOOST_CHECK_EQUAL(0, 0); +} + +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE_END() diff --git a/testinggame/tests/boost_test.mm b/testinggame/tests/boost_test.mm new file mode 100644 index 0000000..b9bd6b1 --- /dev/null +++ b/testinggame/tests/boost_test.mm @@ -0,0 +1,16 @@ +#import + +#include +#include + +BOOST_AUTO_TEST_SUITE(spotify) +BOOST_AUTO_TEST_SUITE(testinggame) + +BOOST_AUTO_TEST_CASE(BoostTestExample) { + NSString *string1 = @"a"; + NSString *string2 = @"a"; + BOOST_CHECK([string1 isEqualToString:string2]); +} + +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE_END() diff --git a/testinggame/tests/nose_test.py b/testinggame/tests/nose_test.py new file mode 100644 index 0000000..bb8a8c3 --- /dev/null +++ b/testinggame/tests/nose_test.py @@ -0,0 +1,5 @@ +import unittest + +class ExampleTest(unittest.TestCase): + def test(self): + self.assertEqual(0, 0) \ No newline at end of file