From fa1f80f2cb99cf44c7584eed85a04fb8dee56a38 Mon Sep 17 00:00:00 2001 From: Will Sackfield Date: Thu, 31 Dec 2015 07:55:48 -0500 Subject: [PATCH 1/9] Move the encoding and shebang to the first two lines --- testing-game.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/testing-game.py b/testing-game.py index 2821504..24849f1 100755 --- a/testing-game.py +++ b/testing-game.py @@ -1,3 +1,5 @@ +# !/usr/bin/python +# -*- coding: utf-8 -*- ''' * Copyright (c) 2015 Spotify AB. * @@ -18,10 +20,6 @@ * specific language governing permissions and limitations * under the License. ''' - -# !/usr/bin/python -# -*- coding: utf-8 -*- - import argparse import os import subprocess From e5aa203f0a5d29f5a82a1aa04e83a36e43ac9c79 Mon Sep 17 00:00:00 2001 From: Will Sackfield Date: Thu, 31 Dec 2015 08:12:29 -0500 Subject: [PATCH 2/9] Add docstrings to the functions --- testing-game.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/testing-game.py b/testing-game.py index 24849f1..2f4801d 100755 --- a/testing-game.py +++ b/testing-game.py @@ -26,6 +26,21 @@ 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 @@ -47,6 +62,20 @@ def find_xctest_tests(blame_lines, names, source, xctestsuperclasses): 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(')') @@ -65,6 +94,19 @@ def find_java_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 @@ -84,6 +126,19 @@ def find_boost_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:] @@ -98,6 +153,19 @@ def find_nose_tests(blame_lines, names, source): 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. + """ names = {} objc_extensions = ['.m', '.mm'] java_extensions = ['.java'] From d7fcfaf2db324eedf432aa8642cb85fa8dd13aa7 Mon Sep 17 00:00:00 2001 From: Will Sackfield Date: Thu, 31 Dec 2015 08:15:57 -0500 Subject: [PATCH 3/9] Make functions private --- testing-game.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/testing-game.py b/testing-game.py index 2f4801d..1a18af2 100755 --- a/testing-game.py +++ b/testing-game.py @@ -25,7 +25,7 @@ import subprocess -def find_xctest_tests(blame_lines, names, source, xctestsuperclasses): +def _find_xctest_tests(blame_lines, names, source, xctestsuperclasses): """ Finds the number of XCTest cases per user. @@ -61,7 +61,7 @@ def find_xctest_tests(blame_lines, names, source, xctestsuperclasses): 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. @@ -93,7 +93,7 @@ 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. @@ -125,7 +125,7 @@ def find_boost_tests(blame_lines, names, source): 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. @@ -152,7 +152,7 @@ def find_nose_tests(blame_lines, names, source): 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 @@ -189,18 +189,22 @@ 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 @@ -219,7 +223,7 @@ def find_git_status(directory, xctestsuperclasses): default='') args = parser.parse_args() 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] From c4f80fcfea30bfd2a4263fe895322fcf30c3b630 Mon Sep 17 00:00:00 2001 From: Will Sackfield Date: Thu, 31 Dec 2015 08:17:18 -0500 Subject: [PATCH 4/9] Make main() its own function --- testing-game.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/testing-game.py b/testing-game.py index 1a18af2..9e57039 100755 --- a/testing-game.py +++ b/testing-game.py @@ -209,7 +209,8 @@ def _find_git_status(directory, xctestsuperclasses): 'Could not open file: ' + absfile return names -if __name__ == "__main__": + +def _main(): parser = argparse.ArgumentParser() parser.add_argument('-d', '--directory', @@ -237,3 +238,6 @@ def _find_git_status(directory, xctestsuperclasses): 'n': t[0], 't': t[1], 'p': percentage} + +if __name__ == "__main__": + _main() From db8f5e4ede2ea857e5085c11a546bac74db10c4c Mon Sep 17 00:00:00 2001 From: Will Sackfield Date: Fri, 1 Jan 2016 16:41:13 -0500 Subject: [PATCH 5/9] Add doctests --- .gitignore | 1 + testing-game.py | 39 +++++++++++++++++++++++--------------- tests/ExampleTest.java | 12 ++++++++++++ tests/SPTTestExampleTest.m | 24 +++++++++++++++++++++++ tests/XCTestExampleTest.mm | 28 +++++++++++++++++++++++++++ tests/boost_test.cpp | 12 ++++++++++++ tests/boost_test.mm | 16 ++++++++++++++++ tests/nose_test.py | 5 +++++ 8 files changed, 122 insertions(+), 15 deletions(-) create mode 100644 .gitignore create mode 100644 tests/ExampleTest.java create mode 100644 tests/SPTTestExampleTest.m create mode 100644 tests/XCTestExampleTest.mm create mode 100644 tests/boost_test.cpp create mode 100644 tests/boost_test.mm create mode 100644 tests/nose_test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/testing-game.py b/testing-game.py index 9e57039..a9813a6 100755 --- a/testing-game.py +++ b/testing-game.py @@ -25,6 +25,22 @@ 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. @@ -51,11 +67,7 @@ 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 @@ -83,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 @@ -115,11 +126,7 @@ 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 @@ -144,9 +151,8 @@ def _find_nose_tests(blame_lines, names, source): 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 @@ -165,6 +171,9 @@ def _find_git_status(directory, xctestsuperclasses): 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'] diff --git a/tests/ExampleTest.java b/tests/ExampleTest.java new file mode 100644 index 0000000..48675ad --- /dev/null +++ b/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/tests/SPTTestExampleTest.m b/tests/SPTTestExampleTest.m new file mode 100644 index 0000000..6c8630a --- /dev/null +++ b/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/tests/XCTestExampleTest.mm b/tests/XCTestExampleTest.mm new file mode 100644 index 0000000..4cb33c3 --- /dev/null +++ b/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/tests/boost_test.cpp b/tests/boost_test.cpp new file mode 100644 index 0000000..92c5840 --- /dev/null +++ b/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/tests/boost_test.mm b/tests/boost_test.mm new file mode 100644 index 0000000..b9bd6b1 --- /dev/null +++ b/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/tests/nose_test.py b/tests/nose_test.py new file mode 100644 index 0000000..bb8a8c3 --- /dev/null +++ b/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 From 81988460bb731fabdf3541ba86926e67ca43a18b Mon Sep 17 00:00:00 2001 From: Will Sackfield Date: Fri, 1 Jan 2016 17:24:19 -0500 Subject: [PATCH 6/9] Add dependencies and package information to the README --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fb773d8..4397b49 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,6 +24,13 @@ 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: From fff5b49a22ba480bb9fd801ad47be1c0970624be Mon Sep 17 00:00:00 2001 From: Will Sackfield Date: Mon, 4 Jan 2016 10:53:01 -0500 Subject: [PATCH 7/9] Output version with --version (or -v) --- testing-game.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/testing-game.py b/testing-game.py index a9813a6..2d54632 100755 --- a/testing-game.py +++ b/testing-game.py @@ -231,7 +231,16 @@ def _main(): 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 From 412065a3a1f047d32ef6bb3013f90f05bcb6dc01 Mon Sep 17 00:00:00 2001 From: Will Sackfield Date: Mon, 4 Jan 2016 11:32:00 -0500 Subject: [PATCH 8/9] Add setup.py to the repository --- .gitignore | 3 +++ README.md | 2 +- setup.py | 16 ++++++++++++++++ testing-game.py => testinggame/__init__.py | 0 {tests => testinggame/tests}/ExampleTest.java | 0 .../tests}/SPTTestExampleTest.m | 0 .../tests}/XCTestExampleTest.mm | 0 {tests => testinggame/tests}/boost_test.cpp | 0 {tests => testinggame/tests}/boost_test.mm | 0 {tests => testinggame/tests}/nose_test.py | 0 10 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 setup.py rename testing-game.py => testinggame/__init__.py (100%) rename {tests => testinggame/tests}/ExampleTest.java (100%) rename {tests => testinggame/tests}/SPTTestExampleTest.m (100%) rename {tests => testinggame/tests}/XCTestExampleTest.mm (100%) rename {tests => testinggame/tests}/boost_test.cpp (100%) rename {tests => testinggame/tests}/boost_test.mm (100%) rename {tests => testinggame/tests}/nose_test.py (100%) diff --git a/.gitignore b/.gitignore index 0d20b64..a7fd95c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ *.pyc +/*.egg-info +build/ +dist/ \ No newline at end of file diff --git a/README.md b/README.md index 4397b49..f962ae3 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ The script should run on any operating system containing these two dependencies. 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 100% rename from testing-game.py rename to testinggame/__init__.py diff --git a/tests/ExampleTest.java b/testinggame/tests/ExampleTest.java similarity index 100% rename from tests/ExampleTest.java rename to testinggame/tests/ExampleTest.java diff --git a/tests/SPTTestExampleTest.m b/testinggame/tests/SPTTestExampleTest.m similarity index 100% rename from tests/SPTTestExampleTest.m rename to testinggame/tests/SPTTestExampleTest.m diff --git a/tests/XCTestExampleTest.mm b/testinggame/tests/XCTestExampleTest.mm similarity index 100% rename from tests/XCTestExampleTest.mm rename to testinggame/tests/XCTestExampleTest.mm diff --git a/tests/boost_test.cpp b/testinggame/tests/boost_test.cpp similarity index 100% rename from tests/boost_test.cpp rename to testinggame/tests/boost_test.cpp diff --git a/tests/boost_test.mm b/testinggame/tests/boost_test.mm similarity index 100% rename from tests/boost_test.mm rename to testinggame/tests/boost_test.mm diff --git a/tests/nose_test.py b/testinggame/tests/nose_test.py similarity index 100% rename from tests/nose_test.py rename to testinggame/tests/nose_test.py From e3a0c5922621c8850cd6dc2a6a345e12f778204a Mon Sep 17 00:00:00 2001 From: Will Sackfield Date: Mon, 4 Jan 2016 11:35:45 -0500 Subject: [PATCH 9/9] Add the code of conduct to the CONTRIBUTING markdown --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) 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