Support alias for test classes when using suite. (#467)

The same test class can be run with different configs.
To report the same class run with different configs, we need to allow users to specify a suffix to differentiate the runs.
diff --git a/mobly/base_test.py b/mobly/base_test.py
index 03f7822..9eed6af 100644
--- a/mobly/base_test.py
+++ b/mobly/base_test.py
@@ -84,8 +84,12 @@
             configs: A config_parser.TestRunConfig object.
         """
         self.tests = []
-        if not self.TAG:
-            self.TAG = self.__class__.__name__
+        self._class_name = self.__class__.__name__
+        if configs.test_class_name_suffix and self.TAG is None:
+            self.TAG = '%s_%s' % (self._class_name,
+                                  configs.test_class_name_suffix)
+        elif self.TAG is None:
+            self.TAG = self._class_name
         # Set params.
         self.log_path = configs.log_path
         self.controller_configs = configs.controller_configs
diff --git a/mobly/config_parser.py b/mobly/config_parser.py
index 4eed6a1..aa43c03 100644
--- a/mobly/config_parser.py
+++ b/mobly/config_parser.py
@@ -169,6 +169,9 @@
             modules.
         summary_writer: records.TestSummaryWriter, used to write elements to
             the test result summary file.
+        test_class_name_suffix: string, suffix to append to the class name for
+                reporting. This is used for differentiating the same class
+                executed with different parameters in a suite.
     """
 
     def __init__(self):
@@ -178,6 +181,7 @@
         self.user_params = None
         self.register_controller = None
         self.summary_writer = None
+        self.test_class_name_suffix = None
 
     def copy(self):
         """Returns a deep copy of the current config.
diff --git a/mobly/test_runner.py b/mobly/test_runner.py
index 4e4b666..af834e8 100644
--- a/mobly/test_runner.py
+++ b/mobly/test_runner.py
@@ -265,9 +265,14 @@
         run it with.
         """
 
-        def __init__(self, config, test_class, tests=None):
+        def __init__(self,
+                     config,
+                     test_class,
+                     tests=None,
+                     test_class_name_suffix=None):
             self.config = config
             self.test_class = test_class
+            self.test_class_name_suffix = test_class_name_suffix
             self.tests = tests
 
     def __init__(self, log_dir, test_bed_name):
@@ -329,7 +334,7 @@
         logger.kill_test_logger(logging.getLogger())
         self._log_path = None
 
-    def add_test_class(self, config, test_class, tests=None):
+    def add_test_class(self, config, test_class, tests=None, name_suffix=None):
         """Adds tests to the execution plan of this TestRunner.
 
         Args:
@@ -338,6 +343,9 @@
             test_class: class, test class to execute.
             tests: list of strings, optional list of test names within the
                 class to execute.
+            name_suffix: string, suffix to append to the class name for
+                reporting. This is used for differentiating the same class
+                executed with different parameters in a suite.
 
         Raises:
             Error: if the provided config has a log_path or test_bed_name which
@@ -356,7 +364,10 @@
                 (self._test_bed_name, config.test_bed_name))
         self._test_run_infos.append(
             TestRunner._TestRunInfo(
-                config=config, test_class=test_class, tests=tests))
+                config=config,
+                test_class=test_class,
+                tests=tests,
+                test_class_name_suffix=name_suffix))
 
     def _run_test_class(self, config, test_class, tests=None):
         """Instantiates and executes a test class.
@@ -370,6 +381,7 @@
             test_class: class, test class to execute.
             tests: Optional list of test names within the class to execute.
         """
+
         with test_class(config) as test_instance:
             try:
                 cls_result = test_instance.run(tests)
@@ -402,9 +414,12 @@
                 test_config.register_controller = functools.partial(
                     self._register_controller, test_config)
                 test_config.summary_writer = summary_writer
+                test_config.test_class_name_suffix = test_run_info.test_class_name_suffix
                 try:
-                    self._run_test_class(test_config, test_run_info.test_class,
-                                         test_run_info.tests)
+                    self._run_test_class(
+                        config=test_config,
+                        test_class=test_run_info.test_class,
+                        tests=test_run_info.tests)
                 except signals.TestAbortAll as e:
                     logging.warning(
                         'Abort all subsequent test classes. Reason: %s', e)
@@ -513,8 +528,8 @@
             logging.warning('No optional debug info found for controller %s. '
                             'To provide it, implement get_info in this '
                             'controller module.', module_config_name)
-        logging.debug('Found %d objects for controller %s',
-                      len(objects), module_config_name)
+        logging.debug('Found %d objects for controller %s', len(objects),
+                      module_config_name)
         destroy_func = module.destroy
         self._controller_destructors[module_ref_name] = destroy_func
         return objects
diff --git a/tests/mobly/test_runner_test.py b/tests/mobly/test_runner_test.py
index c0ff4b5..b369231 100755
--- a/tests/mobly/test_runner_test.py
+++ b/tests/mobly/test_runner_test.py
@@ -259,8 +259,13 @@
         self.assertEqual(results['Requested'], 2)
         self.assertEqual(results['Executed'], 2)
         self.assertEqual(results['Passed'], 2)
+        # Tag of the test class defaults to the class name.
+        record1 = tr.results.executed[0]
+        record2 = tr.results.executed[1]
+        self.assertEqual(record1.test_class, 'Integration2Test')
+        self.assertEqual(record2.test_class, 'IntegrationTest')
 
-    def test_run_two_test_classes_different_configs(self):
+    def test_run_two_test_classes_different_configs_and_aliases(self):
         """Verifies that running more than one test class in one test run with
         different configs works properly.
         """
@@ -272,8 +277,14 @@
         config2 = config1.copy()
         config2.user_params['icecream'] = 10
         tr = test_runner.TestRunner(self.log_dir, self.test_bed_name)
-        tr.add_test_class(config1, integration_test.IntegrationTest)
-        tr.add_test_class(config2, integration_test.IntegrationTest)
+        tr.add_test_class(
+            config1,
+            integration_test.IntegrationTest,
+            name_suffix='FirstConfig')
+        tr.add_test_class(
+            config2,
+            integration_test.IntegrationTest,
+            name_suffix='SecondConfig')
         tr.run()
         results = tr.results.summary_dict()
         self.assertEqual(results['Requested'], 2)
@@ -281,6 +292,10 @@
         self.assertEqual(results['Passed'], 1)
         self.assertEqual(results['Failed'], 1)
         self.assertEqual(tr.results.failed[0].details, '10 != 42')
+        record1 = tr.results.executed[0]
+        record2 = tr.results.executed[1]
+        self.assertEqual(record1.test_class, 'IntegrationTest_FirstConfig')
+        self.assertEqual(record2.test_class, 'IntegrationTest_SecondConfig')
 
     def test_run_with_abort_all(self):
         mock_test_config = self.base_mock_test_config.copy()