4
* Base test case for the very simple libphutil test framework.
6
* @task assert Making Test Assertions
7
* @task exceptions Exception Handling
8
* @task hook Hooks for Setup and Teardown
9
* @task internal Internals
11
abstract class ArcanistPhutilTestCase {
13
private $assertions = 0;
15
private $testStartTime;
16
private $results = array();
17
private $enableCoverage;
18
private $coverage = array();
24
/* -( Making Test Assertions )--------------------------------------------- */
28
* Assert that a value is `false`, strictly. The test fails if it is not.
30
* @param wild The empirically derived value, generated by executing the
32
* @param string A human-readable description of what these values represent,
33
* and particularly of what a discrepancy means.
38
final protected function assertFalse($result, $message = null) {
39
if ($result === false) {
44
$this->failAssertionWithExpectedValue('false', $result, $message);
49
* Assert that a value is `true`, strictly. The test fails if it is not.
51
* @param wild The empirically derived value, generated by executing the
53
* @param string A human-readable description of what these values represent,
54
* and particularly of what a discrepancy means.
59
final protected function assertTrue($result, $message = null) {
60
if ($result === true) {
65
$this->failAssertionWithExpectedValue('true', $result, $message);
70
* Assert that two values are equal, strictly. The test fails if they are not.
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.
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
80
* @param string A human-readable description of what these values represent,
81
* and particularly of what a discrepancy means.
86
final protected function assertEqual($expect, $result, $message = null) {
87
if ($expect === $result) {
92
$expect = PhutilReadableSerializer::printableValue($expect);
93
$result = PhutilReadableSerializer::printableValue($result);
94
$caller = self::getCallerInfo();
95
$file = $caller['file'];
96
$line = $caller['line'];
98
if ($message !== null) {
100
'Assertion failed, expected values to be equal (at %s:%d): %s',
106
'Assertion failed, expected values to be equal (at %s:%d).',
113
if (strpos($expect, "\n") === false && strpos($result, "\n") === false) {
114
$output .= "Expected: {$expect}\n";
115
$output .= " Actual: {$result}";
117
$output .= "Expected vs Actual Output Diff\n";
118
$output .= ArcanistDiffUtils::renderDifferences(
124
$this->failTest($output);
125
throw new ArcanistPhutilTestTerminatedException($output);
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.
134
* @param string Human-readable description of the reason for test failure.
138
final protected function assertFailure($message) {
139
$this->failTest($message);
140
throw new ArcanistPhutilTestTerminatedException($message);
144
* End this test by asserting that the test should be skipped for some
147
* @param string Reason for skipping this test.
151
final protected function assertSkipped($message) {
152
$this->skipTest($message);
153
throw new ArcanistPhutilTestSkippedException($message);
157
/* -( Exception Handling )------------------------------------------------- */
161
* This simplest way to assert exceptions are thrown.
163
* @param exception The expected exception.
164
* @param callable The thing which throws the exception.
169
final protected function assertException($expected_exception_class,
172
array('assertException' => array()),
175
$expected_exception_class);
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:
183
* public function testFruit() {
184
* $this->tryTestCases(
186
* 'apple is a fruit' => new Apple(),
187
* 'rock is not a fruit' => new Rock(),
193
* array($this, 'tryIsAFruit'),
194
* 'NotAFruitException');
197
* protected function tryIsAFruit($input) {
198
* is_a_fruit($input);
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
211
final protected function tryTestCases(
215
$exception_class = 'Exception') {
217
if (count($inputs) !== count($expect)) {
218
$this->assertFailure(
219
'Input and expectations must have the same number of values.');
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];
231
call_user_func($callable, $input);
232
} catch (Exception $ex) {
233
if ($ex instanceof ArcanistPhutilTestTerminatedException) {
236
if (!($ex instanceof $exception_class)) {
242
$actual = !($caught instanceof Exception);
244
if ($expect === $actual) {
246
$message = "Test case '{$label}' did not throw, as expected.";
248
$message = "Test case '{$label}' threw, as expected.";
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();
256
$message = "Test case '{$label}' was expected to raise an ".
257
"exception, but it did not throw anything.";
261
$this->assertEqual($expect, $actual, $message);
267
* Convenience wrapper around @{method:tryTestCases} for cases where your
268
* inputs are scalar. For example:
270
* public function testFruit() {
271
* $this->tryTestCaseMap(
276
* array($this, 'tryIsAFruit'),
277
* 'NotAFruitException');
280
* protected function tryIsAFruit($input) {
281
* is_a_fruit($input);
284
* For cases where your inputs are not scalar, use @{method:tryTestCases}.
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
294
final protected function tryTestCaseMap(
297
$exception_class = 'Exception') {
298
return $this->tryTestCases(
299
array_fuse(array_keys($map)),
306
/* -( Hooks for Setup and Teardown )--------------------------------------- */
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.
316
protected function willRunTests() {
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.
328
protected function didRunTests() {
334
* This hook is invoked once per test, before the test method is invoked.
336
* @param string Method name of the test which will be invoked.
340
protected function willRunOneTest($test_method_name) {
346
* This hook is invoked once per test, after the test method is invoked.
348
* @param string Method name of the test which was invoked.
352
protected function didRunOneTest($test_method_name) {
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.
361
* @param list<ArcanistPhutilTestCase> List of test cases to be run.
365
public function willRunTestCases(array $test_cases) {
371
* This hook is invoked once, after all test cases execute.
373
* @param list<ArcanistPhutilTestCase> List of test cases that ran.
377
public function didRunTestCases(array $test_cases) {
382
/* -( Internals )---------------------------------------------------------- */
386
* Construct a new test case. This method is ##final##, use willRunTests() to
387
* provide test-wide setup logic.
391
final public function __construct() {}
395
* Mark the currently-running test as a failure.
397
* @param string Human-readable description of problems.
402
final private function failTest($reason) {
403
$this->resultTest(ArcanistUnitTestResult::RESULT_FAIL, $reason);
408
* This was a triumph. I'm making a note here: HUGE SUCCESS.
410
* @param string Human-readable overstatement of satisfaction.
415
final private function passTest($reason) {
416
$this->resultTest(ArcanistUnitTestResult::RESULT_PASS, $reason);
421
* Mark the current running test as skipped.
423
* @param string Description for why this test was skipped.
427
final private function skipTest($reason) {
428
$this->resultTest(ArcanistUnitTestResult::RESULT_SKIP, $reason);
432
final private function resultTest($test_result, $reason) {
433
$coverage = $this->endCoverage();
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;
445
if ($this->renderer) {
446
echo $this->renderer->renderUnitResult($result);
452
* Execute the tests in this test case. You should not call this directly;
453
* use @{class:PhutilUnitTestEngine} to orchestrate test execution.
458
final public function run() {
459
$this->results = array();
461
$reflection = new ReflectionClass($this);
462
$methods = $reflection->getMethods();
464
// Try to ensure that poorly-written tests which depend on execution order
465
// (and are thus not properly isolated) will fail.
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);
477
$this->willRunOneTest($name);
479
$this->beginCoverage();
480
$exceptions = array();
482
call_user_func_array(
485
$this->passTest(pht('%d assertion(s) passed.', $this->assertions));
486
} catch (Exception $ex) {
487
$exceptions['Execution'] = $ex;
491
$this->didRunOneTest($name);
492
} catch (Exception $ex) {
493
$exceptions['Shutdown'] = $ex;
497
if (count($exceptions) == 1) {
498
throw head($exceptions);
500
throw new PhutilAggregateException(
501
'Multiple exceptions were raised during test execution.',
506
if (!$this->assertions) {
509
'This test case made no assertions. Test cases must make at '.
510
'least one assertion.'));
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);
526
$this->didRunTests();
528
return $this->results;
531
final public function setEnableCoverage($enable_coverage) {
532
$this->enableCoverage = $enable_coverage;
537
* @phutil-external-symbol function xdebug_start_code_coverage
539
final private function beginCoverage() {
540
if (!$this->enableCoverage) {
544
$this->assertCoverageAvailable();
545
xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
549
* @phutil-external-symbol function xdebug_get_code_coverage
550
* @phutil-external-symbol function xdebug_stop_code_coverage
552
final private function endCoverage() {
553
if (!$this->enableCoverage) {
557
$result = xdebug_get_code_coverage();
558
xdebug_stop_code_coverage($cleanup = false);
562
foreach ($result as $file => $report) {
563
if (strncmp($file, $this->projectRoot, strlen($this->projectRoot))) {
567
$max = max(array_keys($report));
569
for ($ii = 1; $ii <= $max; $ii++) {
570
$c = idx($report, $ii);
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.
578
// See http://bugs.xdebug.org/view.php?id=1041
579
$str .= 'N'; // Not executable.
580
} else if ($c === 1) {
581
$str .= 'C'; // Covered.
583
$str .= 'N'; // Not executable.
586
$coverage[substr($file, strlen($this->projectRoot) + 1)] = $str;
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
593
$coverage = array_select_keys($coverage, $this->paths);
599
final private function assertCoverageAvailable() {
600
if (!function_exists('xdebug_start_code_coverage')) {
602
"You've enabled code coverage but XDebug is not installed.");
606
final public function setProjectRoot($project_root) {
607
$this->projectRoot = $project_root;
611
final public function setPaths(array $paths) {
612
$this->paths = $paths;
616
protected function getLink($method) {
620
public function setRenderer(ArcanistUnitRenderer $renderer) {
621
$this->renderer = $renderer;
626
* Returns info about the caller function.
630
private static final function getCallerInfo() {
635
foreach (array_slice(debug_backtrace(), 1) as $location) {
636
$function = idx($location, 'function');
638
if (!$seen && preg_match('/^assert[A-Z]/', $function)) {
641
} else if ($seen && !preg_match('/^assert[A-Z]/', $function)) {
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'),
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.
664
* This method throws and does not return.
666
* @param string Human readable description of the expected value.
667
* @param string The actual value.
668
* @param string|null Optional assertion message.
672
private function failAssertionWithExpectedValue(
677
$caller = self::getCallerInfo();
678
$file = $caller['file'];
679
$line = $caller['line'];
681
if ($message !== null) {
683
"Assertion failed, expected '%s' (at %s:%d): %s",
690
"Assertion failed, expected '%s' (at %s:%d).",
696
$actual_result = PhutilReadableSerializer::printableValue($actual_result);
697
$header = pht('ACTUAL VALUE');
698
$output = $description."\n\n".$header."\n".$actual_result;
700
$this->failTest($output);
701
throw new ArcanistPhutilTestTerminatedException($output);