Version: current

      1 <?php
      2 /**
      3  * @package sapphire
      4  * @subpackage testing
      5  */
      6
      7 // Check that PHPUnit is installed
      8 function hasPhpUnit() {
      9     $paths = explode(PATH_SEPARATOR, ini_get('include_path'));
     10     foreach($paths as $path) {
     11         if(substr($path,-1) == DIRECTORY_SEPARATOR) $path = substr($path,0,-1);
     12         if(@file_exists("$path/PHPUnit/Framework.php")) return true;
     13     }
     14     return false;
     15 }
     16
     17 /**
     18  */
     19 if(hasPhpUnit()) {
     20 require_once 'PHPUnit/Framework.php';
     21 require_once 'PHPUnit/Util/Report.php';
     22 require_once 'PHPUnit/TextUI/TestRunner.php';
     23 }
     24
     25 /**
     26  * Controller that executes PHPUnit tests.
     27  *
     28  * <h2>URL Options</h2>
     29  * - SkipTests: A comma-separated list of test classes to skip (useful when running dev/tests/all)
     30  *
     31  * See {@link browse()} output for generic usage instructions.
     32  *
     33  * @package sapphire
     34  * @subpackage testing
     35  */
     36 class TestRunner extends Controller {
     37     /** @ignore */
     38     private static $default_reporter;
     39
     40     static $url_handlers = array(
     41         '' => 'browse',
     42         'coverage/module/$ModuleName' => 'coverageModule',
     43         'coverage/$TestCase!' => 'coverageOnly',
     44         'coverage' => 'coverageAll',
     45         'startsession' => 'startsession',
     46         'endsession' => 'endsession',
     47         'cleanupdb' => 'cleanupdb',
     48         'module/$ModuleName' => 'module',
     49         'all' => 'all',
     50         'build' => 'build',
     51         '$TestCase' => 'only',
     52     );
     53
     54     /**
     55      * Override the default reporter with a custom configured subclass.
     56      *
     57      * @param string $reporter
     58      */
     59     static function set_reporter($reporter) {
     60         if (is_string($reporter)) $reporter = new $reporter;
     61         self::$default_reporter = $reporter;
     62     }
     63
     64     function init() {
     65         parent::init();
     66
     67         $canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN"));
     68         if(!$canAccess) return Security::permissionFailure($this);
     69
     70         if (!self::$default_reporter) self::set_reporter(Director::is_cli() ? 'CliDebugView' : 'DebugView');
     71
     72         if(!hasPhpUnit()) {
     73             die("Please install PHPUnit using pear");
     74         }
     75     }
     76
     77     public function Link() {
     78         return Controller::join_links(Director::absoluteBaseURL(), 'dev/tests/');
     79     }
     80
     81     /**
     82      * Run test classes that should be run with every commit.
     83      * Currently excludes PhpSyntaxTest
     84      */
     85     function all($coverage = false) {
     86         ManifestBuilder::load_test_manifest();
     87         $tests = ClassInfo::subclassesFor('SapphireTest');
     88         array_shift($tests);
     89         unset($tests['FunctionalTest']);
     90
     91         // Remove tests that don't need to be executed every time
     92         unset($tests['PhpSyntaxTest']);
     93
     94         foreach($tests as $class => $v) {
     95             $reflection = new ReflectionClass($class);
     96             if(!$reflection->isInstantiable()) unset($tests[$class]);
     97         }
     98
     99         $this->runTests($tests, $coverage);
    100     }
    101
    102     /**
    103      * Run test classes that should be run before build - i.e., everything possible.
    104      */
    105     function build() {
    106         ManifestBuilder::load_test_manifest();
    107         $tests = ClassInfo::subclassesFor('SapphireTest');
    108         array_shift($tests);
    109         unset($tests['FunctionalTest']);
    110         foreach($tests as $class => $v) {
    111             $reflection = new ReflectionClass($class);
    112             if(!$reflection->isInstantiable()) unset($tests[$class]);
    113         }
    114
    115         $this->runTests($tests);
    116
    117     }
    118
    119
    120     /**
    121      * Browse all enabled test cases in the environment
    122      */
    123     function browse() {
    124         ManifestBuilder::load_test_manifest();
    125         self::$default_reporter->writeHeader();
    126         self::$default_reporter->writeInfo('Available Tests', false);
    127         if(Director::is_cli()) {
    128             $tests = ClassInfo::subclassesFor('SapphireTest');
    129             $relativeLink = Director::makeRelative($this->Link());
    130             echo "sake {$relativeLink}all: Run all " . count($tests) . " tests\n";
    131             echo "sake {$relativeLink}coverage: Runs all tests and make test coverage report\n";
    132             echo "sake {$relativeLink}module/<modulename>: Runs all tests in a module folder\n";
    133             foreach ($tests as $test) {
    134                 echo "sake {$relativeLink}$test: Run $test\n";
    135             }
    136         } else {
    137             echo '<div class="trace">';
    138             $tests = ClassInfo::subclassesFor('SapphireTest');
    139             asort($tests);
    140             echo "<h3><a href=\"" . $this->Link() . "all\">Run all " . count($tests) . " tests</a></h3>";
    141             echo "<h3><a href=\"" . $this->Link() . "coverage\">Runs all tests and make test coverage report</a></h3>";
    142             echo "<hr />";
    143             foreach ($tests as $test) {
    144                 echo "<h3><a href=\"" . $this->Link() . "$test\">Run $test</a></h3>";
    145             }
    146             echo '</div>';
    147         }
    148
    149         self::$default_reporter->writeFooter();
    150     }
    151
    152     /**
    153      * Run a coverage test across all modules
    154      */
    155     function coverageAll() {
    156         ManifestBuilder::load_all_classes();
    157         $this->all(true);
    158     }
    159
    160     /**
    161      * Run only a single coverage test class or a comma-separated list of tests
    162      */
    163     function coverageOnly($request) {
    164         $this->only($request, true);
    165     }
    166
    167     /**
    168      * Run coverage tests for one or more "modules".
    169      * A module is generally a toplevel folder, e.g. "mysite" or "sapphire".
    170      */
    171     function coverageModule($request) {
    172         $this->module($request, true);
    173     }
    174
    175     function cleanupdb() {
    176         SapphireTest::delete_all_temp_dbs();
    177     }
    178
    179     /**
    180      * Run only a single test class or a comma-separated list of tests
    181      */
    182     function only($request, $coverage = false) {
    183         ManifestBuilder::load_test_manifest();
    184         if($request->param('TestCase') == 'all') {
    185             $this->all();
    186         } else {
    187             $classNames = explode(',', $request->param('TestCase'));
    188             foreach($classNames as $className) {
    189                 if(!class_exists($className) || !is_subclass_of($className, 'SapphireTest')) {
    190                     user_error("TestRunner::only(): Invalid TestCase '$className', cannot find matching class", E_USER_ERROR);
    191                 }
    192             }
    193
    194             $this->runTests($classNames, $coverage);
    195         }
    196     }
    197
    198     /**
    199      * Run tests for one or more "modules".
    200      * A module is generally a toplevel folder, e.g. "mysite" or "sapphire".
    201      */
    202     function module($request, $coverage = false) {
    203         ManifestBuilder::load_test_manifest();
    204         $classNames = array();
    205         $moduleNames = explode(',', $request->param('ModuleName'));
    206         foreach($moduleNames as $moduleName) {
    207             $classesForModule = ClassInfo::classes_for_folder($moduleName);
    208             if($classesForModule) foreach($classesForModule as $class) {
    209                 if(class_exists($class) && is_subclass_of($class, 'SapphireTest')) {
    210                     $classNames[] = $class;
    211                 }
    212             }
    213         }
    214
    215         $this->runTests($classNames, $coverage);
    216     }
    217
    218     /**
    219      * @param array $classList
    220      * @param boolean $coverage
    221      */
    222     function runTests($classList, $coverage = false) {
    223         $startTime = microtime(true);
    224
    225         // XDEBUG seem to cause problems with test execution :-(
    226         if(function_exists('xdebug_disable')) xdebug_disable();
    227
    228         ini_set('max_execution_time', 0);
    229
    230         $this->setUp();
    231
    232         // Optionally skip certain tests
    233         $skipTests = array();
    234         if($this->request->getVar('SkipTests')) {
    235             $skipTests = explode(',', $this->request->getVar('SkipTests'));
    236         }
    237         $classList = array_diff($classList, $skipTests);
    238
    239         // run tests before outputting anything to the client
    240         $suite = new PHPUnit_Framework_TestSuite();
    241         natcasesort($classList);
    242         foreach($classList as $className) {
    243             // Ensure that the autoloader pulls in the test class, as PHPUnit won't know how to do this.
    244             class_exists($className);
    245             $suite->addTest(new SapphireTestSuite($className));
    246         }
    247
    248         // Remove the error handler so that PHPUnit can add its own
    249         restore_error_handler();
    250
    251         /*, array("reportDirectory" => "/Users/sminnee/phpunit-report")*/
    252         if(Director::is_cli()) $reporter = new CliTestReporter();
    253         else $reporter = new SapphireTestReporter();
    254
    255         self::$default_reporter->writeHeader("Sapphire Test Runner");
    256         if (count($classList) > 1) {
    257             self::$default_reporter->writeInfo("All Tests", "Running test cases: ",implode(", ", $classList));
    258         } else {
    259             self::$default_reporter->writeInfo($classList[0], "");
    260         }
    261
    262         $results = new PHPUnit_Framework_TestResult();
    263         $results->addListener($reporter);
    264
    265         if($coverage === true) {
    266             $results->collectCodeCoverageInformation(true);
    267             $suite->run($results);
    268
    269             if(!file_exists(ASSETS_PATH . '/coverage-report')) mkdir(ASSETS_PATH . '/coverage-report');
    270             PHPUnit_Util_Report::render($results, ASSETS_PATH . '/coverage-report/');
    271             $coverageApp = ASSETS_PATH . '/coverage-report/' . preg_replace('/[^A-Za-z0-9]/','_',preg_replace('/(\/$)|(^\/)/','',Director::baseFolder())) . '.html';
    272             $coverageTemplates = ASSETS_PATH . '/coverage-report/' . preg_replace('/[^A-Za-z0-9]/','_',preg_replace('/(\/$)|(^\/)/','',realpath(TEMP_FOLDER))) . '.html';
    273             echo "<p>Coverage reports available here:<ul>
    274                 <li><a href=\"$coverageApp\">Coverage report of the application</a></li>
    275                 <li><a href=\"$coverageTemplates\">Coverage report of the templates</a></li>
    276             </ul>";
    277         } else {
    278             $suite->run($results);
    279         }
    280
    281         if(!Director::is_cli()) echo '<div class="trace">';
    282         $reporter->writeResults();
    283
    284         $endTime = microtime(true);
    285         if(Director::is_cli()) echo "\n\nTotal time: " . round($endTime-$startTime,3) . " seconds\n";
    286         else echo "<p>Total time: " . round($endTime-$startTime,3) . " seconds</p>\n";
    287
    288         if(!Director::is_cli()) echo '</div>';
    289
    290         // Put the error handlers back
    291         Debug::loadErrorHandlers();
    292
    293         if(!Director::is_cli()) self::$default_reporter->writeFooter();
    294
    295         $this->tearDown();
    296
    297         // Todo: we should figure out how to pass this data back through Director more cleanly
    298         if(Director::is_cli() && ($results->failureCount() + $results->errorCount()) > 0) exit(2);
    299     }
    300
    301     /**
    302      * Start a test session.
    303      * Usage: visit dev/tests/startsession?fixture=(fixturefile).  A test database will be constructed, and your browser session will be amended
    304      * to use this database.  This can only be run on dev and test sites.
    305      */
    306     function startsession() {
    307         if(!Director::isLive()) {
    308             if(SapphireTest::using_temp_db()) {
    309                 $endLink = Director::baseURL() . "/dev/tests/endsession";
    310                 return "<p><a id=\"end-session\" href=\"$endLink\">You're in the middle of a test session; click here to end it.</a></p>";
    311
    312             } else if(!isset($_GET['fixture'])) {
    313                 $me = Director::baseURL() . "/dev/tests/startsession";
    314                 return <<<HTML
    315 <form action="$me">
    316     <p>Enter a fixture file name to start a new test session.  Don't forget to visit dev/tests/endsession when you're done!</p>
    317     <p>Fixture file (leave blank to start with default set-up): <input id="fixture-file" name="fixture" /></p>
    318     <input type="hidden" name="flush" value="1">
    319     <p><input id="start-session" value="Start test session" type="submit" /></p>
    320 </form>
    321 HTML;
    322             } else {
    323                 $fixtureFile = $_GET['fixture'];
    324
    325                 if($fixtureFile) {
    326                     // Validate fixture file
    327                     $realFile = realpath(BASE_PATH.'/'.$fixtureFile);
    328                     $baseDir = realpath(Director::baseFolder());
    329                     if(!$realFile || !file_exists($realFile)) {
    330                         return "<p>Fixture file doesn't exist</p>";
    331                     } else if(substr($realFile,0,strlen($baseDir)) != $baseDir) {
    332                         return "<p>Fixture file must be inside $baseDir</p>";
    333                     } else if(substr($realFile,-4) != '.yml') {
    334                         return "<p>Fixture file must be a .yml file</p>";
    335                     } else if(!preg_match('/^([^\/.][^\/]+)\/tests\//', $fixtureFile)) {
    336                         return "<p>Fixture file must be inside the tests subfolder of one of your modules.</p>";
    337                     }
    338                 }
    339
    340                 $dbname = SapphireTest::create_temp_db();
    341
    342                 DB::set_alternative_database_name($dbname);
    343
    344                 // Fixture
    345                 if($fixtureFile) {
    346                     $fixture = new YamlFixture($fixtureFile);
    347                     $fixture->saveIntoDatabase();
    348
    349                 // If no fixture, then use defaults
    350                 } else {
    351                     $dataClasses = ClassInfo::subclassesFor('DataObject');
    352                     array_shift($dataClasses);
    353                     foreach($dataClasses as $dataClass) singleton($dataClass)->requireDefaultRecords();
    354                 }
    355
    356                 return "<p>Started testing session with fixture '$fixtureFile'.  Time to start testing; where would you like to start?</p>
    357                     <ul>
    358                         <li><a id=\"home-link\" href=\"" .Director::baseURL() . "\">Homepage - published site</a></li>
    359                         <li><a id=\"draft-link\" href=\"" .Director::baseURL() . "?stage=Stage\">Homepage - draft site</a></li>
    360                         <li><a id=\"admin-link\" href=\"" .Director::baseURL() . "admin/\">CMS Admin</a></li>
    361                         <li><a id=\"endsession-link\" href=\"" .Director::baseURL() . "dev/tests/endsession\">End your test session</a></li>
    362                     </ul>";
    363             }
    364
    365         } else {
    366             return "<p>startession can only be used on dev and test sites</p>";
    367         }
    368     }
    369
    370     function endsession() {
    371         SapphireTest::kill_temp_db();
    372         DB::set_alternative_database_name(null);
    373
    374         return "<p>Test session ended.</p>
    375             <ul>
    376                 <li><a id=\"home-link\" href=\"" .Director::baseURL() . "\">Return to your site</a></li>
    377                 <li><a id=\"startsession-link\" href=\"" .Director::baseURL() . "dev/tests/startsession\">Start a new test session</a></li>
    378             </ul>";
    379     }
    380
    381     function setUp() {
    382         // The first DB test will sort out the DB, we don't have to
    383         SSViewer::flush_template_cache();
    384     }
    385
    386     function tearDown() {
    387         SapphireTest::kill_temp_db();
    388         DB::set_alternative_database_name(null);
    389     }
    390 }
    391
    392 // This class is here to help with documentation.
    393 if(!hasPhpUnit()) {
    394     /**
    395      * PHPUnit is a testing framework that can be installed using PEAR.
    396      * It's not bundled with Sapphire, you will need to install it yourself.
    397      *
    398      * @package sapphire
    399      * @subpackage testing
    400      */
    401     class PHPUnit_Framework_TestCase {
    402
    403     }
    404 }
    405

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -