Merge changes I405abf9d,I7652eaa0

* changes:
  Minor adjustments to the audio analysis integration tests.
  Creates a once-and-for-all solution to running all UTs.
diff --git a/acts/framework/tests/acts_unittest_suite.py b/acts/framework/tests/acts_unittest_suite.py
deleted file mode 100755
index a4a753f..0000000
--- a/acts/framework/tests/acts_unittest_suite.py
+++ /dev/null
@@ -1,58 +0,0 @@
-#!/usr/bin/env python3
-#
-#   Copyright 2016 - The Android Open Source Project
-#
-#   Licensed 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 sys
-import unittest
-
-import acts_android_device_test
-import acts_asserts_test
-import acts_base_class_test
-import acts_host_utils_test
-import acts_logger_test
-import acts_records_test
-import acts_test_runner_test
-import acts_utils_test
-
-
-def compile_suite():
-    test_classes_to_run = [
-        acts_asserts_test.ActsAssertsTest,
-        acts_base_class_test.ActsBaseClassTest,
-        acts_test_runner_test.ActsTestRunnerTest,
-        acts_android_device_test.ActsAndroidDeviceTest,
-        acts_records_test.ActsRecordsTest,
-        acts_utils_test.ByPassSetupWizardTests,
-        acts_utils_test.ConcurrentActionsTest,
-        acts_logger_test.ActsLoggerTest,
-        acts_host_utils_test.ActsHostUtilsTest
-    ]
-
-    loader = unittest.TestLoader()
-
-    suites_list = []
-    for test_class in test_classes_to_run:
-        suite = loader.loadTestsFromTestCase(test_class)
-        suites_list.append(suite)
-
-    big_suite = unittest.TestSuite(suites_list)
-    return big_suite
-
-
-if __name__ == "__main__":
-    # This is the entry point for running all ACTS unit tests.
-    runner = unittest.TextTestRunner()
-    results = runner.run(compile_suite())
-    sys.exit(not results.wasSuccessful())
diff --git a/acts/framework/tests/audio_analysis_unittest.py b/acts/framework/tests/audio_analysis_integrationtest.py
similarity index 92%
rename from acts/framework/tests/audio_analysis_unittest.py
rename to acts/framework/tests/audio_analysis_integrationtest.py
index d90b822..b41e662 100644
--- a/acts/framework/tests/audio_analysis_unittest.py
+++ b/acts/framework/tests/audio_analysis_integrationtest.py
@@ -14,11 +14,18 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+# Note: This test has been labelled as an integration test due to its use of
+# real data, and the five to six second execution time.
 import logging
 import numpy
 import os
 import unittest
 
+# TODO(markdr): Remove this after soundfile is added to setup.py
+import sys
+import mock
+sys.modules['soundfile'] = mock.Mock()
+
 import acts.test_utils.audio_analysis_lib.audio_analysis as audio_analysis
 import acts.test_utils.audio_analysis_lib.audio_data as audio_data
 
@@ -83,13 +90,13 @@
         # Sort the peaks by values.
         return sorted(results, key=lambda x: x[1], reverse=True)
 
-    def testPeakDetection(self):
+    def test_peak_detection(self):
         array = [0, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 5, 3, 2, 1, 1, 1, 1, 1]
         result = audio_analysis.peak_detection(array, 4)
         golden_answer = [(12, 5), (4, 4)]
         self.assertEqual(result, golden_answer)
 
-    def testPeakDetectionLarge(self):
+    def test_peak_detection_large(self):
         array = numpy.random.uniform(0, 1, 1000000)
         window_size = 100
         logging.debug('Test large array using dummy peak detection')
@@ -99,7 +106,7 @@
         logging.debug('Compare the result')
         self.assertEqual(dummy_answer, improved_answer)
 
-    def testSpectralAnalysis(self):
+    def test_spectral_analysis(self):
         rate = 48000
         length_in_secs = 0.5
         freq_1 = 490.0
@@ -123,7 +130,7 @@
         self.assertTrue(
             abs(results[0][1] / results[1][1] - coeff_1 / coeff_2) < 0.01)
 
-    def testSpectralAnalysisRealData(self):
+    def test_spectral_snalysis_real_data(self):
         """This unittest checks the spectral analysis works on real data."""
         file_path = os.path.join(
             os.path.dirname(__file__), 'test_data', '1k_2k.raw')
@@ -142,7 +149,7 @@
                 abs(spectral[0][0] - golden_frequency[channel]) < 5,
                 'Dominant frequency is not correct')
 
-    def testNotMeaningfulData(self):
+    def test_not_meaningful_data(self):
         """Checks that sepectral analysis handles un-meaningful data."""
         rate = 48000
         length_in_secs = 0.5
@@ -159,7 +166,7 @@
 
 
 class NormalizeTest(unittest.TestCase):
-    def testNormalize(self):
+    def test_normalize(self):
         y = [1, 2, 3, 4, 5]
         normalized_y = audio_analysis.normalize_signal(y, 10)
         expected = numpy.array([0.1, 0.2, 0.3, 0.4, 0.5])
@@ -251,16 +258,16 @@
             self.assertTrue(detected_secs <= expected_detected_range_secs[1])
             self.assertTrue(detected_secs >= expected_detected_range_secs[0])
 
-    def testGoodSignal(self):
+    def test_good_signal(self):
         """Sine wave signal with no noise or anomaly."""
         self.check_no_anomaly()
 
-    def testGoodSignalNoise(self):
+    def test_good_signal_noise(self):
         """Sine wave signal with noise."""
         self.add_noise()
         self.check_no_anomaly()
 
-    def testZeroAnomaly(self):
+    def test_zero_anomaly(self):
         """Sine wave signal with no noise but with anomaly.
 
         This test case simulates underrun in digital data where there will be
@@ -271,7 +278,7 @@
         self.insert_anomaly()
         self.check_anomaly()
 
-    def testZeroAnomalyNoise(self):
+    def test_zero_anomaly_noise(self):
         """Sine wave signal with noise and anomaly.
 
         This test case simulates underrun in analog data where there will be
@@ -283,7 +290,7 @@
         self.add_noise()
         self.check_anomaly()
 
-    def testLowConstantAnomaly(self):
+    def test_low_constant_anomaly(self):
         """Sine wave signal with low constant anomaly.
 
         The anomaly is one block of constant values.
@@ -293,7 +300,7 @@
         self.insert_anomaly()
         self.check_anomaly()
 
-    def testLowConstantAnomalyNoise(self):
+    def test_low_constant_anomaly_noise(self):
         """Sine wave signal with low constant anomaly and noise.
 
         The anomaly is one block of constant values.
@@ -304,7 +311,7 @@
         self.add_noise()
         self.check_anomaly()
 
-    def testHighConstantAnomaly(self):
+    def test_high_constant_anomaly(self):
         """Sine wave signal with high constant anomaly.
 
         The anomaly is one block of constant values.
@@ -314,7 +321,7 @@
         self.insert_anomaly()
         self.check_anomaly()
 
-    def testHighConstantAnomalyNoise(self):
+    def test_high_constant_anomaly_noise(self):
         """Sine wave signal with high constant anomaly and noise.
 
         The anomaly is one block of constant values.
@@ -325,7 +332,7 @@
         self.add_noise()
         self.check_anomaly()
 
-    def testSkippedAnomaly(self):
+    def test_skipped_anomaly(self):
         """Sine wave signal with skipped anomaly.
 
         The anomaly simulates the symptom where a block is skipped.
@@ -334,7 +341,7 @@
         self.generate_skip_anomaly()
         self.check_anomaly()
 
-    def testSkippedAnomalyNoise(self):
+    def test_skipped_anomaly_noise(self):
         """Sine wave signal with skipped anomaly with noise.
 
         The anomaly simulates the symptom where a block is skipped.
@@ -344,7 +351,7 @@
         self.add_noise()
         self.check_anomaly()
 
-    def testEmptyData(self):
+    def test_empty_data(self):
         """Checks that anomaly detection rejects empty data."""
         self.y = []
         with self.assertRaises(audio_analysis.EmptyDataError):
diff --git a/acts/framework/tests/audio_quality_measurement_unittest.py b/acts/framework/tests/audio_quality_measurement_integrationtest.py
similarity index 93%
rename from acts/framework/tests/audio_quality_measurement_unittest.py
rename to acts/framework/tests/audio_quality_measurement_integrationtest.py
index 0166ce9..e6e0c70 100644
--- a/acts/framework/tests/audio_quality_measurement_unittest.py
+++ b/acts/framework/tests/audio_quality_measurement_integrationtest.py
@@ -14,15 +14,20 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-import logging
+# Note: This test has been labelled as an integration test due to its use of
+# real data, and the 12+ second execution time. It also generates sine waves
+# during the test, rather than using data that has been pre-calculated.
+
 import math
 import numpy
 import unittest
 
