diff --git a/src/unit/engine/ArcanistUnitTestEngine.php b/src/unit/engine/ArcanistUnitTestEngine.php index 5116b964..95d67313 100644 --- a/src/unit/engine/ArcanistUnitTestEngine.php +++ b/src/unit/engine/ArcanistUnitTestEngine.php @@ -1,115 +1,113 @@ supportsRunAllTests() && $run_all_tests) { throw new Exception( pht( "Engine '%s' does not support %s.", get_class($this), '--everything')); } $this->runAllTests = $run_all_tests; return $this; } - public function getRunAllTests() { + final public function getRunAllTests() { return $this->runAllTests; } protected function supportsRunAllTests() { return false; } - final public function __construct() {} - - public function setConfigurationManager( + final public function setConfigurationManager( ArcanistConfigurationManager $configuration_manager) { $this->configurationManager = $configuration_manager; return $this; } - public function getConfigurationManager() { + final public function getConfigurationManager() { return $this->configurationManager; } final public function setWorkingCopy( ArcanistWorkingCopyIdentity $working_copy) { $this->workingCopy = $working_copy; return $this; } final public function getWorkingCopy() { return $this->workingCopy; } final public function setPaths(array $paths) { $this->paths = $paths; return $this; } final public function getPaths() { return $this->paths; } final public function setArguments(array $arguments) { $this->arguments = $arguments; return $this; } final public function getArgument($key, $default = null) { return idx($this->arguments, $key, $default); } final public function setEnableAsyncTests($enable_async_tests) { $this->enableAsyncTests = $enable_async_tests; return $this; } final public function getEnableAsyncTests() { return $this->enableAsyncTests; } final public function setEnableCoverage($enable_coverage) { $this->enableCoverage = $enable_coverage; return $this; } final public function getEnableCoverage() { return $this->enableCoverage; } - public function setRenderer(ArcanistUnitRenderer $renderer) { + final public function setRenderer(ArcanistUnitRenderer $renderer) { $this->renderer = $renderer; return $this; } abstract public function run(); /** * Modify the return value of this function in the child class, if you do * not need to echo the test results after all the tests have been run. This * is the case for example when the child class prints the tests results * while the tests are running. */ public function shouldEchoTestResults() { return true; } } diff --git a/src/unit/engine/PhutilUnitTestEngine.php b/src/unit/engine/PhutilUnitTestEngine.php index 1abf1e74..58cf52df 100644 --- a/src/unit/engine/PhutilUnitTestEngine.php +++ b/src/unit/engine/PhutilUnitTestEngine.php @@ -1,201 +1,215 @@ getRunAllTests()) { $run_tests = $this->getAllTests(); } else { $run_tests = $this->getTestsForPaths(); } if (!$run_tests) { throw new ArcanistNoEffectException(pht('No tests to run.')); } $enable_coverage = $this->getEnableCoverage(); + if ($enable_coverage !== false) { if (!function_exists('xdebug_start_code_coverage')) { if ($enable_coverage === true) { throw new ArcanistUsageException( pht( - 'You specified %s but xdebug is not available, so '. + 'You specified %s but %s is not available, so '. 'coverage can not be enabled for %s.', '--coverage', + 'XDebug', __CLASS__)); } } else { $enable_coverage = true; } } - $project_root = $this->getWorkingCopy()->getProjectRoot(); - $test_cases = array(); + foreach ($run_tests as $test_class) { - $test_case = newv($test_class, array()); - $test_case->setEnableCoverage($enable_coverage); - $test_case->setWorkingCopy($this->getWorkingCopy()); + $test_case = newv($test_class, array()) + ->setEnableCoverage($enable_coverage) + ->setWorkingCopy($this->getWorkingCopy()); if ($this->getPaths()) { $test_case->setPaths($this->getPaths()); } + if ($this->renderer) { $test_case->setRenderer($this->renderer); } + $test_cases[] = $test_case; } foreach ($test_cases as $test_case) { $test_case->willRunTestCases($test_cases); } $results = array(); foreach ($test_cases as $test_case) { $results[] = $test_case->run(); } $results = array_mergev($results); foreach ($test_cases as $test_case) { $test_case->didRunTestCases($test_cases); } return $results; } private function getAllTests() { $project_root = $this->getWorkingCopy()->getProjectRoot(); $symbols = id(new PhutilSymbolLoader()) ->setType('class') ->setAncestorClass('PhutilTestCase') ->setConcreteOnly(true) ->selectSymbolsWithoutLoading(); $in_working_copy = array(); $run_tests = array(); foreach ($symbols as $symbol) { if (!preg_match('@(?:^|/)__tests__/@', $symbol['where'])) { continue; } $library = $symbol['library']; if (!isset($in_working_copy[$library])) { $library_root = phutil_get_library_root($library); $in_working_copy[$library] = Filesystem::isDescendant( $library_root, $project_root); } if ($in_working_copy[$library]) { $run_tests[] = $symbol['name']; } } return $run_tests; } + /** + * Retrieve all relevant test cases. + * + * Looks for any class that extends @{class:PhutilTestCase} inside a + * `__tests__` directory in any parent directory of every affected file. + * + * The idea is that "infrastructure/__tests__/" tests defines general tests + * for all of "infrastructure/", and those tests run for any change in + * "infrastructure/". However, "infrastructure/concrete/rebar/__tests__/" + * defines more specific tests that run only when "rebar/" (or some + * subdirectory) changes. + * + * @return list The names of the test case classes to be executed. + */ private function getTestsForPaths() { - $project_root = $this->getWorkingCopy()->getProjectRoot(); + $look_here = $this->getTestPaths(); + $run_tests = array(); - $look_here = array(); + foreach ($look_here as $path_info) { + $library = $path_info['library']; + $path = $path_info['path']; + + $symbols = id(new PhutilSymbolLoader()) + ->setType('class') + ->setLibrary($library) + ->setPathPrefix($path) + ->setAncestorClass('PhutilTestCase') + ->setConcreteOnly(true) + ->selectAndLoadSymbols(); + + foreach ($symbols as $symbol) { + $run_tests[$symbol['name']] = true; + } + } + + return array_keys($run_tests); + } + + /** + * Returns the paths in which we should look for tests to execute. + * + * @return list A list of paths in which to search for test cases. + */ + public function getTestPaths() { + $root = $this->getWorkingCopy()->getProjectRoot(); + $paths = array(); foreach ($this->getPaths() as $path) { $library_root = phutil_get_library_root_for_path($path); + if (!$library_root) { continue; } + $library_name = phutil_get_library_name_for_root($library_root); if (!$library_name) { throw new Exception( - sprintf( - "%s\n\n %s\n\n%s\n\n - %s\n - %s\n", - pht( - 'Attempting to run unit tests on a libphutil library '. - 'which has not been loaded, at:'), + pht( + "Attempting to run unit tests on a libphutil library which has ". + "not been loaded, at:\n\n". + " %s\n\n". + "This probably means one of two things:\n\n". + " - You may need to add this library to %s.\n". + " - You may be running tests on a copy of libphutil or ". + "arcanist using a different copy of libphutil or arcanist. ". + "This operation is not supported.\n", $library_root, - pht('This probably means one of two things:'), - pht( - 'You may need to add this library to %s.', - '.arcconfig.'), - pht( - 'You may be running tests on a copy of libphutil or '. - 'arcanist using a different copy of libphutil or arcanist. '. - 'This operation is not supported.'))); + '.arcconfig.')); } - $path = Filesystem::resolvePath($path, $project_root); - - if (!is_dir($path)) { - $path = dirname($path); - } + $path = Filesystem::resolvePath($path, $root); + $library_path = Filesystem::readablePath($path, $library_root); - if ($path == $library_root) { - $look_here[$library_name.':.'] = array( - 'library' => $library_name, - 'path' => '', - ); - } else if (!Filesystem::isDescendant($path, $library_root)) { + if (!Filesystem::isDescendant($path, $library_root)) { // We have encountered some kind of symlink maze -- for instance, $path // is some symlink living outside the library that links into some file // inside the library. Just ignore these cases, since the affected file // does not actually lie within the library. continue; - } else { - $library_path = Filesystem::readablePath($path, $library_root); - do { - $look_here[$library_name.':'.$library_path] = array( - 'library' => $library_name, - 'path' => $library_path, - ); - $library_path = dirname($library_path); - } while ($library_path != '.'); } - } - // Look for any class that extends PhutilTestCase inside a `__tests__` - // directory in any parent directory of every affected file. - // - // The idea is that "infrastructure/__tests__/" tests defines general tests - // for all of "infrastructure/", and those tests run for any change in - // "infrastructure/". However, "infrastructure/concrete/rebar/__tests__/" - // defines more specific tests that run only when rebar/ (or some - // subdirectory) changes. - - $run_tests = array(); - foreach ($look_here as $path_info) { - $library = $path_info['library']; - $path = $path_info['path']; - - $symbols = id(new PhutilSymbolLoader()) - ->setType('class') - ->setLibrary($library) - ->setPathPrefix(($path ? $path.'/' : '').'__tests__/') - ->setAncestorClass('PhutilTestCase') - ->setConcreteOnly(true) - ->selectAndLoadSymbols(); + if (is_file($path) && preg_match('@(?:^|/)__tests__/@', $path)) { + $paths[$library_name.':'.$library_path] = array( + 'library' => $library_name, + 'path' => $library_path, + ); + continue; + } - foreach ($symbols as $symbol) { - $run_tests[$symbol['name']] = true; + while (($library_path = dirname($library_path)) != '.') { + $paths[$library_name.':'.$library_path] = array( + 'library' => $library_name, + 'path' => $library_path.'/__tests__/', + ); } } - $run_tests = array_keys($run_tests); - return $run_tests; + return $paths; } public function shouldEchoTestResults() { return !$this->renderer; } } diff --git a/src/unit/engine/__tests__/PhutilUnitTestEngineTestCase.php b/src/unit/engine/__tests__/PhutilUnitTestEngineTestCase.php index 0df0512d..11c132d9 100644 --- a/src/unit/engine/__tests__/PhutilUnitTestEngineTestCase.php +++ b/src/unit/engine/__tests__/PhutilUnitTestEngineTestCase.php @@ -1,123 +1,166 @@ assertEqual( 1, self::$allTestsCounter, pht( 'Expect %s has been called once.', 'willRunTests()')); self::$allTestsCounter--; - $actual_test_count = 4; + $actual_test_count = 5; $this->assertEqual( $actual_test_count, count(self::$distinctWillRunTests), pht( 'Expect %s was called once for each test.', 'willRunOneTest()')); $this->assertEqual( $actual_test_count, count(self::$distinctDidRunTests), pht( 'Expect %s was called once for each test.', 'didRunOneTest()')); $this->assertEqual( self::$distinctWillRunTests, self::$distinctDidRunTests, pht('Expect same tests had pre-run and post-run callbacks invoked.')); } public function __destruct() { if (self::$allTestsCounter !== 0) { throw new Exception( pht( '%s was not called correctly after tests completed!', 'didRunTests()')); } } protected function willRunOneTest($test) { self::$distinctWillRunTests[$test] = true; self::$oneTestCounter++; } protected function didRunOneTest($test) { $this->assertEqual( 1, self::$oneTestCounter, pht('Expect %s depth to be one.', 'willRunOneTest()')); self::$distinctDidRunTests[$test] = true; self::$oneTestCounter--; } public function testPass() { $this->assertEqual(1, 1, pht('This test is expected to pass.')); } public function testFailSkip() { $failed = 0; $skipped = 0; $test_case = id(new PhutilTestCaseTestCase()) ->setWorkingCopy($this->getWorkingCopy()); foreach ($test_case->run() as $result) { if ($result->getResult() == ArcanistUnitTestResult::RESULT_FAIL) { $failed++; } else if ($result->getResult() == ArcanistUnitTestResult::RESULT_SKIP) { $skipped++; } else { $this->assertFailure(pht('These tests should either fail or skip.')); } } $this->assertEqual(1, $failed, pht('One test was expected to fail.')); $this->assertEqual(1, $skipped, pht('One test was expected to skip.')); } public function testTryTestCases() { $this->tryTestCases( array( true, false, ), array( true, false, ), array($this, 'throwIfFalsey')); } public function testTryTestMap() { $this->tryTestCaseMap( array( 1 => true, 0 => false, ), array($this, 'throwIfFalsey')); } protected function throwIfFalsey($input) { if (!$input) { throw new Exception(pht('This is a negative test case!')); } } + public function testGetTestPaths() { + $tests = array( + array( + array(), + array(), + ), + + array( + array(__FILE__), + array(__FILE__), + ), + + array( + array(dirname(__FILE__)), + array( + dirname(dirname(__FILE__)).'/__tests__/', + dirname(dirname(dirname(__FILE__))).'/__tests__/', + ), + ), + ); + + $test_engine = id(new PhutilUnitTestEngine()) + ->setWorkingCopy($this->getWorkingCopy()); + + foreach ($tests as $test) { + list($paths, $tests) = $test; + $expected = array(); + + foreach ($tests as $path) { + $library_root = phutil_get_library_root_for_path($path); + $library = phutil_get_library_name_for_root($library_root); + + $expected[] = array( + 'library' => $library, + 'path' => Filesystem::readablePath($path, $library_root), + ); + } + + $test_engine->setPaths($paths); + $this->assertEqual($expected, array_values($test_engine->getTestPaths())); + } + } + } diff --git a/src/unit/engine/phutil/PhutilTestCase.php b/src/unit/engine/phutil/PhutilTestCase.php index a3f98510..68f0cdb3 100644 --- a/src/unit/engine/phutil/PhutilTestCase.php +++ b/src/unit/engine/phutil/PhutilTestCase.php @@ -1,742 +1,745 @@ assertions++; return; } $this->failAssertionWithExpectedValue('false', $result, $message); } /** * Assert that a value is `true`, strictly. The test fails if it is not. * * @param wild The empirically derived value, generated by executing the * test. * @param string A human-readable description of what these values represent, * and particularly of what a discrepancy means. * * @return void * @task assert */ final protected function assertTrue($result, $message = null) { if ($result === true) { $this->assertions++; return; } $this->failAssertionWithExpectedValue('true', $result, $message); } /** * Assert that two values are equal, strictly. The test fails if they are not. * * NOTE: This method uses PHP's strict equality test operator (`===`) to * compare values. This means values and types must be equal, key order must * be identical in arrays, and objects must be referentially identical. * * @param wild The theoretically expected value, generated by careful * reasoning about the properties of the system. * @param wild The empirically derived value, generated by executing the * test. * @param string A human-readable description of what these values represent, * and particularly of what a discrepancy means. * * @return void * @task assert */ final protected function assertEqual($expect, $result, $message = null) { if ($expect === $result) { $this->assertions++; return; } $expect = PhutilReadableSerializer::printableValue($expect); $result = PhutilReadableSerializer::printableValue($result); $caller = self::getCallerInfo(); $file = $caller['file']; $line = $caller['line']; if ($message !== null) { $output = pht( 'Assertion failed, expected values to be equal (at %s:%d): %s', $file, $line, $message); } else { $output = pht( 'Assertion failed, expected values to be equal (at %s:%d).', $file, $line); } $output .= "\n"; if (strpos($expect, "\n") === false && strpos($result, "\n") === false) { $output .= pht("Expected: %s\n Actual: %s", $expect, $result); } else { $output .= pht( "Expected vs Actual Output Diff\n%s", ArcanistDiffUtils::renderDifferences( $expect, $result, $lines = 0xFFFF)); } $this->failTest($output); throw new PhutilTestTerminatedException($output); } /** * Assert an unconditional failure. This is just a convenience method that * better indicates intent than using dummy values with assertEqual(). This * causes test failure. * * @param string Human-readable description of the reason for test failure. * @return void * @task assert */ final protected function assertFailure($message) { $this->failTest($message); throw new PhutilTestTerminatedException($message); } /** * End this test by asserting that the test should be skipped for some * reason. * * @param string Reason for skipping this test. * @return void * @task assert */ final protected function assertSkipped($message) { $this->skipTest($message); throw new PhutilTestSkippedException($message); } /* -( Exception Handling )------------------------------------------------- */ /** * This simplest way to assert exceptions are thrown. * * @param exception The expected exception. * @param callable The thing which throws the exception. * * @return void * @task exceptions */ - final protected function assertException($expected_exception_class, - $callable) { + final protected function assertException( + $expected_exception_class, + $callable) { + $this->tryTestCases( array('assertException' => array()), array(false), $callable, $expected_exception_class); } /** * Straightforward method for writing unit tests which check if some block of * code throws an exception. For example, this allows you to test the * exception behavior of ##is_a_fruit()## on various inputs: * * public function testFruit() { * $this->tryTestCases( * array( * 'apple is a fruit' => new Apple(), * 'rock is not a fruit' => new Rock(), * ), * array( * true, * false, * ), * array($this, 'tryIsAFruit'), * 'NotAFruitException'); * } * * protected function tryIsAFruit($input) { * is_a_fruit($input); * } * * @param map Map of test case labels to test case inputs. * @param list List of expected results, true to indicate that the case * is expected to succeed and false to indicate that the case * is expected to throw. * @param callable Callback to invoke for each test case. * @param string Optional exception class to catch, defaults to * 'Exception'. * @return void * @task exceptions */ final protected function tryTestCases( array $inputs, array $expect, $callable, $exception_class = 'Exception') { if (count($inputs) !== count($expect)) { $this->assertFailure( pht('Input and expectations must have the same number of values.')); } $labels = array_keys($inputs); $inputs = array_values($inputs); $expecting = array_values($expect); foreach ($inputs as $idx => $input) { $expect = $expecting[$idx]; $label = $labels[$idx]; $caught = null; try { call_user_func($callable, $input); } catch (Exception $ex) { if ($ex instanceof PhutilTestTerminatedException) { throw $ex; } if (!($ex instanceof $exception_class)) { throw $ex; } $caught = $ex; } $actual = !($caught instanceof Exception); if ($expect === $actual) { if ($expect) { $message = pht("Test case '%s' did not throw, as expected.", $label); } else { $message = pht("Test case '%s' threw, as expected.", $label); } } else { if ($expect) { $message = pht( "Test case '%s' was expected to succeed, but it ". "raised an exception of class %s with message: %s", $label, get_class($ex), $ex->getMessage()); } else { $message = pht( "Test case '%s' was expected to raise an ". "exception, but it did not throw anything.", $label); } } $this->assertEqual($expect, $actual, $message); } } /** * Convenience wrapper around @{method:tryTestCases} for cases where your * inputs are scalar. For example: * * public function testFruit() { * $this->tryTestCaseMap( * array( * 'apple' => true, * 'rock' => false, * ), * array($this, 'tryIsAFruit'), * 'NotAFruitException'); * } * * protected function tryIsAFruit($input) { * is_a_fruit($input); * } * * For cases where your inputs are not scalar, use @{method:tryTestCases}. * * @param map Map of scalar test inputs to expected success (true * expects success, false expects an exception). * @param callable Callback to invoke for each test case. * @param string Optional exception class to catch, defaults to * 'Exception'. * @return void * @task exceptions */ final protected function tryTestCaseMap( array $map, $callable, $exception_class = 'Exception') { + return $this->tryTestCases( array_fuse(array_keys($map)), array_values($map), $callable, $exception_class); } /* -( Hooks for Setup and Teardown )--------------------------------------- */ /** * This hook is invoked once, before any tests in this class are run. It * gives you an opportunity to perform setup steps for the entire class. * * @return void * @task hook */ protected function willRunTests() { return; } /** * This hook is invoked once, after any tests in this class are run. It gives * you an opportunity to perform teardown steps for the entire class. * * @return void * @task hook */ protected function didRunTests() { return; } /** * This hook is invoked once per test, before the test method is invoked. * * @param string Method name of the test which will be invoked. * @return void * @task hook */ protected function willRunOneTest($test_method_name) { return; } /** * This hook is invoked once per test, after the test method is invoked. * * @param string Method name of the test which was invoked. * @return void * @task hook */ protected function didRunOneTest($test_method_name) { return; } /** * This hook is invoked once, before any test cases execute. It gives you * an opportunity to perform setup steps for the entire suite of test cases. * * @param list List of test cases to be run. * @return void * @task hook */ public function willRunTestCases(array $test_cases) { return; } /** * This hook is invoked once, after all test cases execute. * * @param list List of test cases that ran. * @return void * @task hook */ public function didRunTestCases(array $test_cases) { return; } /* -( Internals )---------------------------------------------------------- */ /** * Construct a new test case. This method is ##final##, use willRunTests() to * provide test-wide setup logic. * * @task internal */ final public function __construct() {} /** * Mark the currently-running test as a failure. * * @param string Human-readable description of problems. * @return void * * @task internal */ final private function failTest($reason) { $this->resultTest(ArcanistUnitTestResult::RESULT_FAIL, $reason); } /** * This was a triumph. I'm making a note here: HUGE SUCCESS. * * @param string Human-readable overstatement of satisfaction. * @return void * * @task internal */ final private function passTest($reason) { $this->resultTest(ArcanistUnitTestResult::RESULT_PASS, $reason); } /** * Mark the current running test as skipped. * * @param string Description for why this test was skipped. * @return void * @task internal */ final private function skipTest($reason) { $this->resultTest(ArcanistUnitTestResult::RESULT_SKIP, $reason); } final private function resultTest($test_result, $reason) { $coverage = $this->endCoverage(); $result = new ArcanistUnitTestResult(); $result->setCoverage($coverage); $result->setNamespace(get_class($this)); $result->setName($this->runningTest); $result->setLink($this->getLink($this->runningTest)); $result->setResult($test_result); $result->setDuration(microtime(true) - $this->testStartTime); $result->setUserData($reason); $this->results[] = $result; if ($this->renderer) { echo $this->renderer->renderUnitResult($result); } } /** * Execute the tests in this test case. You should not call this directly; * use @{class:PhutilUnitTestEngine} to orchestrate test execution. * * @return void * @task internal */ final public function run() { $this->results = array(); $reflection = new ReflectionClass($this); $methods = $reflection->getMethods(); // Try to ensure that poorly-written tests which depend on execution order // (and are thus not properly isolated) will fail. shuffle($methods); $this->willRunTests(); foreach ($methods as $method) { $name = $method->getName(); if (preg_match('/^test/', $name)) { $this->runningTest = $name; $this->assertions = 0; $this->testStartTime = microtime(true); try { $this->willRunOneTest($name); $this->beginCoverage(); $exceptions = array(); try { call_user_func_array( array($this, $name), array()); $this->passTest(pht('%d assertion(s) passed.', $this->assertions)); } catch (Exception $ex) { $exceptions['Execution'] = $ex; } try { $this->didRunOneTest($name); } catch (Exception $ex) { $exceptions['Shutdown'] = $ex; } if ($exceptions) { if (count($exceptions) == 1) { throw head($exceptions); } else { throw new PhutilAggregateException( pht('Multiple exceptions were raised during test execution.'), $exceptions); } } if (!$this->assertions) { $this->failTest( pht( 'This test case made no assertions. Test cases must make at '. 'least one assertion.')); } } catch (PhutilTestTerminatedException $ex) { // Continue with the next test. } catch (PhutilTestSkippedException $ex) { // Continue with the next test. } catch (Exception $ex) { $ex_class = get_class($ex); $ex_message = $ex->getMessage(); $ex_trace = $ex->getTraceAsString(); $message = sprintf( "%s (%s): %s\n%s", pht('EXCEPTION'), $ex_class, $ex_message, $ex_trace); $this->failTest($message); } } } $this->didRunTests(); return $this->results; } final public function setEnableCoverage($enable_coverage) { $this->enableCoverage = $enable_coverage; return $this; } /** * @phutil-external-symbol function xdebug_start_code_coverage */ final private function beginCoverage() { if (!$this->enableCoverage) { return; } $this->assertCoverageAvailable(); xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); } /** * @phutil-external-symbol function xdebug_get_code_coverage * @phutil-external-symbol function xdebug_stop_code_coverage */ final private function endCoverage() { if (!$this->enableCoverage) { return; } $result = xdebug_get_code_coverage(); xdebug_stop_code_coverage($cleanup = false); $coverage = array(); foreach ($result as $file => $report) { $project_root = $this->getProjectRoot(); if (strncmp($file, $project_root, strlen($project_root))) { continue; } $max = max(array_keys($report)); $str = ''; for ($ii = 1; $ii <= $max; $ii++) { $c = idx($report, $ii); if ($c === -1) { $str .= 'U'; // Un-covered. } else if ($c === -2) { // TODO: This indicates "unreachable", but it flags the closing braces // of functions which end in "return", which is super ridiculous. Just // ignore it for now. // // See http://bugs.xdebug.org/view.php?id=1041 $str .= 'N'; // Not executable. } else if ($c === 1) { $str .= 'C'; // Covered. } else { $str .= 'N'; // Not executable. } } $coverage[substr($file, strlen($project_root) + 1)] = $str; } // Only keep coverage information for files modified by the change. In // the case of --everything, we won't have paths, so just return all the // coverage data. if ($this->paths) { $coverage = array_select_keys($coverage, $this->paths); } return $coverage; } final private function assertCoverageAvailable() { if (!function_exists('xdebug_start_code_coverage')) { throw new Exception( pht("You've enabled code coverage but XDebug is not installed.")); } } final public function getWorkingCopy() { return $this->workingCopy; } final public function setWorkingCopy( ArcanistWorkingCopyIdentity $working_copy) { $this->workingCopy = $working_copy; return $this; } final public function getProjectRoot() { $working_copy = $this->getWorkingCopy(); if (!$working_copy) { throw new PhutilInvalidStateException('setWorkingCopy'); } return $working_copy->getProjectRoot(); } final public function setPaths(array $paths) { $this->paths = $paths; return $this; } final protected function getLink($method) { $base_uri = $this ->getWorkingCopy() ->getProjectConfig('phabricator.uri'); $uri = id(new PhutilURI($base_uri)) ->setPath("/diffusion/symbol/{$method}/") ->setQueryParam('context', get_class($this)) ->setQueryParam('jump', 'true') ->setQueryParam('lang', 'php'); return (string)$uri; } - public function setRenderer(ArcanistUnitRenderer $renderer) { + final public function setRenderer(ArcanistUnitRenderer $renderer) { $this->renderer = $renderer; return $this; } /** * Returns info about the caller function. * * @return map */ private static final function getCallerInfo() { $callee = array(); $caller = array(); $seen = false; foreach (array_slice(debug_backtrace(), 1) as $location) { $function = idx($location, 'function'); if (!$seen && preg_match('/^assert[A-Z]/', $function)) { $seen = true; $caller = $location; } else if ($seen && !preg_match('/^assert[A-Z]/', $function)) { $callee = $location; break; } } return array( 'file' => basename(idx($caller, 'file')), 'line' => idx($caller, 'line'), 'function' => idx($callee, 'function'), 'class' => idx($callee, 'class'), 'object' => idx($caller, 'object'), 'type' => idx($callee, 'type'), 'args' => idx($caller, 'args'), ); } /** * Fail an assertion which checks that some result is equal to a specific * value, like 'true' or 'false'. This prints a readable error message and * fails the current test. * * This method throws and does not return. * * @param string Human readable description of the expected value. * @param string The actual value. * @param string|null Optional assertion message. * @return void * @task internal */ private function failAssertionWithExpectedValue( $expect_description, $actual_result, $message) { $caller = self::getCallerInfo(); $file = $caller['file']; $line = $caller['line']; if ($message !== null) { $description = pht( "Assertion failed, expected '%s' (at %s:%d): %s", $expect_description, $file, $line, $message); } else { $description = pht( "Assertion failed, expected '%s' (at %s:%d).", $expect_description, $file, $line); } $actual_result = PhutilReadableSerializer::printableValue($actual_result); $header = pht('ACTUAL VALUE'); $output = $description."\n\n".$header."\n".$actual_result; $this->failTest($output); throw new PhutilTestTerminatedException($output); } }