diff --git a/core/modules/simpletest/src/Form/SimpletestResultsForm.php b/core/modules/simpletest/src/Form/SimpletestResultsForm.php index c8366fb6a5156620560f846f02bc9820201d5aee..9ddc4516f6985a6ed098bfb96b2e1c376f037d51 100644 --- a/core/modules/simpletest/src/Form/SimpletestResultsForm.php +++ b/core/modules/simpletest/src/Form/SimpletestResultsForm.php @@ -19,6 +19,12 @@ /** * Test results form for $test_id. + * + * Note that the UI strings are not translated because this form is also used + * from run-tests.sh. + * + * @see simpletest_script_open_browser() + * @see run-tests.sh */ class SimpletestResultsForm extends FormBase { @@ -58,37 +64,36 @@ public function __construct(Connection $database) { /** * Builds the status image map. */ - protected function buildStatusImageMap() { - // Initialize image mapping property. + protected static function buildStatusImageMap() { $image_pass = array( '#theme' => 'image', '#uri' => 'core/misc/icons/73b355/check.svg', '#width' => 18, '#height' => 18, - '#alt' => $this->t('Pass'), + '#alt' => 'Pass', ); $image_fail = array( '#theme' => 'image', '#uri' => 'core/misc/icons/ea2800/error.svg', '#width' => 18, '#height' => 18, - '#alt' => $this->t('Fail'), + '#alt' => 'Fail', ); $image_exception = array( '#theme' => 'image', '#uri' => 'core/misc/icons/e29700/warning.svg', '#width' => 18, '#height' => 18, - '#alt' => $this->t('Exception'), + '#alt' => 'Exception', ); $image_debug = array( '#theme' => 'image', '#uri' => 'core/misc/icons/e29700/warning.svg', '#width' => 18, '#height' => 18, - '#alt' => $this->t('Debug'), + '#alt' => 'Debug', ); - $this->statusImageMap = array( + return array( 'pass' => drupal_render($image_pass), 'fail' => drupal_render($image_fail), 'exception' => drupal_render($image_exception), @@ -107,11 +112,9 @@ public function getFormId() { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state, $test_id = NULL) { - $this->buildStatusImageMap(); // Make sure there are test results to display and a re-run is not being // performed. $results = array(); - if (is_numeric($test_id) && !$results = $this->getResults($test_id)) { drupal_set_message($this->t('No test results to display.'), 'error'); return new RedirectResponse($this->url('simpletest.test_form', array(), array('absolute' => TRUE))); @@ -119,90 +122,11 @@ public function buildForm(array $form, FormStateInterface $form_state, $test_id // Load all classes and include CSS. $form['#attached']['library'][] = 'simpletest/drupal.simpletest'; - - // Keep track of which test cases passed or failed. - $filter = array( - 'pass' => array(), - 'fail' => array(), - ); - - // Summary result widget. - $form['result'] = array( - '#type' => 'fieldset', - '#title' => $this->t('Results'), - ); - $form['result']['summary'] = $summary = array( - '#theme' => 'simpletest_result_summary', - '#pass' => 0, - '#fail' => 0, - '#exception' => 0, - '#debug' => 0, - ); - - simpletest_classloader_register(); - - // Cycle through each test group. - $header = array( - $this->t('Message'), - $this->t('Group'), - $this->t('Filename'), - $this->t('Line'), - $this->t('Function'), - array('colspan' => 2, 'data' => $this->t('Status')) - ); - $form['result']['results'] = array(); - foreach ($results as $group => $assertions) { - // Create group details with summary information. - $info = TestDiscovery::getTestInfo($group); - $form['result']['results'][$group] = array( - '#type' => 'details', - '#title' => $info['name'], - '#open' => TRUE, - '#description' => $info['description'], - ); - $form['result']['results'][$group]['summary'] = $summary; - $group_summary =& $form['result']['results'][$group]['summary']; - - // Create table of assertions for the group. - $rows = array(); - foreach ($assertions as $assertion) { - $row = array(); - // Assertion messages are in code, so we assume they are safe. - $row[] = SafeMarkup::set($assertion->message); - $row[] = $assertion->message_group; - $row[] = drupal_basename($assertion->file); - $row[] = $assertion->line; - $row[] = $assertion->function; - $row[] = $this->statusImageMap[$assertion->status]; - - $class = 'simpletest-' . $assertion->status; - if ($assertion->message_group == 'Debug') { - $class = 'simpletest-debug'; - } - $rows[] = array('data' => $row, 'class' => array($class)); - - $group_summary['#' . $assertion->status]++; - $form['result']['summary']['#' . $assertion->status]++; - } - $form['result']['results'][$group]['table'] = array( - '#type' => 'table', - '#header' => $header, - '#rows' => $rows, - ); - - // Set summary information. - $group_summary['#ok'] = $group_summary['#fail'] + $group_summary['#exception'] == 0; - $form['result']['results'][$group]['#open'] = !$group_summary['#ok']; - - // Store test group (class) as for use in filter. - $filter[$group_summary['#ok'] ? 'pass' : 'fail'][] = $group; - } - - // Overall summary status. - $form['result']['summary']['#ok'] = $form['result']['summary']['#fail'] + $form['result']['summary']['#exception'] == 0; + // Add the results form. + $filter = static::addResultForm($form, $results, $this->getStringTranslation()); // Actions. - $form['#action'] = \Drupal::url('simpletest.result_form', array('test_id' => 're-run')); + $form['#action'] = $this->url('simpletest.result_form', array('test_id' => 're-run')); $form['action'] = array( '#type' => 'fieldset', '#title' => $this->t('Actions'), @@ -299,13 +223,36 @@ public function submitForm(array &$form, FormStateInterface $form_state) { * Array of results grouped by test_class. */ protected function getResults($test_id) { - $results = $this->database->select('simpletest') + return $this->database->select('simpletest') ->fields('simpletest') ->condition('test_id', $test_id) ->orderBy('test_class') ->orderBy('message_id') - ->execute(); + ->execute() + ->fetchAll(); + } + /** + * Adds the result form to a $form. + * + * This is a static method so that run-tests.sh can use it to generate a + * results page completely external to Drupal. This is why the UI strings are + * not wrapped in t(). + * + * @param array $form + * The form to attach the results to. + * @param array $test_results + * The simpletest results. + * + * @return array + * A list of tests the passed and failed. The array has two keys, 'pass' and + * 'fail'. Each contains a list of test classes. + * + * @see simpletest_script_open_browser() + * @see run-tests.sh + */ + public static function addResultForm(array &$form, array $results) { + // Transform the test results to be grouped by test class. $test_results = array(); foreach ($results as $result) { if (!isset($test_results[$result->test_class])) { @@ -314,7 +261,93 @@ protected function getResults($test_id) { $test_results[$result->test_class][] = $result; } - return $test_results; + $image_status_map = static::buildStatusImageMap(); + + // Keep track of which test cases passed or failed. + $filter = array( + 'pass' => array(), + 'fail' => array(), + ); + + // Summary result widget. + $form['result'] = array( + '#type' => 'fieldset', + '#title' => 'Results', + // Because this is used in a theme-less situation need to provide a + // default. + '#attributes' => array(), + ); + $form['result']['summary'] = $summary = array( + '#theme' => 'simpletest_result_summary', + '#pass' => 0, + '#fail' => 0, + '#exception' => 0, + '#debug' => 0, + ); + + \Drupal::service('test_discovery')->registerTestNamespaces(); + + // Cycle through each test group. + $header = array( + 'Message', + 'Group', + 'Filename', + 'Line', + 'Function', + array('colspan' => 2, 'data' => 'Status') + ); + $form['result']['results'] = array(); + foreach ($test_results as $group => $assertions) { + // Create group details with summary information. + $info = TestDiscovery::getTestInfo($group); + $form['result']['results'][$group] = array( + '#type' => 'details', + '#title' => $info['name'], + '#open' => TRUE, + '#description' => $info['description'], + ); + $form['result']['results'][$group]['summary'] = $summary; + $group_summary =& $form['result']['results'][$group]['summary']; + + // Create table of assertions for the group. + $rows = array(); + foreach ($assertions as $assertion) { + $row = array(); + // Assertion messages are in code, so we assume they are safe. + $row[] = SafeMarkup::set($assertion->message); + $row[] = $assertion->message_group; + $row[] = \Drupal::service('file_system')->basename(($assertion->file)); + $row[] = $assertion->line; + $row[] = $assertion->function; + $row[] = $image_status_map[$assertion->status]; + + $class = 'simpletest-' . $assertion->status; + if ($assertion->message_group == 'Debug') { + $class = 'simpletest-debug'; + } + $rows[] = array('data' => $row, 'class' => array($class)); + + $group_summary['#' . $assertion->status]++; + $form['result']['summary']['#' . $assertion->status]++; + } + $form['result']['results'][$group]['table'] = array( + '#type' => 'table', + '#header' => $header, + '#rows' => $rows, + ); + + // Set summary information. + $group_summary['#ok'] = $group_summary['#fail'] + $group_summary['#exception'] == 0; + $form['result']['results'][$group]['#open'] = !$group_summary['#ok']; + + // Store test group (class) as for use in filter. + $filter[$group_summary['#ok'] ? 'pass' : 'fail'][] = $group; + } + + // Overall summary status. + $form['result']['summary']['#ok'] = $form['result']['summary']['#fail'] + $form['result']['summary']['#exception'] == 0; + + return $filter; } } diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php index abf09fd05d87ae6e4d5d45f2232b36a04fed1096..497f0855b221fd402aaf29d38bddcf13a0d4b433 100644 --- a/core/modules/simpletest/src/TestBase.php +++ b/core/modules/simpletest/src/TestBase.php @@ -873,10 +873,10 @@ protected function verbose($message) { return; } - $message = '<hr />ID #' . $this->verboseId . ' (<a href="' . $this->verboseClassName . '-' . ($this->verboseId - 1) . '.html">Previous</a> | <a href="' . $this->verboseClassName . '-' . ($this->verboseId + 1) . '.html">Next</a>)<hr />' . $message; - $verbose_filename = $this->verboseDirectory . '/' . $this->verboseClassName . '-' . $this->verboseId . '.html'; - if (file_put_contents($verbose_filename, $message, FILE_APPEND)) { - $url = $this->verboseDirectoryUrl . '/' . $this->verboseClassName . '-' . $this->verboseId . '.html'; + $message = '<hr />ID #' . $this->verboseId . ' (<a href="' . $this->verboseClassName . '-' . ($this->verboseId - 1) . '-' . $this->testId . '.html">Previous</a> | <a href="' . $this->verboseClassName . '-' . ($this->verboseId + 1) . '-' . $this->testId . '.html">Next</a>)<hr />' . $message; + $verbose_filename = $this->verboseClassName . '-' . $this->verboseId . '-' . $this->testId . '.html'; + if (file_put_contents($this->verboseDirectory . '/' . $verbose_filename, $message)) { + $url = $this->verboseDirectoryUrl . '/' . $verbose_filename; // Not using _l() to avoid invoking the theme system, so that unit tests // can use verbose() as well. $url = '<a href="' . $url . '" target="_blank">Verbose message</a>'; diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 7bf821c52c09a56b4c8ae182790ecd9e473e3a84..2c13e752d1d9ef694399e7461abd2d6cbc056cf2 100644 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -6,9 +6,13 @@ */ use Drupal\Component\Utility\Timer; +use Drupal\Component\Uuid\Php; use Drupal\Core\Database\Database; +use Drupal\Core\Form\FormState; use Drupal\Core\Site\Settings; +use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\Test\TestRunnerKernel; +use Drupal\simpletest\Form\SimpletestResultsForm; use Symfony\Component\HttpFoundation\Request; $autoloader = require_once __DIR__ . '/../vendor/autoload.php'; @@ -88,7 +92,12 @@ simpletest_script_reporter_timer_stop(); // Display results before database is cleared. -simpletest_script_reporter_display_results(); +if ($args['browser']) { + simpletest_script_open_browser(); +} +else { + simpletest_script_reporter_display_results(); +} if ($args['xml']) { simpletest_script_reporter_write_xml_results(); @@ -188,6 +197,10 @@ function simpletest_script_help() { test database and configuration directories. Use in combination with --repeat for debugging random test failures. + --browser Opens the results in the browser. This enforces --keep-results and + if you want to also view any pages rendered in the simpletest + browser you need to add --verbose to the command line. + <test1>[,<test2>[,<test3> ...]] One or more tests to be run. By default, these are interpreted @@ -246,6 +259,7 @@ function simpletest_script_parse_args() { 'test_names' => array(), 'repeat' => 1, 'die-on-fail' => FALSE, + 'browser' => FALSE, // Used internally. 'test-id' => 0, 'execute-test' => '', @@ -291,6 +305,9 @@ function simpletest_script_parse_args() { exit; } + if ($args['browser']) { + $args['keep-results'] = TRUE; + } return array($args, $count); } @@ -1162,3 +1179,65 @@ function simpletest_script_load_messages_by_test_id($test_ids) { return $results; } + +/** + * Display test results. + */ +function simpletest_script_open_browser() { + global $test_ids; + + $connection = Database::getConnection('default', 'test-runner'); + $results = $connection->select('simpletest') + ->fields('simpletest') + ->condition('test_id', $test_ids, 'IN') + ->orderBy('test_class') + ->orderBy('message_id') + ->execute() + ->fetchAll(); + + // Get the results form. + $form = array(); + SimpletestResultsForm::addResultForm($form, $results); + + // Get the assets to make the details element collapsible and theme the result + // form. + $assets = new \Drupal\Core\Asset\AttachedAssets(); + $assets->setLibraries(['core/drupal.collapse', 'system/admin', 'simpletest/drupal.simpletest']); + $resolver = \Drupal::service('asset.resolver'); + list($js_assets_header, $js_assets_footer) = $resolver->getJsAssets($assets, FALSE); + $js_collection_renderer = \Drupal::service('asset.js.collection_renderer'); + $js_assets_header = $js_collection_renderer->render($js_assets_header); + $js_assets_footer = $js_collection_renderer->render($js_assets_footer); + $css_assets = \Drupal::service('asset.css.collection_renderer')->render($resolver->getCssAssets($assets, FALSE)); + + // Make the html page to write to disk. + $html = '<head>' . drupal_render($js_assets_header) . drupal_render($css_assets) . '</head><body>' . drupal_render($form) . drupal_render($js_assets_footer) .'</body>'; + + // Ensure we have assets verbose directory - tests with no verbose output will not + // have created one. + $directory = PublicStream::basePath() . '/simpletest/verbose'; + file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + $uuid = new Php(); + $filename = $directory .'/results-'. $uuid->generate() .'.html'; + file_put_contents($filename, $html); + + // See if we can find an OS helper to open URLs in default browser. + $browser = FALSE; + if (shell_exec('which xdg-open')) { + $browser = 'xdg-open'; + } + elseif (shell_exec('which open')) { + $browser = 'open'; + } + elseif (substr(PHP_OS, 0, 3) == 'WIN') { + $browser = 'start'; + } + + if ($browser) { + shell_exec($browser . ' ' . escapeshellarg($filename)); + } + else { + // Can't find assets valid browser. + print 'Open file://' . realpath($filename) . ' in your browser to see the verbose output.'; + } +}