-import acts.test_utils.audio_analysis_lib.audio_data as audio_data
-import acts.test_utils.audio_analysis_lib.audio_analysis as audio_analysis
-import acts.test_utils.audio_analysis_lib.audio_quality_measurement as \
-    audio_quality_measurement
+# TODO(markdr): Remove this after soundfile is added to setup.py
+import sys
+import mock
+sys.modules['soundfile'] = mock.Mock()
+
+import acts.test_utils.audio_analysis_lib.audio_quality_measurement as audio_quality_measurement
 
 
 class NoiseLevelTest(unittest.TestCase):
@@ -30,7 +35,7 @@
         """Uses the same seed to generate noise for each test."""
         numpy.random.seed(0)
 
-    def testNoiseLevel(self):
+    def test_noise_level(self):
         # Generates the standard sin wave with standard_noise portion of noise.
         rate = 48000
         length_in_secs = 2
@@ -61,7 +66,7 @@
 
 
 class ErrorTest(unittest.TestCase):
-    def testError(self):
+    def test_error(self):
         value1 = [0.2, 0.4, 0.1, 0.01, 0.01, 0.01]
         value2 = [0.3, 0.3, 0.08, 0.0095, 0.0098, 0.0099]
         error = [0.5, 0.25, 0.2, 0.05, 0.02, 0.01]
@@ -138,7 +143,7 @@
                 self.y[j] = self.amplitude * (3 + numpy.random.uniform(-1, 1))
 
     def generate_volume_changing(self):
-        "Generates volume changing during playing."
+        """Generates volume changing during playing."""
         start_time = [0.300, 1.400]
         end_time = [0.600, 1.700]
         for i in range(len(start_time)):
@@ -149,7 +154,7 @@
         self.volume_changing = [+1, -1, +1, -1]
         self.volume_changing_time = [0.3, 0.6, 1.4, 1.7]
 
-    def testGoodSignal(self):
+    def test_good_signal(self):
         """Sine wave signal with no noise or artifacts."""
         result = audio_quality_measurement.quality_measurement(self.y,
                                                                self.rate)
@@ -160,7 +165,7 @@
         self.assertTrue(len(result['volume_changes']) == 0)
         self.assertTrue(result['equivalent_noise_level'] < 0.005)
 
