URL Options * - SkipTests: A comma-separated list of test classes to skip (useful when running dev/tests/all) * * See {@link browse()} output for generic usage instructions. * * @package sapphire * @subpackage testing */ class TestRunner extends Controller { /** @ignore */ private static $default_reporter; static $url_handlers = array( '' => 'browse', 'coverage' => 'coverage', 'startsession' => 'startsession', 'endsession' => 'endsession', 'cleanupdb' => 'cleanupdb', 'module/$ModuleName' => 'module', '$TestCase' => 'only', ); /** * Override the default reporter with a custom configured subclass. * * @param string $reporter */ static function set_reporter($reporter) { if (is_string($reporter)) $reporter = new $reporter; self::$default_reporter = $reporter; } function init() { parent::init(); $canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN")); if(!$canAccess) return Security::permissionFailure($this); ManifestBuilder::load_test_manifest(); if (!self::$default_reporter) self::set_reporter(Director::is_cli() ? 'CliDebugView' : 'DebugView'); if(!hasPhpUnit()) { die("Please install PHPUnit using pear"); } } public function Link() { return Controller::join_links(Director::absoluteBaseURL(), 'dev/tests/'); } /** * Run all test classes */ function all() { $tests = ClassInfo::subclassesFor('SapphireTest'); array_shift($tests); unset($tests['FunctionalTest']); foreach($tests as $class => $v) { $reflection = new ReflectionClass($class); if(!$reflection->isInstantiable()) unset($tests[$class]); } $this->runTests($tests); } /** * Browse all enabled test cases in the environment */ function browse() { self::$default_reporter->writeHeader(); self::$default_reporter->writeInfo('Available Tests', false); if(Director::is_cli()) { $tests = ClassInfo::subclassesFor('SapphireTest'); $relativeLink = Director::makeRelative($this->Link()); echo "sake {$relativeLink}all: Run all " . count($tests) . " tests\n"; echo "sake {$relativeLink}coverage: Runs all tests and make test coverage report\n"; echo "sake {$relativeLink}module/: Runs all tests in a module folder\n"; foreach ($tests as $test) { echo "sake {$relativeLink}$test: Run $test\n"; } } else { echo '
'; $tests = ClassInfo::subclassesFor('SapphireTest'); asort($tests); echo "

Link() . "all\">Run all " . count($tests) . " tests

"; echo "

Link() . "coverage\">Runs all tests and make test coverage report

"; echo "
"; foreach ($tests as $test) { echo "

Link() . "$test\">Run $test

"; } echo '
'; } self::$default_reporter->writeFooter(); } function coverage() { $tests = ClassInfo::subclassesFor('SapphireTest'); array_shift($tests); unset($tests['FunctionalTest']); $this->runTests($tests, true); } function cleanupdb() { SapphireTest::delete_all_temp_dbs(); } /** * Run only a single test class or a comma-separated list of tests */ function only($request) { if($request->param('TestCase') == 'all') { $this->all(); } else { $classNames = explode(',', $request->param('TestCase')); foreach($classNames as $className) { if(!class_exists($className) || !is_subclass_of($className, 'SapphireTest')) { user_error("TestRunner::only(): Invalid TestCase '$className', cannot find matching class", E_USER_ERROR); } } $this->runTests($classNames); } } /** * Run tests for one or more "modules". * A module is generally a toplevel folder, e.g. "mysite" or "sapphire". */ function module($request) { $classNames = array(); $moduleNames = explode(',', $request->param('ModuleName')); foreach($moduleNames as $moduleName) { $classesForModule = ClassInfo::classes_for_folder($moduleName); if($classesForModule) foreach($classesForModule as $class) { if(class_exists($class) && is_subclass_of($class, 'SapphireTest')) { $classNames[] = $class; } } } $this->runTests($classNames); } /** * @param array $classList * @param boolean $coverage */ function runTests($classList, $coverage = false) { $startTime = microtime(true); // XDEBUG seem to cause problems with test execution :-( if(function_exists('xdebug_disable')) xdebug_disable(); ini_set('max_execution_time', 0); $this->setUp(); // Optionally skip certain tests $skipTests = array(); if($this->request->getVar('SkipTests')) { $skipTests = explode(',', $this->request->getVar('SkipTests')); } $classList = array_diff($classList, $skipTests); // run tests before outputting anything to the client $suite = new PHPUnit_Framework_TestSuite(); natcasesort($classList); foreach($classList as $className) { // Ensure that the autoloader pulls in the test class, as PHPUnit won't know how to do this. class_exists($className); $suite->addTest(new SapphireTestSuite($className)); } // Remove the error handler so that PHPUnit can add its own restore_error_handler(); /*, array("reportDirectory" => "/Users/sminnee/phpunit-report")*/ if(Director::is_cli()) $reporter = new CliTestReporter(); else $reporter = new SapphireTestReporter(); self::$default_reporter->writeHeader("Sapphire Test Runner"); if (count($classList) > 1) { self::$default_reporter->writeInfo("All Tests", "Running test cases: " . implode(",", $classList)); } else { self::$default_reporter->writeInfo($classList[0], ""); } $results = new PHPUnit_Framework_TestResult(); $results->addListener($reporter); if($coverage) { $results->collectCodeCoverageInformation(true); $suite->run($results); if(!file_exists(ASSETS_PATH . '/coverage-report')) mkdir(ASSETS_PATH . '/coverage-report'); PHPUnit_Util_Report::render($results, ASSETS_PATH . '/coverage-report/'); $coverageApp = ASSETS_PATH . '/coverage-report/' . preg_replace('/[^A-Za-z0-9]/','_',preg_replace('/(\/$)|(^\/)/','',Director::baseFolder())) . '.html'; $coverageTemplates = ASSETS_PATH . '/coverage-report/' . preg_replace('/[^A-Za-z0-9]/','_',preg_replace('/(\/$)|(^\/)/','',realpath(TEMP_FOLDER))) . '.html'; echo "

Coverage reports available here:

"; } else { $suite->run($results); } if(!Director::is_cli()) echo '
'; $reporter->writeResults(); $endTime = microtime(true); if(Director::is_cli()) echo "\n\nTotal time: " . round($endTime-$startTime,3) . " seconds\n"; else echo "

Total time: " . round($endTime-$startTime,3) . " seconds

\n"; if(!Director::is_cli()) echo '
'; // Put the error handlers back Debug::loadErrorHandlers(); if(!Director::is_cli()) self::$default_reporter->writeFooter(); $this->tearDown(); // Todo: we should figure out how to pass this data back through Director more cleanly if(Director::is_cli() && ($results->failureCount() + $results->errorCount()) > 0) exit(2); } /** * Start a test session. * Usage: visit dev/tests/startsession?fixture=(fixturefile). A test database will be constructed, and your browser session will be amended * to use this database. This can only be run on dev and test sites. */ function startsession() { if(!Director::isLive()) { if(SapphireTest::using_temp_db()) { $endLink = Director::baseURL() . "/dev/tests/endsession"; return "

You're in the middle of a test session; click here to end it.

"; } else if(!isset($_GET['fixture'])) { $me = Director::baseURL() . "/dev/tests/startsession"; return <<

Enter a fixture file name to start a new test session. Don't forget to visit dev/tests/endsession when you're done!

Fixture file:

HTML; } else { $fixtureFile = $_GET['fixture']; // Validate fixture file $realFile = realpath('../' . $fixtureFile); $baseDir = realpath(Director::baseFolder()); if(!$realFile || !file_exists($realFile)) { return "

Fixture file doesn't exist

"; } else if(substr($realFile,0,strlen($baseDir)) != $baseDir) { return "

Fixture file must be inside $baseDir

"; } else if(substr($realFile,-4) != '.yml') { return "

Fixture file must be a .yml file

"; } else if(!preg_match('/^([^\/.][^\/]+)\/tests\//', $fixtureFile)) { return "

Fixture file must be inside the tests subfolder of one of your modules.

"; } $dbname = SapphireTest::create_temp_db(); DB::set_alternative_database_name($dbname); $fixture = new YamlFixture($_GET['fixture']); $fixture->saveIntoDatabase(); return "

Started testing session with fixture '$fixtureFile'. Time to start testing; where would you like to start?

"; } } else { return "

startession can only be used on dev and test sites

"; } } function endsession() { SapphireTest::kill_temp_db(); DB::set_alternative_database_name(null); return "

Test session ended.

"; } function setUp() { SapphireTest::create_temp_db(); SSViewer::flush_template_cache(); } function tearDown() { SapphireTest::kill_temp_db(); DB::set_alternative_database_name(null); } } // This class is here to help with documentation. if(!hasPhpUnit()) { /** * PHPUnit is a testing framework that can be installed using PEAR. * It's not bundled with Sapphire, you will need to install it yourself. * * @package sapphire * @subpackage testing */ class PHPUnit_Framework_TestCase { } }