Location: PHPKode > projects > SolarPHP > solar-system-1.1.1/solar/source/solar/Solar/Test/Suite.php
<?php
/**
 * 
 * Class for running suites of unit tests.
 * 
 * Expects a directory structure like this ...
 * 
 *     Test/
 *       Solar.php      -- Test_Solar
 *       Solar/         
 *         Base.php     -- Test_Solar_Base
 *         Uri.php      -- Test_Solar_Uri
 *         Uri/     
 *           Action.php -- Test_Solar_Uri_Action
 * 
 * @category Solar
 * 
 * @package Solar_Test
 * 
 * @author Paul M. Jones <hide@address.com>
 * 
 * @license http://opensource.org/licenses/bsd-license.php BSD
 * 
 * @version $Id: Suite.php 4533 2010-04-23 16:35:15Z pmjones $
 * 
 */
class Solar_Test_Suite extends Solar_Base
{
    /**
     * 
     * Default configuration values.
     * 
     * @config dependency log A Solar_Log dependency for logging test results.
     * 
     * @config mixed test_config The config param to use for Solar::start() 
     * when running tests in the separate PHP environment.
     * 
     * @config bool verbose Whether or not to show verbose output.
     * 
     * @config bool stop_on_fail Stop running tests at the first failure.
     * 
     * @var array
     * 
     */
    protected $_Solar_Test_Suite = array(
        'log'           => array(
            'adapter'   => 'Solar_Log_Adapter_Echo',
            'format'    => '%m',
        ),
        'test_config'   => null,
        'verbose'       => null,
        'stop_on_fail'  => false,
    );
    
    /**
     * 
     * The directory where tests are located.
     * 
     * @var string
     * 
     */
    protected $_dir;
    
    /**
     * 
     * The log of pass/skip/todo/fail messages.
     * 
     * @var array
     * 
     */
    protected $_info;
    
    /**
     * 
     * The test classes (and their methods) to run.
     * 
     * In the form of array($class => array($method1, $method2, ...)).
     * 
     * @var array
     * 
     */
    protected $_tests;
    
    /**
     * 
     * A Solar_Log instance.
     * 
     * @var Solar_Log
     * 
     */
    protected $_log;
    
    /**
     * 
     * A Solar_Debug_Var instance.
     * 
     * @var Solar_Debug_Var
     * 
     */
    protected $_var;
    
    /**
     * 
     * When in 'verbose' mode, all diagnostic output will be displayed.
     * 
     * @var bool
     * 
     */
    protected $_verbose = false;
    
    /**
     * 
     * The result of the last test ('fail', 'todo', 'skip', or 'pass').
     * 
     * @var string
     * 
     */
    protected $_test_result = null;
    
    /**
     * 
     * Post-construction tasks to complete object construction.
     * 
     * @return void
     * 
     */
    protected function _postConstruct()
    {
        parent::_postConstruct();
        
        // verbosity
        if ($this->_config['verbose'] !== null) {
            $this->setVerbose($this->_config['verbose']);
        }
        
        // keep a Solar_Debug_Var object around for later
        $this->_var = Solar::factory('Solar_Debug_Var');
        
        // set the include directory
        $this->_dir = Solar::$system . "/include";
        
        // logging
        $this->_log = Solar::dependency(
            'Solar_Log',
            $this->_config['log']
        );
    }
    
    /**
     * 
     * Turns 'verbose' mode on and off.
     * 
     * @param bool $flag True for verbose, false for not.
     * 
     * @return void
     * 
     */
    public function setVerbose($flag)
    {
        $this->_verbose = (bool) $flag;
    }
    
    /**
     * 
     * Creates a new Solar_Php instance with some default settings.
     * 
     * @return Solar_Php
     * 
     */
    protected function _newPhp()
    {
        $php = Solar::factory('Solar_Php');
        $php->setEcho($this->_verbose)
            ->setIniVal('include_path', $this->_dir)
            ->setIniVal('error_reporting', E_ALL | E_STRICT)
            ->setIniVal('display_errors', true)
            ->setIniVal('html_errors', false)
            ->setIniVal('log_errors', true)
            ->setIniVal('error_log', '/tmp/php_errors.log');
        
        return $php;
    }
    
    /**
     * 
     * Finds tests and loads them into the plan.
     * 
     * @param string $class Start with this test class; e.g, "Test_Foo".
     * 
     * @param string $method Load only this test method; e.g, "testBar".
     * 
     * @param bool $only Load only the named class, or class and method, 
     * instead of descending into sub-tests.
     * 
     * @return void
     * 
     */
    public function loadTests($class = null, $method = null, $only = null)
    {
        // if no class, at least pass a string zero
        if (! $class) {
            $method = '0';
        }
        
        // if no method, at least pass a string zero
        if (! $method) {
            $method = '0';
        }
        
        // the load-tests file to run
        $file = Solar_Class::dir($this) . '/load-tests.php';
        
        // find the tests using a separate php process
        $php = $this->_newPhp();
        $php->addArgv($this->_dir . '/')
            ->addArgv($class)
            ->addArgv($method)
            ->addArgv((int) $only)
            ->run($file);
        
        // how'd it go?
        $exit_code = $php->getExitCode();
        if ($exit_code != Solar_Test::EXIT_PASS) {
            throw $this->_exception('ERR_LOAD_TESTS', array(
                'class' => $class,
                'method' => ($method ? $method : '*'),
                'exit_code' => $exit_code,
                'last_line' => $php->getLastLine(),
            ));
        }
        
        // retain the list of found tests
        $data = unserialize($php->getOutput());
        $this->_info['plan'] = $data['plan'];
        $this->_tests = $data['tests'];
    }
    
    /**
     * 
     * Runs the test suite (or the sub-test series) and logs as it goes.
     * 
     * Returns an array of statistics with these keys ...
     * 
     * `plan`
     * : (int) The planned number of tests.
     * 
     * `done`
     * : (int) The number of tests actually done.
     * 
     * `time`
     * : (int) The time, in seconds, it took to run all tests.
     * 
     * `pass`
     * : (array) Log of tests that passed.
     * 
     * `skip`
     * : (array) Log of tests that were skipped.
     * 
     * `todo`
     * : (array) Log of tests that are incomplete.
     * 
     * `fail`
     * : (array) Log of tests that failed.
     * 
     * @param string $class Run only this test class series. If empty, will
     * run all test classes.
     * 
     * @param string $method Run only this test method in the test class
     * series.  If empty, will run all test methods.
     * 
     * @param bool $only Run only the name class, or named class and method, 
     * instead of descending into sub-tests.
     * 
     * @return array A statistics array.
     * 
     */
    public function run($class = null, $method = false, $only = false)
    {
        // prepare
        $this->_prepare($class, $method, $only);
        
        // is there a plan?
        if (! $this->_info['plan']) {
            throw $this->_exception('ERR_FIND_TESTS', array(
                'class' => $class,
                'method' => ($method ? $method : '*'),
            ));
        }
        
        // show the plan
        $this->_log("1..{$this->_info['plan']}");
        
        // set up the PHP environment
        $php = $this->_newPhp();
        
        // the time before running the tests
        $time = time();
        
        // run the test cases
        foreach ($this->_tests as $class => $methods) {
            
            // set the config for this test case. we do this on each class
            // because there may be multiple vendors involved.
            $config = $this->_fetchTestCaseConfig($class);
            $this->_log("# $class config: $config");
            $php->setSolarConfig($config);
            
            // try constructing the test case once
            $this->_testConstruct($php, $class);
            
            // was it a stop-on-fail?
            $stop = $this->_test_result == 'fail'
                 && $this->_config['stop_on_fail'];
            if ($stop) {
                // stop testing entirely
                break;
            }
            
            // was it a fail/todo/skip?
            $continue = in_array($this->_test_result, array(
                'fail', 'todo', 'skip',
            ));
            if ($continue) {
                continue;
            }
            
            // run each test method
            foreach ($methods as $method) {
                // run the method
                $this->_testMethod($php, $class, $method);
                // did it fail?
                $fail = $this->_test_result == 'fail';
                // should we stop on failure?
                $stop = $this->_config['stop_on_fail'];
                if ($fail && $stop) {
                    // stop testing entirely
                    break 2;
                }
            }
        }
        
        // the test time duration
        $this->_info['time'] = time() - $time;
        
        // report, then return the run information
        $this->_report();
        return $this->_info;
    }
    
    /**
     * 
     * Finds the config file for a test case.
     * 
     * The order of precedence is:
     * 
     * 1. Use the value of --test-config when not empty.
     * 
     * 2. Look for `$system/config/test/Vendor.config.php` and use that if it
     *    exists.
     * 
     * 3. Look for `$system/source/vendor/tests/config.php` and use that if it
     *    exists.
     * 
     * 4. No config for the test case.
     * 
     * @param string $class The test case class to find configs for.
     * 
     * @return string The config file location for the test case.
     * 
     */
    protected function _fetchTestCaseConfig($class)
    {
        // explicit test-config
        if ($this->_config['test_config']) {
            return $this->_config['test_config'];
        }
        
        // convenience var
        $system = Solar::$system;
        
        // strip the 'Test_' prefix, then get the vendor name
        $vendor = Solar_Class::vendor(substr($class, 5));
        
        // the dashes version
        $dashes = Solar_Registry::get('inflect')->camelToDashes($vendor);
        
        // look for a config/run-tests/vendor.php file
        $path = "$system/config/run-tests/$dashes.php";
        $file = Solar_File::exists($path);
        if ($file) {
            return $file;
        }
        
        // look for a source/vendor/config/run-tests.php file
        $path = "$system/source/$dashes/config/run-tests.php";
        $file = Solar_File::exists($path);
        if ($file) {
            return $file;
        }
        
        // no test config
        return null;
    }
    
    /**
     * 
     * Test the construction of the test class to see if it works.
     * 
     * @param Solar_Php $php The PHP execution object.
     * 
     * @param string $class The test class for construction.
     * 
     * @return void
     * 
     */
    protected function _testConstruct($php, $class)
    {
        $this->_test_result = null;
        
        $file = Solar_Class::dir($this) . 'pre-test.php';
        
        $php->setArgv(array($class))
            ->runSolar($file);
        
        $exit = $php->getExitCode();
        
        if ($exit != Solar_Test::EXIT_PASS) {
            $this->_done($exit, $class, $php->getLastLine());
            $count = count($this->_tests[$class]);
            $type = $this->_test_result;
            $this->_log("# " . ucfirst($type) . ": $count test methods in $class");
            $k = $count - 1;
            for ($i = 0; $i < $k; $i ++) {
                $this->_info['done'] ++;
                $this->_info[$type]["{$class}::{$i}"] = array(null, "$type constructor");
            }
        }
    }
    
    /**
     * 
     * Run a single test method from the test class.
     * 
     * @param Solar_Php $php The PHP execution object.
     * 
     * @param string $class The test class.
     * 
     * @param string $method The test method.
     * 
     * @return void
     * 
     */
    protected function _testMethod($php, $class, $method)
    {
        $this->_test_result = null;
        
        $file = Solar_Class::dir($this) . 'run-test.php';
        
        $php->setArgv(array($class, $method))
            ->runSolar($file);
        
        $this->_done(
            $php->getExitCode(),
            "$class::$method",
            $php->getLastLine()
        );
    }
    
    /**
     * 
     * Prepares class properties for a test run.
     * 
     * @param string $class Only prepare tests for this class series.  Don't
     * include the 'Test_' prefix.  If empty, will run all test classes.
     * 
     * @param string $method When empty, recurse into subdirectories and run 
     * sub-test classes and methods.  When non-empty, run **only** this test 
     * method in the named test class; do not include the "test" prefix. 
     * Default null; ignored when $class is empty.
     * 
     * @param bool $only Load only the named class, or class and method, 
     * instead of descending into sub-tests.
     * 
     * @return void
     * 
     */
    protected function _prepare($class = null, $method = null, $only = false)
    {
        // reset
        $this->_info = array(
            'plan' => 0,
            'done' => 0,
            'time' => 0,
            'pass' => array(),
            'skip' => array(),
            'todo' => array(),
            'fail' => array(),
        );
        
        $this->loadTests($class, $method, $only);
    }
    
    /**
     * 
     * Generates a report from class properties.
     * 
     * @return void
     * 
     */
    protected function _report()
    {
        // report summary
        $done = $this->_info['done'];
        $plan = $this->_info['plan'];
        $time = $this->_info['time'];
        
        $this->_log("$done/$plan tests, $time seconds");
        
        $tmp = array();
        $show = array('fail', 'todo', 'skip', 'pass');
        foreach ($show as $type) {
            $count = count($this->_info[$type]);
            $tmp[] = "$count $type";
        }
        $this->_log(implode(', ', $tmp));
    }
    
    /**
     * 
     * Formats a test line, logs it, and saves the info.
     * 
     * @param int $exit Pass, skip, todo, or fail.
     * 
     * @param string $name The test name.
     * 
     * @param string $note Additional note about the test.
     * 
     * @param string $diag Diagnostics for the test.
     * 
     * @return void
     * 
     */
    protected function _done($exit, $name, $note = null, $diag = null)
    {
        $this->_info['done'] ++;
        
        $text = '';
        
        if (is_array($diag) || is_object($diag)) {
            $diag = $this->_var->dump($diag);
        }
        
        $diag = trim($diag);
        if ($diag) {
            $text = "$text\n# " . str_replace("\n", "\n# ", trim($diag));
        }
        
        if ($text) {
            $text .= "\n";
        }
        
        $num = $this->_info['done'];
        
        $note = ltrim($note, '# ');
        
        switch ($exit) {
        case Solar_Test::EXIT_FAIL:
            $type = 'fail';
            $text .= "not ok $num - $name # FAIL $note";
            break;
        
        case Solar_Test::EXIT_TODO:
            $type = 'todo';
            $text .= "not ok $num - $name # TODO $note";
            break;
        
        case Solar_Test::EXIT_SKIP:
            $type = 'skip';
            $text .= "ok $num - $name # SKIP $note";
            break;
        
        case Solar_Test::EXIT_PASS:
            $type = 'pass';
            $text .= "ok $num - $name";
            break;
        
        default:
            $type = 'fail';
            $text .= "not ok $num - $name # FAIL exit code '$exit'";
            break;
        }
        
        $this->_test_result = $type;
        $this->_log($text);
        $this->_info[$type][$name] = array($num, $note);
    }
    
    /**
     * 
     * Saves an event and message to the log.
     * 
     * @param string $event The log event type.
     * 
     * @param string $message The log message.
     * 
     * @return boolean True if saved, false if not, null if logging
     * not enabled.
     * 
     * @see Solar_Test_Suite::$_log
     * 
     * @see Solar_Log_Adapter::save()
     * 
     */
    protected function _log($message)
    {
        $this->_log->save(get_class($this), 'test', $message);
    }
}
Return current item: SolarPHP