-    def testGoodSignalNoise(self):
+    def test_good_signal_with_noise(self):
         """Sine wave signal with noise."""
         self.add_noise()
         result = audio_quality_measurement.quality_measurement(self.y,
@@ -170,10 +175,9 @@
         self.assertTrue(len(result['artifacts']['delay_during_playback']) == 0)
         self.assertTrue(len(result['artifacts']['burst_during_playback']) == 0)
         self.assertTrue(len(result['volume_changes']) == 0)
-        self.assertTrue(0.009 < result['equivalent_noise_level'] and
-                        result['equivalent_noise_level'] < 0.011)
+        self.assertTrue(0.009 < result['equivalent_noise_level'] < 0.011)
 
-    def testDelay(self):
+    def test_delay(self):
         """Sine wave with delay during playing."""
         self.generate_delay()
         result = audio_quality_measurement.quality_measurement(self.y,
@@ -196,7 +200,7 @@
                         duration)
             self.assertTrue(delta < 0.001)
 
-    def testArtifactsBeforePlayback(self):
+    def test_artifacts_before_playback(self):
         """Sine wave with artifacts before playback."""
         self.generate_artifacts_before_playback()
         result = audio_quality_measurement.quality_measurement(self.y,
@@ -212,7 +216,7 @@
         self.assertTrue(len(result['volume_changes']) == 0)
         self.assertTrue(result['equivalent_noise_level'] < 0.005)
 
-    def testArtifactsAfterPlayback(self):
+    def test_artifacts_after_playback(self):
         """Sine wave with artifacts after playback."""
         self.generate_artifacts_after_playback()
         result = audio_quality_measurement.quality_measurement(self.y,
@@ -228,7 +232,7 @@
         self.assertTrue(len(result['volume_changes']) == 0)
         self.assertTrue(result['equivalent_noise_level'] < 0.005)
 
-    def testBurstDuringPlayback(self):
+    def test_burst_during_playback(self):
         """Sine wave with burst during playback."""
         self.generate_burst_during_playback()
         result = audio_quality_measurement.quality_measurement(self.y,
@@ -244,7 +248,7 @@
                 'burst_during_playback'][i])
             self.assertTrue(delta < 0.002)
 
-    def testVolumeChanging(self):
+    def test_volume_changing(self):
         """Sine wave with volume changing during playback."""
         self.generate_volume_changing()
         result = audio_quality_measurement.quality_measurement(self.y,
diff --git a/acts/framework/tests/controllers/android_lib/android_lib_unittest_bundle.py b/acts/framework/tests/controllers/android_lib/android_lib_unittest_bundle.py
deleted file mode 100755
index cf8e81e..0000000
--- a/acts/framework/tests/controllers/android_lib/android_lib_unittest_bundle.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-#
-#   Copyright 2018 - The Android Open Source Project
-#
-#   Licensed 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 os
-import sys
-import unittest
-
-
-def main():
-    suite = unittest.TestLoader().discover(
-        start_dir=os.path.dirname(__file__), pattern='*_test.py')
-    return suite
-
-
-if __name__ == '__main__':
-    test_suite = main()
-    runner = unittest.TextTestRunner()
-    test_run = runner.run(test_suite)
-    sys.exit(not test_run.wasSuccessful())
diff --git a/acts/framework/tests/controllers/sl4a_lib/test_suite.py b/acts/framework/tests/controllers/sl4a_lib/test_suite.py
deleted file mode 100755
index de6835c..0000000
--- a/acts/framework/tests/controllers/sl4a_lib/test_suite.py
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/usr/bin/env python3
-#
-#   Copyright 2018 - The Android Open Source Project
-#
-#   Licensed 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 sys
-import unittest
-
-import rpc_client_test
-import rpc_connection_test
-import sl4a_manager_test
-import sl4a_session_test
-
-
-def compile_suite():
-    test_classes_to_run = [
-        rpc_client_test.RpcClientTest,
-        rpc_connection_test.RpcConnectionTest,
-        sl4a_manager_test.Sl4aManagerFactoryTest,
-        sl4a_manager_test.Sl4aManagerTest,
-        sl4a_session_test.Sl4aSessionTest,
-    ]
-    loader = unittest.TestLoader()
-
-    suites_list = []
-    for test_class in test_classes_to_run:
-        suite = loader.loadTestsFromTestCase(test_class)
-        suites_list.append(suite)
-
-    big_suite = unittest.TestSuite(suites_list)
-    return big_suite
-
-
-if __name__ == "__main__":
-    # This is the entry point for running all SL4A Lib unit tests.
-    runner = unittest.TextTestRunner()
-    results = runner.run(compile_suite())
-    sys.exit(not results.wasSuccessful())
diff --git a/acts/framework/tests/event/event_unittest_bundle.py b/acts/framework/tests/event/event_unittest_bundle.py
deleted file mode 100755
index 56c3e09..0000000
--- a/acts/framework/tests/event/event_unittest_bundle.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-#
-#   Copyright 2017 - The Android Open Source Project
-#
-#   Licensed 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 os
-import sys
-import unittest
-
-
-def main():
-    suite = unittest.TestLoader().discover(
-        start_dir=os.path.dirname(__file__), pattern='*_test.py')
-    return suite
-
-
-if __name__ == '__main__':
-    test_suite = main()
-    runner = unittest.TextTestRunner()
-    test_run = runner.run(test_suite)
-    sys.exit(not test_run.wasSuccessful())
diff --git a/acts/framework/tests/libs/logging/logging_unittest_bundle.py b/acts/framework/tests/libs/logging/logging_unittest_bundle.py
deleted file mode 100755
index cf8e81e..0000000
--- a/acts/framework/tests/libs/logging/logging_unittest_bundle.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-#
-#   Copyright 2018 - The Android Open Source Project
-#
-#   Licensed 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 os
-import sys
-import unittest
-
-
-def main():
-    suite = unittest.TestLoader().discover(
-        start_dir=os.path.dirname(__file__), pattern='*_test.py')
-    return suite
-
-
-if __name__ == '__main__':
-    test_suite = main()
-    runner = unittest.TextTestRunner()
-    test_run = runner.run(test_suite)
-    sys.exit(not test_run.wasSuccessful())
diff --git a/acts/framework/tests/libs/metrics/unittest_bundle.py b/acts/framework/tests/libs/metrics/unittest_bundle.py
deleted file mode 100755
index 87d5bc5..0000000
--- a/acts/framework/tests/libs/metrics/unittest_bundle.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-#
-#   Copyright 2017 - The Android Open Source Project
-#
-#   Licensed 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 os
-import sys
-import unittest
-
-
-def main():
-    suite = unittest.TestLoader().discover(
-        start_dir=os.path.dirname(__file__), pattern='*_test.py')
-    return suite
-
-
-if __name__ == "__main__":
-    test_suite = main()
-    runner = unittest.TextTestRunner()
-    test_run = runner.run(test_suite)
-    sys.exit(not test_run.wasSuccessful())
diff --git a/acts/framework/tests/libs/ota/unittest_bundle.py b/acts/framework/tests/libs/ota/unittest_bundle.py
deleted file mode 100755
index 87d5bc5..0000000
--- a/acts/framework/tests/libs/ota/unittest_bundle.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-#
-#   Copyright 2017 - The Android Open Source Project
-#
-#   Licensed 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 os
-import sys
-import unittest
-
-
-def main():
-    suite = unittest.TestLoader().discover(
-        start_dir=os.path.dirname(__file__), pattern='*_test.py')
-    return suite
-
-
-if __name__ == "__main__":
-    test_suite = main()
-    runner = unittest.TextTestRunner()
-    test_run = runner.run(test_suite)
-    sys.exit(not test_run.wasSuccessful())
diff --git a/acts/framework/tests/libs/proc/proc_unittest_bundle.py b/acts/framework/tests/libs/proc/proc_unittest_bundle.py
deleted file mode 100755
index cf8e81e..0000000
--- a/acts/framework/tests/libs/proc/proc_unittest_bundle.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-#
-#   Copyright 2018 - The Android Open Source Project
-#
-#   Licensed 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 os
-import sys
-import unittest
-
-
-def main():
-    suite = unittest.TestLoader().discover(
-        start_dir=os.path.dirname(__file__), pattern='*_test.py')
-    return suite
-
-
-if __name__ == '__main__':
-    test_suite = main()
-    runner = unittest.TextTestRunner()
-    test_run = runner.run(test_suite)
-    sys.exit(not test_run.wasSuccessful())
diff --git a/acts/framework/tests/metrics/unittest_bundle.py b/acts/framework/tests/metrics/unittest_bundle.py
deleted file mode 100755
index 56c3e09..0000000
--- a/acts/framework/tests/metrics/unittest_bundle.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-#
-#   Copyright 2017 - The Android Open Source Project
-#
-#   Licensed 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 os
-import sys
-import unittest
-
-
-def main():
-    suite = unittest.TestLoader().discover(
-        start_dir=os.path.dirname(__file__), pattern='*_test.py')
-    return suite
-
-
-if __name__ == '__main__':
-    test_suite = main()
-    runner = unittest.TextTestRunner()
-    test_run = runner.run(test_suite)
-    sys.exit(not test_run.wasSuccessful())
diff --git a/acts/framework/tests/test_runner_test.py b/acts/framework/tests/test_runner_test.py
index 2ec3da4..066c855 100755
--- a/acts/framework/tests/test_runner_test.py
+++ b/acts/framework/tests/test_runner_test.py
@@ -14,11 +14,11 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import os
 import shutil
 import tempfile
 import unittest
 
-import os
 from mobly.config_parser import TestRunConfig
 from mock import Mock
 from mock import patch
@@ -122,5 +122,5 @@
                          expected_timestamp))
 
 
-if __name__ == "__main__":
+if __name__ == '__main__':
     unittest.main()
diff --git a/acts/framework/tests/test_suite.py b/acts/framework/tests/test_suite.py
new file mode 100755
index 0000000..d10586b
--- /dev/null
+++ b/acts/framework/tests/test_suite.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+#
+#   Copyright 2017 - The Android Open Source Project
+#
+#   Licensed 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 os
+import sys
+import tempfile
+import unittest
+import multiprocessing
+
+
+def run_tests(test_suite, output_file):
+    # Redirects stdout and stderr to the given output file.
+    stdout_fd = os.open(output_file, os.O_WRONLY | os.O_CREAT)
+    stderr_fd = os.dup(stdout_fd)
+    os.dup2(stdout_fd, 1)
+    os.dup2(stderr_fd, 2)
+    test_run = unittest.TextTestRunner(verbosity=2).run(test_suite)
+    return test_run.wasSuccessful()
+
+
+class TestResult(object):
+    def __init__(self, process_result, output_file, test_suite):
+        self.process_result = process_result
+        self.output_file = output_file
+        self.test_suite = test_suite
+
+
+def run_all_unit_tests():
+    # Due to some incredibly powerful black magic, running this twice
+    # causes the metrics/, test_utils/ and test_runner_test.py tests to load
+    # properly. They do no load properly the first time.
+    suite = unittest.TestLoader().discover(
+        start_dir=os.path.dirname(__file__), pattern='*_test.py')
+    suite = unittest.TestLoader().discover(
+        start_dir=os.path.dirname(__file__), pattern='*_test.py')
+
+    process_pool = multiprocessing.Pool(10)
+    output_dir = tempfile.mkdtemp()
+
+    results = []
+
+    for index, test in enumerate(suite._tests):
+        output_file = os.path.join(output_dir, 'test_%s.output' % index)
+        process_result = process_pool.apply_async(run_tests,
+                                                  args=(test, output_file))
+        results.append(TestResult(process_result, output_file, test))
+
+    success = True
+    for index, result in enumerate(results):
+        try:
+            if not result.process_result.get(timeout=60):
+                success = False
+                print('Received the following test failure:')
+                with open(result.output_file, 'r') as out_file:
+                    print(out_file.read())
+        except multiprocessing.TimeoutError:
+            success = False
+            print('The following test timed out: %r' % result.test_suite,
+                  file=sys.stderr)
+            with open(result.output_file, 'r') as out_file:
+                print(out_file.read())
+
+    exit(not success)
+
+
+if __name__ == '__main__':
+    run_all_unit_tests()
diff --git a/acts/framework/tests/controllers/test_suite.py b/acts/framework/tests/test_utils/__init__.py
similarity index 100%
rename from acts/framework/tests/controllers/test_suite.py
rename to acts/framework/tests/test_utils/__init__.py
diff --git a/acts/framework/tests/controllers/test_suite.py b/acts/framework/tests/test_utils/instrumentation/__init__.py
similarity index 100%
copy from acts/framework/tests/controllers/test_suite.py
copy to acts/framework/tests/test_utils/instrumentation/__init__.py
diff --git a/acts/framework/tests/controllers/test_suite.py b/acts/framework/tests/test_utils/power/__init__.py
similarity index 100%
copy from acts/framework/tests/controllers/test_suite.py
copy to acts/framework/tests/test_utils/power/__init__.py
diff --git a/acts/framework/tests/controllers/test_suite.py b/acts/framework/tests/test_utils/power/tel/__init__.py
similarity index 100%
copy from acts/framework/tests/controllers/test_suite.py
copy to acts/framework/tests/test_utils/power/tel/__init__.py
diff --git a/acts/tests/meta/ActsUnitTest.py b/acts/tests/meta/ActsUnitTest.py
index 89db2c4..bcf50dd 100755
--- a/acts/tests/meta/ActsUnitTest.py
+++ b/acts/tests/meta/ActsUnitTest.py
@@ -23,46 +23,8 @@
 from acts import base_test
 from acts import signals
 
-# The files under acts/framework to consider as unit tests.
-UNITTEST_FILES = [
-    'tests/acts_adb_test.py',
-    'tests/acts_android_device_test.py',
-    'tests/acts_asserts_test.py',
-    'tests/acts_base_class_test.py',
-    'tests/acts_context_test.py',
-    'tests/acts_error_test.py',
-    'tests/acts_host_utils_test.py',
-    'tests/acts_import_test_utils_test.py',
-    'tests/acts_import_unit_test.py',
-    'tests/acts_job_test.py',
-    'tests/libs/ota/unittest_bundle.py',
-    'tests/acts_logger_test.py',
-    'tests/libs/metrics/unittest_bundle.py',
-    'tests/acts_records_test.py',
-    'tests/acts_relay_controller_test.py',
-    'tests/acts_test_runner_test.py',
-    'tests/acts_unittest_suite.py',
-    'tests/acts_utils_test.py',
-    'tests/controllers/android_lib/android_lib_unittest_bundle.py',
-    'tests/event/event_unittest_bundle.py',
-    'tests/test_utils/instrumentation/unit_test_suite.py',
-    'tests/libs/logging/logging_unittest_bundle.py',
-    'tests/metrics/unittest_bundle.py',
-    'tests/libs/proc/proc_unittest_bundle.py',
-    'tests/controllers/sl4a_lib/test_suite.py',
-    'tests/test_runner_test.py',
-    'tests/libs/version_selector_test.py',
-    'tests/test_utils/power/tel/lab/consume_parameter_test.py',
-    'tests/test_utils/power/tel/lab/ensure_valid_calibration_table_test.py',
-    'tests/test_utils/power/tel/lab/init_simulation_test.py',
-    'tests/test_utils/power/tel/lab/initialize_simulator_test.py',
-    'tests/test_utils/power/tel/lab/save_summary_to_file_test.py',
-    'tests/test_utils/power/tel/lab/power_tel_traffic_e2e_test.py'
-]
-
-# The number of seconds to wait before considering the unit test to have timed
-# out.
-UNITTEST_TIMEOUT = 60
+# The number of seconds to wait before considering the test to have timed out.
+TIMEOUT = 60
 
 
 class ActsUnitTest(base_test.BaseTestClass):
@@ -71,47 +33,39 @@
     This is a hack to run the ACTS unit tests through CI. Please use the main
     function below if you need to run these tests.
     """
+
     def test_units(self):
-        """Runs all the ACTS unit tests in parallel."""
-        acts_unittest_path = os.path.dirname(acts.__path__[0])
-        test_processes = []
+        """Runs all the ACTS unit tests in test_suite.py."""
+        test_script = os.path.join(os.path.dirname(acts.__path__[0]),
+                                   'tests/test_suite.py')
+        test_process = subprocess.Popen([sys.executable, test_script],
+                                        stdout=subprocess.PIPE,
+                                        stderr=subprocess.STDOUT)
 
-        fail_test = False
+        killed = False
+        try:
+            stdout, _ = test_process.communicate(timeout=TIMEOUT)
+        except subprocess.TimeoutExpired:
+            killed = True
+            self.log.error('Test %s timed out after %s seconds.' %
+                           (test_process.args, TIMEOUT))
+            test_process.kill()
+            stdout, _ = test_process.communicate()
 
-        for unittest_file in UNITTEST_FILES:
-            file_path = os.path.join(acts_unittest_path, unittest_file)
-            test_processes.append(
-                subprocess.Popen([sys.executable, file_path],
-                                 stdout=subprocess.PIPE,
-                                 stderr=subprocess.STDOUT))
-
-        for test_process in test_processes:
-            killed = False
-            try:
-                stdout, _ = test_process.communicate(timeout=UNITTEST_TIMEOUT)
-            except subprocess.TimeoutExpired:
-                killed = True
-                self.log.error('Unit test %s timed out after %s seconds.' %
-                               (test_process.args, UNITTEST_TIMEOUT))
-                test_process.kill()
-                stdout, _ = test_process.communicate()
-            if test_process.returncode != 0 or killed:
-                self.log.error('=' * 79)
-                self.log.error('Unit Test %s failed with error %s.' %
-                               (test_process.args, test_process.returncode))
-                self.log.error('=' * 79)
-                self.log.error('Failure for `%s`:\n%s' %
-                               (test_process.args,
-                                stdout.decode('utf-8', errors='replace')))
-                fail_test = True
-            else:
-                self.log.debug('Output for `%s`:\n%s' %
-                               (test_process.args,
-                                stdout.decode('utf-8', errors='replace')))
-
-        if fail_test:
+        if test_process.returncode != 0 or killed:
+            self.log.error('=' * 79)
+            self.log.error('Test %s failed with error %s.' %
+                           (test_process.args, test_process.returncode))
+            self.log.error('=' * 79)
+            self.log.error('Failure for `%s`:\n%s' %
+                           (test_process.args,
+                            stdout.decode('utf-8', errors='replace')))
             raise signals.TestFailure(
                 'One or more unit tests failed. See the logs.')
+        else:
+            self.log.debug('Output for `%s`:\n%s' %
+                           (test_process.args,
+                            stdout.decode('utf-8', errors='replace')))
 
 
 def main():