SimpletestResultsForm.php 10.1 KB
Newer Older
1 2 3 4 5
<?php

namespace Drupal\simpletest\Form;

use Drupal\Core\Database\Connection;
6
use Drupal\Core\Form\FormBase;
7 8
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
9
use Drupal\Core\Url;
10
use Drupal\simpletest\TestDiscovery;
11
use Symfony\Component\DependencyInjection\ContainerInterface;
12
use Symfony\Component\HttpFoundation\RedirectResponse;
13 14 15

/**
 * Test results form for $test_id.
16 17 18 19
 *
 * Note that the UI strings are not translated because this form is also used
 * from run-tests.sh.
 *
20 21
 * @internal
 *
22 23
 * @see simpletest_script_open_browser()
 * @see run-tests.sh
24
 */
25
class SimpletestResultsForm extends FormBase {
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

  /**
   * Associative array of themed result images keyed by status.
   *
   * @var array
   */
  protected $statusImageMap;

  /**
   * The database connection service.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
45
    return new static(
46
      $container->get('database')
47
    );
48 49 50 51 52 53 54 55
  }

  /**
   * Constructs a \Drupal\simpletest\Form\SimpletestResultsForm object.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection service.
   */
56
  public function __construct(Connection $database) {
57
    $this->database = $database;
58 59 60 61 62
  }

  /**
   * Builds the status image map.
   */
63
  protected static function buildStatusImageMap() {
64
    $image_pass = [
65
      '#theme' => 'image',
66
      '#uri' => 'core/misc/icons/73b355/check.svg',
67 68
      '#width' => 18,
      '#height' => 18,
69
      '#alt' => 'Pass',
70 71
    ];
    $image_fail = [
72
      '#theme' => 'image',
73
      '#uri' => 'core/misc/icons/e32700/error.svg',
74 75
      '#width' => 18,
      '#height' => 18,
76
      '#alt' => 'Fail',
77 78
    ];
    $image_exception = [
79
      '#theme' => 'image',
80
      '#uri' => 'core/misc/icons/e29700/warning.svg',
81 82
      '#width' => 18,
      '#height' => 18,
83
      '#alt' => 'Exception',
84 85
    ];
    $image_debug = [
86
      '#theme' => 'image',
87
      '#uri' => 'core/misc/icons/e29700/warning.svg',
88 89
      '#width' => 18,
      '#height' => 18,
90
      '#alt' => 'Debug',
91 92
    ];
    return [
93 94 95 96
      'pass' => $image_pass,
      'fail' => $image_fail,
      'exception' => $image_exception,
      'debug' => $image_debug,
97
    ];
98 99 100 101 102
  }

  /**
   * {@inheritdoc}
   */
103
  public function getFormId() {
104 105 106 107 108 109
    return 'simpletest_results_form';
  }

  /**
   * {@inheritdoc}
   */
110
  public function buildForm(array $form, FormStateInterface $form_state, $test_id = NULL) {
111 112
    // Make sure there are test results to display and a re-run is not being
    // performed.
113
    $results = [];
114
    if (is_numeric($test_id) && !$results = $this->getResults($test_id)) {
115
      $this->messenger()->addError($this->t('No test results to display.'));
116
      return new RedirectResponse($this->url('simpletest.test_form', [], ['absolute' => TRUE]));
117 118 119
    }

    // Load all classes and include CSS.
120
    $form['#attached']['library'][] = 'simpletest/drupal.simpletest';
121 122
    // Add the results form.
    $filter = static::addResultForm($form, $results, $this->getStringTranslation());
123 124

    // Actions.
125 126
    $form['#action'] = $this->url('simpletest.result_form', ['test_id' => 're-run']);
    $form['action'] = [
127
      '#type' => 'fieldset',
128
      '#title' => $this->t('Actions'),
129
      '#attributes' => ['class' => ['container-inline']],
130
      '#weight' => -11,
131
    ];
132

133
    $form['action']['filter'] = [
134 135
      '#type' => 'select',
      '#title' => 'Filter',
136 137 138 139 140 141
      '#options' => [
        'all' => $this->t('All (@count)', ['@count' => count($filter['pass']) + count($filter['fail'])]),
        'pass' => $this->t('Pass (@count)', ['@count' => count($filter['pass'])]),
        'fail' => $this->t('Fail (@count)', ['@count' => count($filter['fail'])]),
      ],
    ];
142 143 144
    $form['action']['filter']['#default_value'] = ($filter['fail'] ? 'fail' : 'all');

    // Categorized test classes for to be used with selected filter value.
145
    $form['action']['filter_pass'] = [
146 147
      '#type' => 'hidden',
      '#default_value' => implode(',', $filter['pass']),
148 149
    ];
    $form['action']['filter_fail'] = [
150 151
      '#type' => 'hidden',
      '#default_value' => implode(',', $filter['fail']),
152
    ];
153

154
    $form['action']['op'] = [
155
      '#type' => 'submit',
156
      '#value' => $this->t('Run tests'),
157
    ];
158

159
    $form['action']['return'] = [
160
      '#type' => 'link',
161
      '#title' => $this->t('Return to list'),
162
      '#url' => Url::fromRoute('simpletest.test_form'),
163
    ];
164 165 166 167 168 169 170 171 172 173 174

    if (is_numeric($test_id)) {
      simpletest_clean_results_table($test_id);
    }

    return $form;
  }

  /**
   * {@inheritdoc}
   */
175
  public function submitForm(array &$form, FormStateInterface $form_state) {
176 177
    $pass = $form_state->getValue('filter_pass') ? explode(',', $form_state->getValue('filter_pass')) : [];
    $fail = $form_state->getValue('filter_fail') ? explode(',', $form_state->getValue('filter_fail')) : [];
178

179
    if ($form_state->getValue('filter') == 'all') {
180 181
      $classes = array_merge($pass, $fail);
    }
182
    elseif ($form_state->getValue('filter') == 'pass') {
183 184 185 186 187 188 189
      $classes = $pass;
    }
    else {
      $classes = $fail;
    }

    if (!$classes) {
190
      $form_state->setRedirect('simpletest.test_form');
191 192 193
      return;
    }

194
    $form_execute = [];
195
    $form_state_execute = new FormState();
196
    foreach ($classes as $class) {
197
      $form_state_execute->setValue(['tests', $class], $class);
198 199
    }

200
    // Submit the simpletest test form to rerun the tests.
201 202
    // Under normal circumstances, a form object's submitForm() should never be
    // called directly, FormBuilder::submitForm() should be called instead.
203
    // However, it calls $form_state->setProgrammed(), which disables the Batch API.
204
    $simpletest_test_form = SimpletestTestForm::create(\Drupal::getContainer());
205 206
    $simpletest_test_form->buildForm($form_execute, $form_state_execute);
    $simpletest_test_form->submitForm($form_execute, $form_state_execute);
207 208 209
    if ($redirect = $form_state_execute->getRedirect()) {
      $form_state->setRedirectUrl($redirect);
    }
210 211 212 213 214 215 216 217 218
  }

  /**
   * Get test results for $test_id.
   *
   * @param int $test_id
   *   The test_id to retrieve results of.
   *
   * @return array
219
   *   Array of results grouped by test_class.
220 221
   */
  protected function getResults($test_id) {
222
    return $this->database->select('simpletest')
223 224 225 226
      ->fields('simpletest')
      ->condition('test_id', $test_id)
      ->orderBy('test_class')
      ->orderBy('message_id')
227 228 229
      ->execute()
      ->fetchAll();
  }
230

231 232 233 234 235 236 237 238 239
  /**
   * 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.
240
   * @param array $results
241 242 243 244 245 246 247 248 249 250 251
   *   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.
252
    $test_results = [];
253 254
    foreach ($results as $result) {
      if (!isset($test_results[$result->test_class])) {
255
        $test_results[$result->test_class] = [];
256 257 258 259
      }
      $test_results[$result->test_class][] = $result;
    }

260 261 262
    $image_status_map = static::buildStatusImageMap();

    // Keep track of which test cases passed or failed.
263 264 265 266
    $filter = [
      'pass' => [],
      'fail' => [],
    ];
267 268

    // Summary result widget.
269
    $form['result'] = [
270 271 272 273
      '#type' => 'fieldset',
      '#title' => 'Results',
      // Because this is used in a theme-less situation need to provide a
      // default.
274 275 276
      '#attributes' => [],
    ];
    $form['result']['summary'] = $summary = [
277 278 279 280 281
      '#theme' => 'simpletest_result_summary',
      '#pass' => 0,
      '#fail' => 0,
      '#exception' => 0,
      '#debug' => 0,
282
    ];
283 284 285 286

    \Drupal::service('test_discovery')->registerTestNamespaces();

    // Cycle through each test group.
287
    $header = [
288 289 290 291 292
      'Message',
      'Group',
      'Filename',
      'Line',
      'Function',
293 294 295
      ['colspan' => 2, 'data' => 'Status']
    ];
    $form['result']['results'] = [];
296 297 298
    foreach ($test_results as $group => $assertions) {
      // Create group details with summary information.
      $info = TestDiscovery::getTestInfo($group);
299
      $form['result']['results'][$group] = [
300 301 302 303
        '#type' => 'details',
        '#title' => $info['name'],
        '#open' => TRUE,
        '#description' => $info['description'],
304
      ];
305 306 307 308
      $form['result']['results'][$group]['summary'] = $summary;
      $group_summary =& $form['result']['results'][$group]['summary'];

      // Create table of assertions for the group.
309
      $rows = [];
310
      foreach ($assertions as $assertion) {
311
        $row = [];
312
        $row[] = ['data' => ['#markup' => $assertion->message]];
313 314 315 316
        $row[] = $assertion->message_group;
        $row[] = \Drupal::service('file_system')->basename(($assertion->file));
        $row[] = $assertion->line;
        $row[] = $assertion->function;
317
        $row[] = ['data' => $image_status_map[$assertion->status]];
318 319 320 321 322

        $class = 'simpletest-' . $assertion->status;
        if ($assertion->message_group == 'Debug') {
          $class = 'simpletest-debug';
        }
323
        $rows[] = ['data' => $row, 'class' => [$class]];
324 325 326 327

        $group_summary['#' . $assertion->status]++;
        $form['result']['summary']['#' . $assertion->status]++;
      }
328
      $form['result']['results'][$group]['table'] = [
329 330 331
        '#type' => 'table',
        '#header' => $header,
        '#rows' => $rows,
332
      ];
333 334 335 336 337 338 339 340 341 342 343 344 345

      // 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;
346 347 348
  }

}