run-tests.sh 16 KB
Newer Older
1
2
3
4
<?php
// $Id$
/**
 * @file
5
 * This script runs Drupal tests from command line.
6
7
 */

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
define('SIMPLETEST_SCRIPT_COLOR_PASS', 32); // Green.
define('SIMPLETEST_SCRIPT_COLOR_FAIL', 31); // Red.
define('SIMPLETEST_SCRIPT_COLOR_EXCEPTION', 33); // Brown.

// Set defaults and get overrides.
list($args, $count) = simpletest_script_parse_args();

simpletest_script_init();

if ($args['help'] || $count == 0) {
  simpletest_script_help();
  exit;
}

if ($args['execute-batch']) {
  simpletest_script_execute_batch();
}

26
// Bootstrap to perform initial validation or other operations.
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
if (!module_exists('simpletest')) {
  simpletest_script_print_error("The simpletest module must be enabled before this script can run.");
  exit;
}

if ($args['clean']) {
  // Clean up left-over times and directories.
  simpletest_clean_environment();
  echo "\nEnvironment cleaned.\n";

  // Get the status messages and print them.
  $messages = array_pop(drupal_get_messages('status'));
  foreach($messages as $text) {
    echo " - " . $text . "\n";
  }
  exit;
}

// Load SimpleTest files.
$all_tests = simpletest_get_all_tests();
$groups = simpletest_categorize_tests($all_tests);
$test_list = array();

if ($args['list']) {
52
  // Display all available tests.
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
  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";
    }
  }
  exit;
}

$test_list = simpletest_script_get_test_list();

// If not in 'safe mode', increase the maximum execution time.
if (!ini_get('safe_mode')) {
  set_time_limit(0);
}

simpletest_script_reporter_init();

// Setup database for test results.
75
$test_id = db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
76
77
78
79
80
81
82
83

// Execute tests.
simpletest_script_command($args['concurrency'], $test_id, implode(",", $test_list));

// Display results before database is cleared.
simpletest_script_reporter_display_results();

// Cleanup our test results.
84
85
86
db_delete("simpletest")
  ->condition('test_id', $test_id)
  ->execute();
87
88
89
90
91
92
93


/**
 * Print help text.
 */
function simpletest_script_help() {
  global $args;
94
95
96
97
98

  echo <<<EOF

Run Drupal tests from the shell.

99
100
Usage:        {$args['script']} [OPTIONS] <tests>
Example:      {$args['script']} Profile
101
102
103
104

All arguments are long options.

  --help      Print this page.
105
106
107

  --list      Display all available test groups.

108
109
110
  --clean     Cleans up database tables or directories from previous, failed,
              tests and then exits (no tests are run).

111
112
  --url       Immediately preceeds a URL to set the host and path. You will
              need this parameter if Drupal is in a subdirectory on your
113
              localhost and you have not set \$base_url in settings.php.
114

115
  --php       The absolute path to the PHP executable. Usually not needed.
116

117
118
  --concurrency [num]

119
              Run tests in parallel, up to [num] tests at a time. This requires
120
              the Process Control Extension (PCNTL) to be compiled in PHP, not
121
              supported under Windows.
122

123
  --all       Run all available tests.
124

125
  --class     Run tests identified by specific class names, instead of group names.
126

127
  --file      Run tests identified by specific file names, instead of group names.
128
              Specify the path and the extension (i.e. 'modules/user/user.test').
129

130
  --color     Output the results with color highlighting.
131
132
133

  --verbose   Output detailed assertion messages in addition to summary.

134
135
  <test1>[,<test2>[,<test3> ...]]

136
              One or more tests to be run. By default, these are interpreted
137
138
139
140
              as the names of test groups as shown at ?q=admin/build/testing.
              These group names typically correspond to module names like "User"
              or "Profile" or "System", but there is also a group "XML-RPC".
              If --class is specified then these are interpreted as the names of
141
142
              specific test classes whose test methods will be run. Tests must
              be separated by commas. Ignored if --all is specified.
143

144
To run this script you will normally invoke it from the root directory of your
145
Drupal installation as the webserver user (differs per configuration), or root:
146

147
148
149
150
sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']}
  --url http://example.com/ --all
sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']}
  --url http://example.com/ --class UploadTestCase
151
152
153
154
\n
EOF;
}

155
156
157
158
159
160
161
162
163
164
165
166
167
/**
 * Parse execution argument and ensure that all are valid.
 *
 * @return The list of arguments.
 */
