~ubuntu-branches/ubuntu/wily/phabricator/wily

« back to all changes in this revision

Viewing changes to src/unit/engine/phutil/ArcanistPhutilTestCase.php

  • Committer: Package Import Robot
  • Author(s): Richard Sellam
  • Date: 2014-11-01 23:20:06 UTC
  • mto: This revision was merged to the branch mainline in revision 4.
  • Revision ID: package-import@ubuntu.com-20141101232006-mvlnp0cil67tsboe
Tags: upstream-0~git20141101/arcanist
Import upstream version 0~git20141101, component arcanist

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
 
 
3
/**
 
4
 * Base test case for the very simple libphutil test framework.
 
5
 *
 
6
 * @task assert       Making Test Assertions
 
7
 * @task exceptions   Exception Handling
 
8
 * @task hook         Hooks for Setup and Teardown
 
9
 * @task internal     Internals
 
10
 */
 
11
abstract class ArcanistPhutilTestCase {
 
12
 
 
13
  private $assertions = 0;
 
14
  private $runningTest;
 
15
  private $testStartTime;
 
16
  private $results = array();
 
17
  private $enableCoverage;
 
18
  private $coverage = array();
 
19
  private $projectRoot;
 
20
  private $paths;
 
21
  private $renderer;
 
22
 
 
23
 
 
24
/* -(  Making Test Assertions  )--------------------------------------------- */
 
25
 
 
26
 
 
27
  /**
 
28
   * Assert that a value is `false`, strictly. The test fails if it is not.
 
29
   *
 
30
   * @param wild    The empirically derived value, generated by executing the
 
31
   *                test.
 
32
   * @param string  A human-readable description of what these values represent,
 
33
   *                and particularly of what a discrepancy means.
 
34
   *
 
35
   * @return void
 
36
   * @task assert
 
37
   */
 
38
  final protected function assertFalse($result, $message = null) {
 
39
    if ($result === false) {
 
40
      $this->assertions++;
 
41
      return;
 
42
    }
 
43
 
 
44
    $this->failAssertionWithExpectedValue('false', $result, $message);
 
45
  }
 
46
 
 
47
 
 
48
  /**
 
49
   * Assert that a value is `true`, strictly. The test fails if it is not.
 
50
   *
 
51
   * @param wild    The empirically derived value, generated by executing the
 
52
   *                test.
 
53
   * @param string  A human-readable description of what these values represent,
 
54
   *                and particularly of what a discrepancy means.
 
55
   *
 
56
   * @return void
 
57
   * @task assert
 
58
   */
 
59
  final protected function assertTrue($result, $message = null) {
 
60
    if ($result === true) {
 
61
      $this->assertions++;
 
62
      return;
 
63
    }
 
64
 
 
65
    $this->failAssertionWithExpectedValue('true', $result, $message);
 
66
  }
 
67
 
 
68
 
 
69
  /**
 
70
   * Assert that two values are equal, strictly. The test fails if they are not.
 
71
   *
 
72
   * NOTE: This method uses PHP's strict equality test operator (`===`) to
 
73
   * compare values. This means values and types must be equal, key order must
 
74
   * be identical in arrays, and objects must be referentially identical.
 
75
   *
 
76
   * @param wild    The theoretically expected value, generated by careful
 
77
   *                reasoning about the properties of the system.
 
78
   * @param wild    The empirically derived value, generated by executing the
 
79
   *                test.
 
80
   * @param string  A human-readable description of what these values represent,
 
81
   *                and particularly of what a discrepancy means.
 
82
   *
 
83
   * @return void
 
84
   * @task assert
 
85
   */
 
86
  final protected function assertEqual($expect, $result, $message = null) {
 
87
    if ($expect === $result) {
 
88
      $this->assertions++;
 
89
      return;
 
90
    }
 
91
 
 
92
    $expect = PhutilReadableSerializer::printableValue($expect);
 
93
    $result = PhutilReadableSerializer::printableValue($result);
 
94
    $caller = self::getCallerInfo();
 
95
    $file = $caller['file'];
 
96
    $line = $caller['line'];
 
97
 
 
98
    if ($message !== null) {
 
99
      $output = pht(
 
100
        'Assertion failed, expected values to be equal (at %s:%d): %s',
 
101
        $file,
 
102
        $line,
 
103
        $message);
 
104
    } else {
 
105
      $output = pht(
 
106
        'Assertion failed, expected values to be equal (at %s:%d).',
 
107
        $file,
 
108
        $line);
 
109
    }
 
110
 
 
111
    $output .= "\n";
 
112
 
 
113
    if (strpos($expect, "\n") === false && strpos($result, "\n") === false) {
 
114
      $output .= "Expected: {$expect}\n";
 
115
      $output .= "  Actual: {$result}";
 
116
    } else {
 
117
      $output .= "Expected vs Actual Output Diff\n";
 
118
      $output .= ArcanistDiffUtils::renderDifferences(
 
119
        $expect,
 
120
        $result,
 
121
        $lines = 0xFFFF);
 
122
    }
 
123
 
 
124
    $this->failTest($output);
 
125
    throw new ArcanistPhutilTestTerminatedException($output);
 
126
  }
 
127
 
 
128
 
 
129
  /**
 
130
   * Assert an unconditional failure. This is just a convenience method that
 
131
   * better indicates intent than using dummy values with assertEqual(). This
 
132
   * causes test failure.
 
133
   *
 
134
   * @param   string  Human-readable description of the reason for test failure.
 
135
   * @return  void
 
136
   * @task    assert
 
137
   */
 
138
  final protected function assertFailure($message) {
 
139
    $this->failTest($message);
 
140
    throw new ArcanistPhutilTestTerminatedException($message);
 
141
  }
 
142
 
 
143
  /**
 
144
   * End this test by asserting that the test should be skipped for some
 
145
   * reason.
 
146
   *
 
147
   * @param   string  Reason for skipping this test.
 
148
   * @return  void
 
149
   * @task    assert
 
150
   */
 
151
  final protected function assertSkipped($message) {
 
152
    $this->skipTest($message);
 
153
    throw new ArcanistPhutilTestSkippedException($message);
 
154
  }
 
155
 
 
156
 
 
157
/* -(  Exception Handling  )------------------------------------------------- */
 
158
 
 
159
 
 
160
  /**
 
161
   * This simplest way to assert exceptions are thrown.
 
162
   *
 
163
   * @param exception   The expected exception.
 
164
   * @param callable    The thing which throws the exception.
 
165
   *
 
166
   * @return void
 
167
   * @task exceptions
 
168
   */
 
169
  final protected function assertException($expected_exception_class,
 
170
                                           $callable) {
 
171
    $this->tryTestCases(
 
172
      array('assertException' => array()),
 
173
      array(false),
 
174
      $callable,
 
175
      $expected_exception_class);
 
176
  }
 
177
 
 
178
  /**
 
179
   * Straightforward method for writing unit tests which check if some block of
 
180
   * code throws an exception. For example, this allows you to test the
 
181
   * exception behavior of ##is_a_fruit()## on various inputs:
 
182
   *
 
183
   *    public function testFruit() {
 
184
   *      $this->tryTestCases(
 
185
   *        array(
 
186
   *          'apple is a fruit'    => new Apple(),
 
187
   *          'rock is not a fruit' => new Rock(),
 
188
   *        ),
 
189
   *        array(
 
190
   *          true,
 
191
   *          false,
 
192
   *        ),
 
193
   *        array($this, 'tryIsAFruit'),
 
194
   *        'NotAFruitException');
 
195
   *    }
 
196
   *
 
197
   *    protected function tryIsAFruit($input) {
 
198
   *      is_a_fruit($input);
 
199
   *    }
 
200
   *
 
201
   * @param map       Map of test case labels to test case inputs.
 
202
   * @param list      List of expected results, true to indicate that the case
 
203
   *                  is expected to succeed and false to indicate that the case
 
204
   *                  is expected to throw.
 
205
   * @param callable  Callback to invoke for each test case.
 
206
   * @param string    Optional exception class to catch, defaults to
 
207
   *                  'Exception'.
 
208
   * @return void
 
209
   * @task exceptions
 
210
   */
 
211
  final protected function tryTestCases(
 
212
    array $inputs,
 
213
    array $expect,
 
214
    $callable,
 
215
    $exception_class = 'Exception') {
 
216
 
 
217
    if (count($inputs) !== count($expect)) {
 
218
      $this->assertFailure(
 
219
        'Input and expectations must have the same number of values.');
 
220
    }
 
221
 
 
222
    $labels = array_keys($inputs);
 
223
    $inputs = array_values($inputs);
 
224
    $expecting = array_values($expect);
 
225
    foreach ($inputs as $idx => $input) {
 
226
      $expect = $expecting[$idx];
 
227
      $label  = $labels[$idx];
 
228
 
 
229
      $caught = null;
 
230
      try {
 
231
        call_user_func($callable, $input);
 
232
      } catch (Exception $ex) {
 
233
        if ($ex instanceof ArcanistPhutilTestTerminatedException) {
 
234
          throw $ex;
 
235
        }
 
236
        if (!($ex instanceof $exception_class)) {
 
237
          throw $ex;
 
238
        }
 
239
        $caught = $ex;
 
240
      }
 
241
 
 
242
      $actual = !($caught instanceof Exception);
 
243
 
 
244
      if ($expect === $actual) {
 
245
        if ($expect) {
 
246
          $message = "Test case '{$label}' did not throw, as expected.";
 
247
        } else {
 
248
          $message = "Test case '{$label}' threw, as expected.";
 
249
        }
 
250
      } else {
 
251
        if ($expect) {
 
252
          $message = "Test case '{$label}' was expected to succeed, but it ".
 
253
                     "raised an exception of class ".get_class($ex)." with ".
 
254
                     "message: ".$ex->getMessage();
 
255
        } else {
 
256
          $message = "Test case '{$label}' was expected to raise an ".
 
257
                     "exception, but it did not throw anything.";
 
258
        }
 
259
      }
 
260
 
 
261
      $this->assertEqual($expect, $actual, $message);
 
262
    }
 
263
  }
 
264
 
 
265
 
 
266
  /**
 
267
   * Convenience wrapper around @{method:tryTestCases} for cases where your
 
268
   * inputs are scalar. For example:
 
269
   *
 
270
   *    public function testFruit() {
 
271
   *      $this->tryTestCaseMap(
 
272
   *        array(
 
273
   *          'apple' => true,
 
274
   *          'rock'  => false,
 
275
   *        ),
 
276
   *        array($this, 'tryIsAFruit'),
 
277
   *        'NotAFruitException');
 
278
   *    }
 
279
   *
 
280
   *    protected function tryIsAFruit($input) {
 
281
   *      is_a_fruit($input);
 
282
   *    }
 
283
   *
 
284
   * For cases where your inputs are not scalar, use @{method:tryTestCases}.
 
285
   *
 
286
   * @param map       Map of scalar test inputs to expected success (true
 
287
   *                  expects success, false expects an exception).
 
288
   * @param callable  Callback to invoke for each test case.
 
289
   * @param string    Optional exception class to catch, defaults to
 
290
   *                  'Exception'.
 
291
   * @return void
 
292
   * @task exceptions
 
293
   */
 
294
  final protected function tryTestCaseMap(
 
295
    array $map,
 
296
    $callable,
 
297
    $exception_class = 'Exception') {
 
298
    return $this->tryTestCases(
 
299
      array_fuse(array_keys($map)),
 
300
      array_values($map),
 
301
      $callable,
 
302
      $exception_class);
 
303
  }
 
304
 
 
305
 
 
306
/* -(  Hooks for Setup and Teardown  )--------------------------------------- */
 
307
 
 
308
 
 
309
  /**
 
310
   * This hook is invoked once, before any tests in this class are run. It
 
311
   * gives you an opportunity to perform setup steps for the entire class.
 
312
   *
 
313
   * @return void
 
314
   * @task hook
 
315
   */
 
316
  protected function willRunTests() {
 
317
    return;
 
318
  }
 
319
 
 
320
 
 
321
  /**
 
322
   * This hook is invoked once, after any tests in this class are run. It gives
 
323
   * you an opportunity to perform teardown steps for the entire class.
 
324
   *
 
325
   * @return void
 
326
   * @task hook
 
327
   */
 
328
  protected function didRunTests() {
 
329
    return;
 
330
  }
 
331
 
 
332
 
 
333
  /**
 
334
   * This hook is invoked once per test, before the test method is invoked.
 
335
   *
 
336
   * @param string Method name of the test which will be invoked.
 
337
   * @return void
 
338
   * @task hook
 
339
   */
 
340
  protected function willRunOneTest($test_method_name) {
 
341
    return;
 
342
  }
 
343
 
 
344
 
 
345
  /**
 
346
   * This hook is invoked once per test, after the test method is invoked.
 
347
   *
 
348
   * @param string Method name of the test which was invoked.
 
349
   * @return void
 
350
   * @task hook
 
351
   */
 
352
  protected function didRunOneTest($test_method_name) {
 
353
    return;
 
354
  }
 
355
 
 
356
 
 
357
  /**
 
358
   * This hook is invoked once, before any test cases execute. It gives you
 
359
   * an opportunity to perform setup steps for the entire suite of test cases.
 
360
   *
 
361
   * @param list<ArcanistPhutilTestCase> List of test cases to be run.
 
362
   * @return void
 
363
   * @task hook
 
364
   */
 
365
  public function willRunTestCases(array $test_cases) {
 
366
    return;
 
367
  }
 
368
 
 
369
 
 
370
  /**
 
371
   * This hook is invoked once, after all test cases execute.
 
372
   *
 
373
   * @param list<ArcanistPhutilTestCase> List of test cases that ran.
 
374
   * @return void
 
375
   * @task hook
 
376
   */
 
377
  public function didRunTestCases(array $test_cases) {
 
378
    return;
 
379
  }
 
380
 
 
381
 
 
382
/* -(  Internals  )---------------------------------------------------------- */
 
383
 
 
384
 
 
385
  /**
 
386
   * Construct a new test case. This method is ##final##, use willRunTests() to
 
387
   * provide test-wide setup logic.
 
388
   *
 
389
   * @task internal
 
390
   */
 
391
  final public function __construct() {}
 
392
 
 
393
 
 
394
  /**
 
395
   * Mark the currently-running test as a failure.
 
396
   *
 
397
   * @param string  Human-readable description of problems.
 
398
   * @return void
 
399
   *
 
400
   * @task internal
 
401
   */
 
402
  final private function failTest($reason) {
 
403
    $this->resultTest(ArcanistUnitTestResult::RESULT_FAIL, $reason);
 
404
  }
 
405
 
 
406
 
 
407
  /**
 
408
   * This was a triumph. I'm making a note here: HUGE SUCCESS.
 
409
   *
 
410
   * @param string  Human-readable overstatement of satisfaction.
 
411
   * @return void
 
412
   *
 
413
   * @task internal
 
414
   */
 
415
  final private function passTest($reason) {
 
416
    $this->resultTest(ArcanistUnitTestResult::RESULT_PASS, $reason);
 
417
  }
 
418
 
 
419
 
 
420
  /**
 
421
   * Mark the current running test as skipped.
 
422
   *
 
423
   * @param string  Description for why this test was skipped.
 
424
   * @return void
 
425
   * @task internal
 
426
   */
 
427
  final private function skipTest($reason) {
 
428
    $this->resultTest(ArcanistUnitTestResult::RESULT_SKIP, $reason);
 
429
  }
 
430
 
 
431
 
 
432
  final private function resultTest($test_result, $reason) {
 
433
    $coverage = $this->endCoverage();
 
434
 
 
435
    $result = new ArcanistUnitTestResult();
 
436
    $result->setCoverage($coverage);
 
437
    $result->setNamespace(get_class($this));
 
438
    $result->setName($this->runningTest);
 
439
    $result->setLink($this->getLink($this->runningTest));
 
440
    $result->setResult($test_result);
 
441
    $result->setDuration(microtime(true) - $this->testStartTime);
 
442
    $result->setUserData($reason);
 
443
    $this->results[] = $result;
 
444
 
 
445
    if ($this->renderer) {
 
446
      echo $this->renderer->renderUnitResult($result);
 
447
    }
 
448
  }
 
449
 
 
450
 
 
451
  /**
 
452
   * Execute the tests in this test case. You should not call this directly;
 
453
   * use @{class:PhutilUnitTestEngine} to orchestrate test execution.
 
454
   *
 
455
   * @return void
 
456
   * @task internal
 
457
   */
 
458
  final public function run() {
 
459
    $this->results = array();
 
460
 
 
461
    $reflection = new ReflectionClass($this);
 
462
    $methods = $reflection->getMethods();
 
463
 
 
464
    // Try to ensure that poorly-written tests which depend on execution order
 
465
    // (and are thus not properly isolated) will fail.
 
466
    shuffle($methods);
 
467
 
 
468
    $this->willRunTests();
 
469
    foreach ($methods as $method) {
 
470
      $name = $method->getName();
 
471
      if (preg_match('/^test/', $name)) {
 
472
        $this->runningTest = $name;
 
473
        $this->assertions = 0;
 
474
        $this->testStartTime = microtime(true);
 
475
 
 
476
        try {
 
477
          $this->willRunOneTest($name);
 
478
 
 
479
          $this->beginCoverage();
 
480
          $exceptions = array();
 
481
          try {
 
482
            call_user_func_array(
 
483
              array($this, $name),
 
484
              array());
 
485
            $this->passTest(pht('%d assertion(s) passed.', $this->assertions));
 
486
          } catch (Exception $ex) {
 
487
            $exceptions['Execution'] = $ex;
 
488
          }
 
489
 
 
490
          try {
 
491
            $this->didRunOneTest($name);
 
492
          } catch (Exception $ex) {
 
493
            $exceptions['Shutdown'] = $ex;
 
494
          }
 
495
 
 
496
          if ($exceptions) {
 
497
            if (count($exceptions) == 1) {
 
498
              throw head($exceptions);
 
499
            } else {
 
500
              throw new PhutilAggregateException(
 
501
                'Multiple exceptions were raised during test execution.',
 
502
                $exceptions);
 
503
            }
 
504
          }
 
505
 
 
506
          if (!$this->assertions) {
 
507
            $this->failTest(
 
508
              pht(
 
509
                'This test case made no assertions. Test cases must make at '.
 
510
                'least one assertion.'));
 
511
          }
 
512
 
 
513
        } catch (ArcanistPhutilTestTerminatedException $ex) {
 
514
          // Continue with the next test.
 
515
        } catch (ArcanistPhutilTestSkippedException $ex) {
 
516
          // Continue with the next test.
 
517
        } catch (Exception $ex) {
 
518
          $ex_class = get_class($ex);
 
519
          $ex_message = $ex->getMessage();
 
520
          $ex_trace = $ex->getTraceAsString();
 
521
          $message = "EXCEPTION ({$ex_class}): {$ex_message}\n{$ex_trace}";
 
522
          $this->failTest($message);
 
523
        }
 
524
      }
 
525
    }
 
526
    $this->didRunTests();
 
527
 
 
528
    return $this->results;
 
529
  }
 
530
 
 
531
  final public function setEnableCoverage($enable_coverage) {
 
532
    $this->enableCoverage = $enable_coverage;
 
533
    return $this;
 
534
  }
 
535
 
 
536
  /**
 
537
   * @phutil-external-symbol function xdebug_start_code_coverage
 
538
   */
 
539
  final private function beginCoverage() {
 
540
    if (!$this->enableCoverage) {
 
541
      return;
 
542
    }
 
543
 
 
544
    $this->assertCoverageAvailable();
 
545
    xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
 
546
  }
 
547
 
 
548
  /**
 
549
   * @phutil-external-symbol function xdebug_get_code_coverage
 
550
   * @phutil-external-symbol function xdebug_stop_code_coverage
 
551
   */
 
552
  final private function endCoverage() {
 
553
    if (!$this->enableCoverage) {
 
554
      return;
 
555
    }
 
556
 
 
557
    $result = xdebug_get_code_coverage();
 
558
    xdebug_stop_code_coverage($cleanup = false);
 
559
 
 
560
    $coverage = array();
 
561
 
 
562
    foreach ($result as $file => $report) {
 
563
      if (strncmp($file, $this->projectRoot, strlen($this->projectRoot))) {
 
564
        continue;
 
565
      }
 
566
 
 
567
      $max = max(array_keys($report));
 
568
      $str = '';
 
569
      for ($ii = 1; $ii <= $max; $ii++) {
 
570
        $c = idx($report, $ii);
 
571
        if ($c === -1) {
 
572
          $str .= 'U'; // Un-covered.
 
573
        } else if ($c === -2) {
 
574
          // TODO: This indicates "unreachable", but it flags the closing braces
 
575
          // of functions which end in "return", which is super ridiculous. Just
 
576
          // ignore it for now.
 
577
          //
 
578
          // See http://bugs.xdebug.org/view.php?id=1041
 
579
          $str .= 'N'; // Not executable.
 
580
        } else if ($c === 1) {
 
581
          $str .= 'C'; // Covered.
 
582
        } else {
 
583
          $str .= 'N'; // Not executable.
 
584
        }
 
585
      }
 
586
      $coverage[substr($file, strlen($this->projectRoot) + 1)] = $str;
 
587
    }
 
588
 
 
589
    // Only keep coverage information for files modified by the change. In
 
590
    // the case of --everything, we won't have paths, so just return all the
 
591
    // coverage data.
 
592
    if ($this->paths) {
 
593
      $coverage = array_select_keys($coverage, $this->paths);
 
594
    }
 
595
 
 
596
    return $coverage;
 
597
  }
 
598
 
 
599
  final private function assertCoverageAvailable() {
 
600
    if (!function_exists('xdebug_start_code_coverage')) {
 
601
      throw new Exception(
 
602
        "You've enabled code coverage but XDebug is not installed.");
 
603
    }
 
604
  }
 
605
 
 
606
  final public function setProjectRoot($project_root) {
 
607
    $this->projectRoot = $project_root;
 
608
    return $this;
 
609
  }
 
610
 
 
611
  final public function setPaths(array $paths) {
 
612
    $this->paths = $paths;
 
613
    return $this;
 
614
  }
 
615
 
 
616
  protected function getLink($method) {
 
617
    return null;
 
618
  }
 
619
 
 
620
  public function setRenderer(ArcanistUnitRenderer $renderer) {
 
621
    $this->renderer = $renderer;
 
622
    return $this;
 
623
  }
 
624
 
 
625
  /**
 
626
   * Returns info about the caller function.
 
627
   *
 
628
   * @return map
 
629
   */
 
630
  private static final function getCallerInfo() {
 
631
    $callee = array();
 
632
    $caller = array();
 
633
    $seen = false;
 
634
 
 
635
    foreach (array_slice(debug_backtrace(), 1) as $location) {
 
636
      $function = idx($location, 'function');
 
637
 
 
638
      if (!$seen && preg_match('/^assert[A-Z]/', $function)) {
 
639
        $seen = true;
 
640
        $caller = $location;
 
641
      } else if ($seen && !preg_match('/^assert[A-Z]/', $function)) {
 
642
        $callee = $location;
 
643
        break;
 
644
      }
 
645
    }
 
646
 
 
647
    return array(
 
648
      'file' => basename(idx($caller, 'file')),
 
649
      'line' => idx($caller, 'line'),
 
650
      'function' => idx($callee, 'function'),
 
651
      'class' => idx($callee, 'class'),
 
652
      'object' => idx($caller, 'object'),
 
653
      'type' => idx($callee, 'type'),
 
654
      'args' => idx($caller, 'args'),
 
655
    );
 
656
  }
 
657
 
 
658
 
 
659
  /**
 
660
   * Fail an assertion which checks that some result is equal to a specific
 
661
   * value, like 'true' or 'false'. This prints a readable error message and
 
662
   * fails the current test.
 
663
   *
 
664
   * This method throws and does not return.
 
665
   *
 
666
   * @param   string      Human readable description of the expected value.
 
667
   * @param   string      The actual value.
 
668
   * @param   string|null Optional assertion message.
 
669
   * @return  void
 
670
   * @task    internal
 
671
   */
 
672
  private function failAssertionWithExpectedValue(
 
673
    $expect_description,
 
674
    $actual_result,
 
675
    $message) {
 
676
 
 
677
    $caller = self::getCallerInfo();
 
678
    $file = $caller['file'];
 
679
    $line = $caller['line'];
 
680
 
 
681
    if ($message !== null) {
 
682
      $description = pht(
 
683
        "Assertion failed, expected '%s' (at %s:%d): %s",
 
684
        $expect_description,
 
685
        $file,
 
686
        $line,
 
687
        $message);
 
688
    } else {
 
689
      $description = pht(
 
690
        "Assertion failed, expected '%s' (at %s:%d).",
 
691
        $expect_description,
 
692
        $file,
 
693
        $line);
 
694
    }
 
695
 
 
696
    $actual_result = PhutilReadableSerializer::printableValue($actual_result);
 
697
    $header = pht('ACTUAL VALUE');
 
698
    $output = $description."\n\n".$header."\n".$actual_result;
 
699
 
 
700
    $this->failTest($output);
 
701
    throw new ArcanistPhutilTestTerminatedException($output);
 
702
  }
 
703
 
 
704
}