Index: modules/simpletest/simpletest.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.info,v
retrieving revision 1.4
diff -u -r1.4 simpletest.info
--- modules/simpletest/simpletest.info	11 Oct 2008 02:33:01 -0000	1.4
+++ modules/simpletest/simpletest.info	18 Feb 2009 08:10:53 -0000
@@ -5,4 +5,5 @@
 version = VERSION
 core = 7.x
 files[] = simpletest.module
+files[] = simpletest.test.inc
 files[] = simpletest.install
Index: modules/simpletest/simpletest.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.module,v
retrieving revision 1.36
diff -u -r1.36 simpletest.module
--- modules/simpletest/simpletest.module	13 Feb 2009 00:39:01 -0000	1.36
+++ modules/simpletest/simpletest.module	18 Feb 2009 08:10:53 -0000
@@ -2,6 +2,11 @@
 // $Id: simpletest.module,v 1.36 2009/02/13 00:39:01 webchick Exp $
 
 /**
+ * @file
+ * Provides testing framework module base.
+ */
+
+/**
  * Implementation of hook_help().
  */
 function simpletest_help($path, $arg) {
@@ -89,7 +94,7 @@
     $header = array(t('Message'), t('Group'), t('Filename'), t('Line'), t('Function'), array('colspan' => 2, 'data' => t('Status')));
     while ($result = db_fetch_object($results)) {
       $class = $result->test_class;
-      $info = $uncategorized_tests[$class]->getInfo();
+      $info = $uncategorized_tests[$class];
       $group = $info['group'];
       $selected_tests[$group][$class] = TRUE;
       if (!isset($group_summary[$group])) {
@@ -133,7 +138,7 @@
     foreach ($form['results'] as $group => &$elements) {
       $group_ok = TRUE;
       foreach ($elements as $class => &$element) {
-        $info = $uncategorized_tests[$class]->getInfo();
+        $info = $uncategorized_tests[$class];
         $ok = $element['summary']['#fail'] + $element['summary']['#exception'] == 0;
         $element += array(
           '#type' => 'fieldset',
@@ -169,9 +174,7 @@
     $form['tests']['table'][$group_name] = array(
       '#collapsed' => TRUE,
     );
-    foreach ($test_group as $test) {
-      $test_info = $test->getInfo();
-      $test_class = get_class($test);
+    foreach ($test_group as $test_class => $test_info) {
       $is_selected = isset($selected_tests[$group_name][$test_class]);
       $form['tests']['table'][$group_name][$test_class] = array(
         '#type' => 'checkbox',
@@ -324,21 +327,26 @@
  * Run selected tests.
  */
 function simpletest_test_form_submit($form, &$form_state) {
-  // Ensure that all classes are loaded before we create instances to get test information and run.
-  simpletest_get_all_tests();
-
   // Get list of tests.
-  $tests_list = array();
-  foreach ($form_state['values'] as $class_name => $value) {
-    if (class_exists($class_name) && $value === 1) {
-      $tests_list[] = $class_name;
+  $test_classes = array();
+  foreach ($form_state['values'] as $test_class => $value) {
+    if ($value === 1) {
+      // Load test file and ensure that test class exists.
+      simpletest_load_test($test_class);
+      if (class_exists($test_class)) {
+        $test_classes[] = $test_class;
+      }
+      else {
+        drupal_set_message(t('No such test class @class.', array('@class' => $test_class)), 'error');
+      }
     }
   }
-  if (count($tests_list) > 0 ) {
-    simpletest_run_tests($tests_list, 'drupal');
+
+  if ($test_classes) {
+    simpletest_run_tests($test_classes, 'drupal');
   }
   else {
-    drupal_set_message(t('No test(s) selected.'), 'error');
+    drupal_set_message(t('No test(s) to run.'), 'error');
   }
 }
 
@@ -353,13 +361,13 @@
  */
 function simpletest_run_tests($test_list, $reporter = 'drupal') {
   cache_clear_all();
+  $tests = simpletest_get_all_tests();
   $test_id = db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
 
   // Get the info for the first test being run.
   $first_test = array_shift($test_list);
-  $first_instance = new $first_test();
+  $info = $tests[$first_test];
   array_unshift($test_list, $first_test);
-  $info = $first_instance->getInfo();
 
   $batch = array(
     'title' => t('Running SimpleTests'),
@@ -386,9 +394,6 @@
  * Batch operation callback.
  */
 function _simpletest_batch_operation($test_list_init, $test_id, &$context) {
-  // Ensure that all classes are loaded before we unserialize some instances.
-  simpletest_get_all_tests();
-
   // Get working values.
   if (!isset($context['sandbox']['max'])) {
     // First iteration: initialize working values.
@@ -405,10 +410,15 @@
 
   // Perform the next test.
   $test_class = array_shift($test_list);
+
+  // Get test info and load test file.
+  $tests = simpletest_get_all_tests();
+  $info = $tests[$test_class];
+  simpletest_load_test($test_class);
+
   $test = new $test_class($test_id);
   $test->run();
   $size = count($test_list);
-  $info = $test->getInfo();
 
   // Gather results and compose the report.
   $test_results[$test_class] = $test->results;
@@ -450,46 +460,29 @@
  * Get a list of all of the tests.
  *
  * @return
- *   An array of tests, with the class name as the keys and the instantiated
- *   versions of the classes as the values.
+ *   An array of tests, with the test class name as the keys and the test
+ *   information as the value.
+ * @see hook_test()
  */
 function simpletest_get_all_tests() {
-  static $formatted_classes;
-  if (!isset($formatted_classes)) {
-    require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'simpletest') . '/drupal_web_test_case.php';
-    $files = array();
-    foreach (array_keys(module_rebuild_cache()) as $module) {
-      $module_path = drupal_get_path('module', $module);
-      $test = $module_path . "/$module.test";
-      if (file_exists($test)) {
-        $files[] = $test;
-      }
+  static $tests;
 
-      $tests_directory = $module_path . '/tests';
-      if (is_dir($tests_directory)) {
-        foreach (file_scan_directory($tests_directory, '/\.test$/') as $file) {
-          $files[] = $file->filename;
+  if (!isset($tests)) {
+    // Manually call each module so that we can know which module a given test
+    // came from.
+    $tests = array();
+    foreach (module_implements('test', TRUE) as $module) {
+      $test_classes = module_invoke($module, 'test');
+      if (isset($test_classes) && is_array($test_classes)) {
+        foreach (array_keys($test_classes) as $test_class) {
+          $test_classes[$test_class]['module'] = $module;
         }
+        $tests = array_merge($tests, $test_classes);
       }
     }
-
-    $existing_classes = get_declared_classes();
-    foreach ($files as $file) {
-      include_once DRUPAL_ROOT . '/' . $file;
-    }
-    $classes = array_values(array_diff(get_declared_classes(), $existing_classes));
-    $formatted_classes = array();
-    foreach ($classes as $key => $class) {
-      if (method_exists($class, 'getInfo')) {
-        $formatted_classes[$class] = new $class;
-      }
-    }
-  }
-  if (count($formatted_classes) == 0) {
-    drupal_set_message('No test cases found.', 'error');
-    return FALSE;
   }
-  return $formatted_classes;
+
+  return $tests;
 }
 
 /**
@@ -501,15 +494,29 @@
  */
 function simpletest_categorize_tests($tests) {
   $groups = array();
-  foreach ($tests as $test => $instance) {
-    $info = $instance->getInfo();
-    $groups[$info['group']][$test] = $instance;
+  foreach ($tests as $test_case => $info) {
+    $groups[$info['group']][$test_case] = $info;
   }
   uksort($groups, 'strnatcasecmp');
   return $groups;
 }
 
 /**
+ * Load test file of specified test class. Also includes the DrupalWebTestCase.
+ *
+ * @param $test_class
+ *   Test class to load file for.
+ */
+function simpletest_load_test($test_class) {
+  module_load_include('php', 'simpletest', 'drupal_web_test_case');
+  $tests = simpletest_get_all_tests();
+
+  $info = $tests[$test_class];
+  require_once drupal_get_path('module', $info['module']) . '/' .
+                 (isset($info['file']) ? 'tests/' . $info['file'] : $info['module']) . '.test';
+}
+
+/**
  * Remove all temporary database tables and directories.
  */
 function simpletest_clean_environment() {
Index: modules/simpletest/simpletest.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.test,v
retrieving revision 1.16
diff -u -r1.16 simpletest.test
--- modules/simpletest/simpletest.test	22 Jan 2009 12:46:06 -0000	1.16
+++ modules/simpletest/simpletest.test	18 Feb 2009 08:10:53 -0000
@@ -13,17 +13,6 @@
    */
   protected $test_ids = array();
 
-  function getInfo() {
-    return array(
-      'name' => t('SimpleTest functionality'),
-      'description' => t('Test SimpleTest\'s web interface: check that the intended tests were
-                          run and ensure that test reports display the intended results. Also
-                          test SimpleTest\'s internal browser and API\'s both explicitly and
-                          implicitly.'),
-      'group' => t('SimpleTest')
-    );
-  }
-
   function setUp() {
     if (!$this->inCURL()) {
       parent::setUp('simpletest');
@@ -231,7 +220,8 @@
    */
   function getResultFieldSet() {
     $fieldsets = $this->xpath('//fieldset');
-    $info = $this->getInfo();
+    $tests = simpletest_get_all_tests();
+    $info = $tests[get_class($this)];
     foreach ($fieldsets as $fieldset) {
       if ($fieldset->legend == $info['group']) {
         return $fieldset;
Index: scripts/run-tests.sh
===================================================================
RCS file: /cvs/drupal/drupal/scripts/run-tests.sh,v
retrieving revision 1.23
diff -u -r1.23 run-tests.sh
--- scripts/run-tests.sh	1 Feb 2009 16:42:26 -0000	1.23
+++ scripts/run-tests.sh	18 Feb 2009 08:10:53 -0000
@@ -52,11 +52,9 @@
   // Display all available tests.
   echo "\nAvailable test groups & classes\n";
   echo   "-------------------------------\n\n";
-  foreach ($groups as $group => $tests) {
-    echo $group . "\n";
-    foreach ($tests as $class_name => $instance) {
-      $info = $instance->getInfo();
-      echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n";
+  foreach ($groups as $group_name => $test_group) {
+    foreach ($test_group as $test_class => $test_info) {
+      echo " - " . $test_info['name'] . ' (' . $class_name . ')' . "\n";
     }
   }
   exit;
@@ -343,10 +341,12 @@
  * Run a single test (assume a Drupal bootstrapped environment).
  */
 function simpletest_script_run_one_test($test_id, $test_class) {
-  simpletest_get_all_tests();
+  simpletest_load_test($test_class);
+  $tests = simpletest_get_all_tests();
+  $info = $tests[$test_class];
+
   $test = new $test_class($test_id);
   $test->run();
-  $info = $test->getInfo();
 
   $status = ((isset($test->results['#fail']) && $test->results['#fail'] > 0)
            || (isset($test->results['#exception']) && $test->results['#exception'] > 0) ? 'fail' : 'pass');
@@ -443,7 +443,7 @@
   else {
     echo "Tests to be run:\n";
     foreach ($test_list as $class_name) {
-      $info = $all_tests[$class_name]->getInfo();
+      $info = $all_tests[$class_name];
       echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n";
     }
     echo "\n";
Index: modules/simpletest/simpletest.api.php
===================================================================
RCS file: modules/simpletest/simpletest.api.php
diff -N modules/simpletest/simpletest.api.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/simpletest.api.php	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,53 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Hooks provided by the SimpleTest module.
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Define tests provided.
+ *
+ * This hook allows modules to add tests to global list available for use. The
+ * hook is executed before listing tests and before running tests.
+ *
+ * @return
+ *   The test case class name related to the item should be specified in the
+ *   array key.
+ *   - "name": Required. The translated formal name of the test.
+ *   - "description": Required. The translated description of what the test does.
+ *   - "group": Required. The translated group name to categorize the test in.
+ *   - "file": The name of the file the test case is located in, excluding the
+ *     test extension. If specified the file should be located in a tests
+ *     folder in the module root, /path/to/modules/tests. Deaults to
+ *     MODULENAME.test.
+ */
+function hook_test() {
+  $tests = array();
+
+  $tests['ExampleTestCase'] = array(
+    'name' => t('Example'),
+    'description' => t('Tests example stuff.'),
+    'group' => t('Example'),
+  );
+
+  // File additional_example.test found in /path/to/module/tests directory.
+  $tests['AdditionalExampleTestCase'] = array(
+    'name' => t('Additional example'),
+    'description' => t('Tests additional example stuff.'),
+    'group' => t('Example'),
+    'file' => 'additional_example',
+  );
+
+  return $tests;
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */
Index: modules/simpletest/simpletest.test.inc
===================================================================
RCS file: modules/simpletest/simpletest.test.inc
diff -N modules/simpletest/simpletest.test.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/simpletest.test.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,25 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Defines all tests provided by SimpleTest module.
+ */
+
+/**
+ * Implementation of hook_test().
+ */
+function simpletest_test() {
+  $tests = array();
+
+  $tests['SimpleTestFunctionalTest'] = array(
+    'name' => t('SimpleTest functionality'),
+    'description' => t('Test SimpleTest\'s web interface: check that the intended tests were
+                        run and ensure that test reports display the intended results. Also
+                        test SimpleTest\'s internal browser and API\'s both explicitly and
+                        implicitly.'),
+    'group' => t('SimpleTest')
+  );
+
+  return $tests;
+}