function simpletest_script_parse_args() {
  // Set default values.
  $args = array(
    'script' => '',
    'help' => FALSE,
    'list' => FALSE,
    'clean' => FALSE,
    'url' => '',
168
    'php' => '',
169
170
171
    'concurrency' => 1,
    'all' => FALSE,
    'class' => FALSE,
172
    'file' => FALSE,
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
    'color' => FALSE,
    'verbose' => FALSE,
    'test_names' => array(),
    // Used internally.
    'test-id' => NULL,
    'execute-batch' => FALSE
  );

  // Override with set values.
  $args['script'] = basename(array_shift($_SERVER['argv']));

  $count = 0;
  while ($arg = array_shift($_SERVER['argv'])) {
    if (preg_match('/--(\S+)/', $arg, $matches)) {
      // Argument found.
      if (array_key_exists($matches[1], $args)) {
        // Argument found in list.
        $previous_arg = $matches[1];
        if (is_bool($args[$previous_arg])) {
          $args[$matches[1]] = TRUE;
        }
        else {
          $args[$matches[1]] = array_shift($_SERVER['argv']);
        }
197
        // Clear extraneous values.
198
199
200
201
202
203
204
205
206
207
208
209
        $args['test_names'] = array();
        $count++;
      }
      else {
        // Argument not found in list.
        simpletest_script_print_error("Unknown argument '$arg'.");
        exit;
      }
    }
    else {
      // Values found without an argument should be test names.
      $args['test_names'] += explode(',', $arg);
210
      $count++;
211
    }
212
  }
213
214
215
216
217
218

  // Validate the concurrency argument
  if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) {
    simpletest_script_print_error("--concurrency must be a strictly positive integer.");
    exit;
  }
219
  elseif ($args['concurrency'] > 1 && !function_exists('pcntl_fork')) {
220
221
222
223
    simpletest_script_print_error("Parallel test execution requires the Process Control extension to be compiled in PHP. Please see http://php.net/manual/en/intro.pcntl.php for more information.");
    exit;
  }

224
  return array($args, $count);
225
226
}

227
228
229
230
231
232
233
234
/**
 * Initialize script variables and perform general setup requirements.
 */
function simpletest_script_init() {
  global $args, $php;

  $host = 'localhost';
  $path = '';
235
  // Determine location of php command automatically, unless a command line argument is supplied.
236
  if (!empty($args['php'])) {
237
238
    $php = $args['php'];
  }
239
  elseif (!empty($_ENV['_'])) {
240
241
242
    // '_' is an environment variable set by the shell. It contains the command that was executed.
    $php = $_ENV['_'];
  }
243
  elseif (!empty($_ENV['SUDO_COMMAND'])) {
244
245
246
247
248
249
    // 'SUDO_COMMAND' is an environment variable set by the sudo program.
    // Extract only the PHP interpreter, not the rest of the command.
    list($php, ) = explode(' ', $_ENV['SUDO_COMMAND'], 2);
  }
  else {
    simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Please supply the --php command line argument.');
250
    simpletest_script_help();
251
252
    exit();
  }
253
254
255
256
257
258
259
260
261
262
263
264
265
266

  // Get url from arguments.
  if (!empty($args['url'])) {
    $parsed_url = parse_url($args['url']);
    $host = $parsed_url['host'];
    $path = $parsed_url['path'];
  }

  $_SERVER['HTTP_HOST'] = $host;
  $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
  $_SERVER['SERVER_ADDR'] = '127.0.0.1';
  $_SERVER['SERVER_SOFTWARE'] = 'Apache';
  $_SERVER['SERVER_NAME'] = 'localhost';
  $_SERVER['REQUEST_URI'] = $path .'/';
267
  $_SERVER['REQUEST_METHOD'] = 'GET';
268
269
270
271
272
  $_SERVER['SCRIPT_NAME'] = $path .'/index.php';
  $_SERVER['PHP_SELF'] = $path .'/index.php';
  $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';

  chdir(realpath(dirname(__FILE__) . '/..'));
273
274
  define('DRUPAL_ROOT', getcwd());
  require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
275
276
277
278
279
280
281
282
283
284
}

/**
 * Execute a batch of tests.
 */
