Repository: DataBiosphere/toil

Issue:
Build takes too long



Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -12,13 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import sys
 from version import version
 from setuptools import find_packages, setup
 
-botoVersionRequired = 'boto==2.38.0'
+botoRequirement = 'boto==2.38.0'
 
-kwargs = dict(
+setup(
     name='toil',
     version=version,
     description='Pipeline management software for clusters.',
@@ -27,15 +26,11 @@
     url="https://github.com/BD2KGenomics/toil",
     install_requires=[
         'bd2k-python-lib==1.13.dev14'],
-    tests_require=[
-        'mock==1.0.1',
-        'pytest==2.8.3'],
-    test_suite='toil',
     extras_require={
         'mesos': [
             'psutil==3.0.1'],
         'aws': [
-            botoVersionRequired,
+            botoRequirement,
             'cgcloud-lib==1.4a1.dev195' ],
         'azure': [
             'azure==1.0.3'],
@@ -43,7 +38,7 @@
             'pynacl==0.3.0'],
         'google': [
             'gcs_oauth2_boto_plugin==1.9',
-            botoVersionRequired],
+            botoRequirement],
         'cwl': [
             'cwltool==1.0.20160425140546']},
     package_dir={'': 'src'},
@@ -55,30 +50,3 @@
             'cwltoil = toil.cwl.cwltoil:main [cwl]',
             'cwl-runner = toil.cwl.cwltoil:main [cwl]',
             '_toil_mesos_executor = toil.batchSystems.mesos.executor:main [mesos]']})
-
-from setuptools.command.test import test as TestCommand
-
-
-class PyTest(TestCommand):
-    user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]
-
-    def initialize_options(self):
-        TestCommand.initialize_options(self)
-        self.pytest_args = []
-
-    def finalize_options(self):
-        TestCommand.finalize_options(self)
-        self.test_args = []
-        self.test_suite = True
-
-    def run_tests(self):
-        import pytest
-        # Sanitize command line arguments to avoid confusing Toil code attempting to parse them
-        sys.argv[1:] = []
-        errno = pytest.main(self.pytest_args)
-        sys.exit(errno)
-
-
-kwargs['cmdclass'] = {'test': PyTest}
-
-setup(**kwargs)


Test patch:
diff --git a/run_tests.py b/run_tests.py
new file mode 100644
--- /dev/null
+++ b/run_tests.py
@@ -0,0 +1,84 @@
+import glob
+import itertools
+import logging
+import os
+import subprocess
+import sys
+
+log = logging.getLogger(__name__)
+
+# A "keyword" is an argument to pytest's -k option. It acts as a selector for tests. Each of the 
+# keywords in the list below will be run concurrently. Once they are done, everything else will 
+# be run sequentially. Please note that keywords are match as substrings: Foo will match Foo, 
+# FooBar and BarFoo. 
+#
+
+parallelizable_keywords = [
+    'SortTest',
+    'AWSJobStoreTest',
+    'AzureJobStoreTest',
+    'FileJobStoreTest',
+    'GoogleJobStoreTest',
+    'AwsJobStoreCacheTest',
+    'AzureJobStoreCacheTest',
+    'FileJobStoreCacheTest',
+    'GoogleJobStoreCacheTest']
+
+
+def run_tests(keywords, index, args):
+    args = [sys.executable, '-m', 'pytest', '-vv',
+            '--junitxml', 'test-report-%s.xml' % index,
+            '-k', keywords] + args
+    log.info('Running %r', args)
+    return subprocess.Popen(args)
+
+
+def main(args):
+    for name in glob.glob('test-report-*.xml'):
+        os.unlink(name)
+    num_failures = 0
+    index = itertools.count()
+    pids = set()
+    try:
+        for keyword in parallelizable_keywords:
+            process = run_tests(keyword, str(next(index)), args)
+            pids.add(process.pid)
+        while pids:
+            pid, status = os.wait()
+            pids.remove(pid)
+            if os.WIFEXITED(status):
+                status = os.WEXITSTATUS(status)
+                if status:
+                    num_failures += 1
+            else:
+                num_failures += 1
+    except:
+        for pid in pids:
+            os.kill(pid, 15)
+        raise
+
+    everything_else = ' and '.join('not ' + keyword for keyword in parallelizable_keywords)
+    process = run_tests(everything_else, str(next(index)), args)
+    if process.wait():
+        num_failures += 1
+
+    import xml.etree.ElementTree as ET
+    testsuites = ET.Element('testsuites')
+    for name in glob.glob('test-report-*.xml'):
+        log.info("Reading test report %s", name)
+        tree = ET.parse(name)
+        testsuites.append(tree.getroot())
+        os.unlink(name)
+    name = 'test-report.xml'
+    log.info('Writing aggregate test report %s', name)
+    ET.ElementTree(testsuites).write(name, xml_declaration=True)
+
+    if num_failures:
+        log.error('%i out %i child processes failed', num_failures, next(index))
+
+    return num_failures
+
+
+if __name__ == '__main__':
+    logging.basicConfig(level=logging.INFO)
+    sys.exit(main(sys.argv[1:]))
diff --git a/src/toil/test/__init__.py b/src/toil/test/__init__.py
--- a/src/toil/test/__init__.py
+++ b/src/toil/test/__init__.py
@@ -19,9 +19,9 @@
 import unittest
 import shutil
 import re
-import subprocess
 
 from bd2k.util.files import mkdir_p
+from bd2k.util.processes import which
 
 from toil import toilPackageDirPath
 
@@ -198,15 +198,11 @@ def needs_gridengine(test_item):
     Use as a decorator before test classes or methods to only run them if GridEngine is installed.
     """
     test_item = _mark_test('gridengine', test_item)
-    try:
-        with open(os.devnull, 'r+') as devnull:
-            subprocess.Popen('qsub', stdout=devnull, stderr=devnull, stdin=devnull)
-    except OSError:
-        return unittest.skip("Skipping test. Install GridEngine to include this test.")(test_item)
-    except:
-        raise
-    else:
+    if next(which('qsub'), None):
         return test_item
+    else:
+        return unittest.skip("Skipping test. Install GridEngine to include this test.")(test_item)
+        
 
 
 def needs_mesos(test_item):
@@ -232,15 +228,10 @@ def needs_parasol(test_item):
     Use as decorator so tests are only run if Parasol is installed.
     """
     test_item = _mark_test('parasol', test_item)
-    try:
-        with open(os.devnull, 'r+') as devnull:
-            subprocess.Popen('parasol', stdout=devnull, stderr=devnull, stdin=devnull)
-    except OSError:
-        return unittest.skip("Skipping test. Install Parasol to include this test.")(test_item)
-    except:
-        raise
-    else:
+    if next(which('parasol'), None):
         return test_item
+    else:
+        return unittest.skip("Skipping test. Install Parasol to include this test.")(test_item)
 
 
 def needs_slurm(test_item):
@@ -248,15 +239,10 @@ def needs_slurm(test_item):
     Use as a decorator before test classes or methods to only run them if Slurm is installed.
     """
     test_item = _mark_test('slurm', test_item)
-    try:
-        with open(os.devnull, 'r+') as devnull:
-            subprocess.Popen('squeue', stdout=devnull, stderr=devnull, stdin=devnull)
-    except OSError:
-        return unittest.skip("Skipping test. Install Slurm to include this test.")(test_item)
-    except:
-        raise
-    else:
+    if next(which('squeue'), None):
         return test_item
+    else:
+        return unittest.skip("Skipping test. Install Slurm to include this test.")(test_item)
 
 
 def needs_encryption(test_item):