function simpletest_script_execute_batch() {
  global $args;

  if (is_null($args['test-id'])) {
    simpletest_script_print_error("--execute-batch should not be called interactively.");
285
286
    exit;
  }
287
  if ($args['concurrency'] == 1) {
288
289
290
291
292
    // Fallback to mono-threaded execution.
    if (count($args['test_names']) > 1) {
      foreach ($args['test_names'] as $test_class) {
        // Execute each test in its separate Drupal environment.
        simpletest_script_command(1, $args['test-id'], $test_class);
293
294
295
296
      }
      exit;
    }
    else {
297
298
      // Execute an individual test.
      $test_class = array_shift($args['test_names']);
299
      drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
300
      simpletest_script_run_one_test($args['test-id'], $test_class);
301
302
303
304
      exit;
    }
  }
  else {
305
    // Multi-threaded execution.
306
    $children = array();
307
308
    while (!empty($args['test_names']) || !empty($children)) {
      // Fork children safely since Drupal is not bootstrapped yet.
309
      while (count($children) < $args['concurrency']) {
310
        if (empty($args['test_names'])) break;
311
312

        $child = array();
313
        $child['test_class'] = $test_class = array_shift($args['test_names']);
314
315
        $child['pid'] = pcntl_fork();
        if (!$child['pid']) {
316
          // This is the child process, bootstrap and execute the test.
317
          drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
318
          simpletest_script_run_one_test($args['test-id'], $test_class);
319
320
321
          exit;
        }
        else {
322
          // Register our new child.
323
324
325
326
          $children[] = $child;
        }
      }

327
      // Wait for children every 200ms.
328
329
      usleep(200000);

330
331
      // Check if some children finished.
      foreach ($children as $cid => $child) {
332
        if (pcntl_waitpid($child['pid'], $status, WUNTRACED | WNOHANG)) {
333
          // This particular child exited.
334
335
336
337
338
339
340
341
          unset($children[$cid]);
        }
      }
    }
    exit;
  }
}

342
/**
343
 * Run a single test (assume a Drupal bootstrapped environment).
344
345
346
347
348
349
 */
function simpletest_script_run_one_test($test_id, $test_class) {
  simpletest_get_all_tests();
  $test = new $test_class($test_id);
  $test->run();
  $info = $test->getInfo();
350

351
352
353
  $status = ((isset($test->results['#fail']) && $test->results['#fail'] > 0)
           || (isset($test->results['#exception']) && $test->results['#exception'] > 0) ? 'fail' : 'pass');
  simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->results) . "\n", simpletest_script_color_code($status));
354
355
}

356
357
358
359
360
/**
 * Execute a command to run batch of tests in separate process.
 */
function simpletest_script_command($concurrency, $test_id, $tests) {
  global $args, $php;
361

362
363
364
  $command = "$php ./scripts/{$args['script']} --url {$args['url']}";
  if ($args['color']) {
    $command .= ' --color';
365
  }
366
  $command .= " --php " . escapeshellarg($php) . " --concurrency $concurrency --test-id $test_id --execute-batch $tests";
367
  passthru($command);
368
369
}

370
/**
371
 * Get list of tests based on arguments. If --all specified then
372
373
374
375
376
377
378
379
380
381
382
383
 * returns all available tests, otherwise reads list of tests.
 *
 * Will print error and exit if no valid tests were found.
 *
 * @return List of tests.
 */
function simpletest_script_get_test_list() {
  global $args, $all_tests, $groups;

  $test_list = array();
  if ($args['all']) {
    $test_list = array_keys($all_tests);
384
385
  }
  else {
386
    if ($args['class']) {
387
388
389
      // Check for valid class names.
      foreach ($args['test_names'] as $class_name) {
        if (isset($all_tests[$class_name])) {
390
391
392
393
          $test_list[] = $class_name;
        }
      }
    }
394
    elseif ($args['file']) {
395
396
397
398
      $files = array();
      foreach ($args['test_names'] as $file) {
        $files[realpath($file)] = 1;
      }
399

400
401
402
403
404
405
406
407
408
      // Check for valid class names.
      foreach ($all_tests as $class_name => $instance) {
        $refclass = new ReflectionClass($class_name);
        $file = $refclass->getFileName();
        if (isset($files[$file])) {
          $test_list[] = $class_name;
        }
      }
    }
409
410
411
412
413
414
415
416
417
418
    else {
      // Check for valid group names and get all valid classes in group.
      foreach ($args['test_names'] as $group_name) {
        if (isset($groups[$group_name])) {
          foreach($groups[$group_name] as $class_name => $instance) {
            $test_list[] = $class_name;
          }
        }
      }
    }
419
  }
420

421
422
423
424
425
  if (empty($test_list)) {
    simpletest_script_print_error('No valid tests were specified.');
    exit;
  }
  return $test_list;
426
427
}

428
429
430
431
432
/**
 * Initialize the reporter.
 */
function simpletest_script_reporter_init() {
  global $args, $all_tests, $test_list;
433

434
435
436
437
  echo "\n";
  echo "Drupal test run\n";
  echo "---------------\n";
  echo "\n";
438

439
440
441
442
443
444
445
446
447
448
449
  // Tell the user about what tests are to be run.
  if ($args['all']) {
    echo "All tests will run.\n\n";
  }
  else {
    echo "Tests to be run:\n";
    foreach ($test_list as $class_name) {
      $info = $all_tests[$class_name]->getInfo();
      echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n";
    }
    echo "\n";
450
  }
451

452
  echo "Test run started: " . format_date($_SERVER['REQUEST_TIME'], 'long') . "\n";
453
  timer_start('run-tests');
454
455
456
457
  echo "\n";

  echo "Test summary:\n";
  echo "-------------\n";
458
  echo "\n";
459
460
}

461
462
463
464
465
/**
 * Display test results.
 */
function simpletest_script_reporter_display_results() {
  global $args, $test_id, $results_map;
466

467
  echo "\n";
468
469
  $end = timer_stop('run-tests');
  echo "Test run duration: " . format_interval($end['time'] / 1000);
470
  echo "\n";
471

472
473
474
475
476
477
478
479
480
481
  if ($args['verbose']) {
    // Report results.
    echo "Detailed test results:\n";
    echo "----------------------\n";
    echo "\n";

    $results_map = array(
      'pass' => 'Pass',
      'fail' => 'Fail',
      'exception' => 'Exception'
482
    );
483

484
    $results = db_query("SELECT * FROM {simpletest} WHERE test_id = :test_id ORDER BY test_class, message_id", array(':test_id' => $test_id));
485
    $test_class = '';
486
    foreach ($results as $result) {
487
488
489
490
491
492
493
494
495
496
      if (isset($results_map[$result->status])) {
        if ($result->test_class != $test_class) {
          // Display test class every time results are for new test class.
          echo "\n\n---- $result->test_class ----\n\n\n";
          $test_class = $result->test_class;
        }

        simpletest_script_format_result($result);
      }
    }
497
498
499
  }
}

500
501
502
503
504
505
506
507
508
509
510
511
512
/**
 * Format the result so that it fits within the default 80 character
 * terminal size.
 *
 * @param $result The result object to format.
 */
function simpletest_script_format_result($result) {
  global $results_map, $color;

  $summary = sprintf("%-10.10s %-10.10s %-30.30s %-5.5s %-20.20s\n",
    $results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->caller);

  simpletest_script_print($summary, simpletest_script_color_code($result->status));
513

514
515
516
517
518
  $lines = explode("\n", wordwrap(trim(strip_tags($result->message)), 76));
  foreach ($lines as $line) {
    echo "    $line\n";
  }
}
519

520
/**
521
522
523
524
 * Print error message prefixed with "  ERROR: " and displayed in fail color
 * if color output is enabled.
 *
 * @param $message The message to print.
525
 */
526
527
function simpletest_script_print_error($message) {
  simpletest_script_print("  ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
528
}
529

530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
/**
 * Print a message to the console, if color is enabled then the specified
 * color code will be used.
 *
 * @param $message The message to print.
 * @param $color_code The color code to use for coloring.
 */
function simpletest_script_print($message, $color_code) {
  global $args;
  if ($args['color']) {
    echo "\033[" . $color_code . "m" . $message . "\033[0m";
  }
  else {
    echo $message;
  }
}

/**
 * Get the color code associated with the specified status.
 *
 * @param $status The status string to get code for.
 * @return Color code.
 */
function simpletest_script_color_code($status) {
  switch ($status) {
    case 'pass':
      return SIMPLETEST_SCRIPT_COLOR_PASS;
    case 'fail':
      return SIMPLETEST_SCRIPT_COLOR_FAIL;
    case 'exception':
      return SIMPLETEST_SCRIPT_COLOR_EXCEPTION;
  }
  return 0; // Default formatting.
}