<?php
    /**
     *	Base include file for SimpleTest
     *	@package	SimpleTest
     *	@subpackage	UnitTester
     *	@version	$Id: test_case.php 68 2009-07-31 18:23:01Z dlandau $
     */

    /**#@+
     * Includes SimpleTest files and defined the root constant
     * for dependent libraries.
     */
    require_once(dirname(__FILE__) . '/invoker.php');
    require_once(dirname(__FILE__) . '/errors.php');
    require_once(dirname(__FILE__) . '/compatibility.php');
    require_once(dirname(__FILE__) . '/scorer.php');
    require_once(dirname(__FILE__) . '/expectation.php');
    require_once(dirname(__FILE__) . '/dumper.php');
    require_once(dirname(__FILE__) . '/simpletest.php');
    if (version_compare(phpversion(), '5') >= 0) {
        require_once(dirname(__FILE__) . '/exceptions.php');
        require_once(dirname(__FILE__) . '/reflection_php5.php');
    } else {
        require_once(dirname(__FILE__) . '/reflection_php4.php');
    }
    if (! defined('SIMPLE_TEST')) {
        /** @ignore */
        define('SIMPLE_TEST', dirname(__FILE__) . DIRECTORY_SEPARATOR);
    }
    /**#@-*/

    /**
     *    Basic test case. This is the smallest unit of a test
     *    suite. It searches for
     *    all methods that start with the the string "test" and
     *    runs them. Working test cases extend this class.
	 *    @package		SimpleTest
	 *    @subpackage	UnitTester
     */
    class SimpleTestCase {
        var $_label = false;
        var $_reporter;
        var $_observers;
		var $_should_skip = false;

        /**
         *    Sets up the test with no display.
         *    @param string $label    If no test name is given then
         *                            the class name is used.
         *    @access public
         */
        function SimpleTestCase($label = false) {
            if ($label) {
                $this->_label = $label;
            }
        }

        /**
         *    Accessor for the test name for subclasses.
         *    @return string           Name of the test.
         *    @access public
         */
        function getLabel() {
            return $this->_label ? $this->_label : get_class($this);
        }

        /**
         *    This is a placeholder for skipping tests. In this
         *    method you place skipIf() and skipUnless() calls to
         *    set the skipping state.
         *    @access public
         */
        function skip() {
        }
		
        /**
         *    Will issue a message to the reporter and tell the test
         *    case to skip if the incoming flag is true.
         *    @param string $should_skip    Condition causing the tests to be skipped.
         *    @param string $message    	Text of skip condition.
         *    @access public
         */
        function skipIf($should_skip, $message = '%s') {
			if ($should_skip && ! $this->_should_skip) {
				$this->_should_skip = true;
				$message = sprintf($message, 'Skipping [' . get_class($this) . ']');
				$this->_reporter->paintSkip($message . $this->getAssertionLine());
			}
        }
		
        /**
         *    Will issue a message to the reporter and tell the test
         *    case to skip if the incoming flag is false.
         *    @param string $shouldnt_skip  Condition causing the tests to be run.
         *    @param string $message    	Text of skip condition.
         *    @access public
         */
        function skipUnless($shouldnt_skip, $message = false) {
			$this->skipIf(! $shouldnt_skip, $message);
        }
		
        /**
         *    Used to invoke the single tests.
         *    @return SimpleInvoker        Individual test runner.
         *    @access public
         */
        function &createInvoker() {
            $invoker = &new SimpleErrorTrappingInvoker(new SimpleInvoker($this));
            if (version_compare(phpversion(), '5') >= 0) {
                $invoker = &new SimpleExceptionTrappingInvoker($invoker);
            }
            return $invoker;
        }

        /**
         *    Uses reflection to run every method within itself
         *    starting with the string "test" unless a method
         *    is specified.
         *    @param SimpleReporter $reporter    Current test reporter.
         *    @return boolean                    True if all tests passed.
         *    @access public
         */
        function run(&$reporter) {
			$context = &SimpleTest::getContext();
			$context->setTest($this);
			$context->setReporter($reporter);
            $this->_reporter = &$reporter;
            $reporter->paintCaseStart($this->getLabel());
			$this->skip();
            if (! $this->_should_skip) {
                foreach ($this->getTests() as $method) {
                    if ($reporter->shouldInvoke($this->getLabel(), $method)) {
                        $invoker = &$this->_reporter->createInvoker($this->createInvoker());
                        $invoker->before($method);
                        $invoker->invoke($method);
                        $invoker->after($method);
                    }
                }
            }
            $reporter->paintCaseEnd($this->getLabel());
            unset($this->_reporter);
            return $reporter->getStatus();
        }

        /**
         *    Gets a list of test names. Normally that will
         *    be all internal methods that start with the
         *    name "test". This method should be overridden
         *    if you want a different rule.
         *    @return array        List of test names.
         *    @access public
         */
        function getTests() {
            $methods = array();
            foreach (get_class_methods(get_class($this)) as $method) {
                if ($this->_isTest($method)) {
                    $methods[] = $method;
                }
            }
            return $methods;
        }

        /**
         *    Tests to see if the method is a test that should
         *    be run. Currently any method that starts with 'test'
         *    is a candidate unless it is the constructor.
         *    @param string $method        Method name to try.
         *    @return boolean              True if test method.
         *    @access protected
         */
        function _isTest($method) {
            if (strtolower(substr($method, 0, 4)) == 'test') {
                return ! SimpleTestCompatibility::isA($this, strtolower($method));
            }
            return false;
        }

        /**
         *    Announces the start of the test.
         *    @param string $method    Test method just started.
         *    @access public
         */
        function before($method) {
            $this->_reporter->paintMethodStart($method);
            $this->_observers = array();
        }

        /**
         *    Sets up unit test wide variables at the start
         *    of each test method. To be overridden in
         *    actual user test cases.
         *    @access public
         */
        function setUp() {
        }

        /**
         *    Clears the data set in the setUp() method call.
         *    To be overridden by the user in actual user test cases.
         *    @access public
         */
        function tearDown() {
        }

        /**
         *    Announces the end of the test. Includes private clean up.
         *    @param string $method    Test method just finished.
         *    @access public
         */
        function after($method) {
            for ($i = 0; $i < count($this->_observers); $i++) {
                $this->_observers[$i]->atTestEnd($method, $this);
            }
            $this->_reporter->paintMethodEnd($method);
        }

        /**
         *    Sets up an observer for the test end.
         *    @param object $observer    Must have atTestEnd()
         *                               method.
         *    @access public
         */
        function tell(&$observer) {
            $this->_observers[] = &$observer;
        }

        /**
         *    @deprecated
         */
        function pass($message = "Pass") {
            if (! isset($this->_reporter)) {
                trigger_error('Can only make assertions within test methods');
            }
            $this->_reporter->paintPass(
                    $message . $this->getAssertionLine());
            return true;
        }

        /**
         *    Sends a fail event with a message.
         *    @param string $message        Message to send.
         *    @access public
         */
        function fail($message = "Fail") {
            if (! isset($this->_reporter)) {
                trigger_error('Can only make assertions within test methods');
            }
            $this->_reporter->paintFail(
                    $message . $this->getAssertionLine());
            return false;
        }

        /**
         *    Formats a PHP error and dispatches it to the
         *    reporter.
         *    @param integer $severity  PHP error code.
         *    @param string $message    Text of error.
         *    @param string $file       File error occoured in.
         *    @param integer $line      Line number of error.
         *    @access public
         */
        function error($severity, $message, $file, $line) {
            if (! isset($this->_reporter)) {
                trigger_error('Can only make assertions within test methods');
            }
            $this->_reporter->paintError(
                    "Unexpected PHP error [$message] severity [$severity] in [$file line $line]");
        }

        /**
         *    Formats an exception and dispatches it to the
         *    reporter.
         *    @param Exception $exception    Object thrown.
         *    @access public
         */
        function exception($exception) {
            $this->_reporter->paintException($exception);
        }

        /**
         *    @deprecated
         */
        function signal($type, &$payload) {
            if (! isset($this->_reporter)) {
                trigger_error('Can only make assertions within test methods');
            }
            $this->_reporter->paintSignal($type, $payload);
        }

        /**
         *    Runs an expectation directly, for extending the
         *    tests with new expectation classes.
         *    @param SimpleExpectation $expectation  Expectation subclass.
         *    @param mixed $compare               Value to compare.
         *    @param string $message                 Message to display.
         *    @return boolean                        True on pass
         *    @access public
         */
        function assert(&$expectation, $compare, $message = '%s') {
            if ($expectation->test($compare)) {
                return $this->pass(sprintf(
                        $message,
                        $expectation->overlayMessage($compare, $this->_reporter->getDumper())));
            } else {
                return $this->fail(sprintf(
                        $message,
                        $expectation->overlayMessage($compare, $this->_reporter->getDumper())));
            }
        }

        /**
         *	  @deprecated
         */
        function assertExpectation(&$expectation, $compare, $message = '%s') {
        	return $this->assert($expectation, $compare, $message);
        }

        /**
         *    Uses a stack trace to find the line of an assertion.
         *    @return string           Line number of first assert*
         *                             method embedded in format string.
         *    @access public
         */
        function getAssertionLine() {
            $trace = new SimpleStackTrace(array('assert', 'expect', 'pass', 'fail', 'skip'));
            return $trace->traceMethod();
        }

        /**
         *    Sends a formatted dump of a variable to the
         *    test suite for those emergency debugging
         *    situations.
         *    @param mixed $variable    Variable to display.
         *    @param string $message    Message to display.
         *    @return mixed             The original variable.
         *    @access public
         */
        function dump($variable, $message = false) {
            $dumper = $this->_reporter->getDumper();
            $formatted = $dumper->dump($variable);
            if ($message) {
                $formatted = $message . "\n" . $formatted;
            }
            $this->_reporter->paintFormattedMessage($formatted);
            return $variable;
        }

        /**
         *    @deprecated
         */
        function sendMessage($message) {
            $this->_reporter->PaintMessage($message);
        }

        /**
         *    Accessor for the number of subtests.
         *    @return integer           Number of test cases.
         *    @access public
         *    @static
         */
        function getSize() {
            return 1;
        }
    }

    /**
     *    This is a composite test class for combining
     *    test cases and other RunnableTest classes into
     *    a group test.
	 *    @package		SimpleTest
	 *    @subpackage	UnitTester
     */
    class TestSuite {
        var $_label;
        var $_test_cases;
        var $_old_track_errors;
        var $_xdebug_is_enabled;

        /**
         *    Sets the name of the test suite.
         *    @param string $label    Name sent at the start and end
         *                            of the test.
         *    @access public
         */
        function TestSuite($label = false) {
            $this->_label = $label ? $label : get_class($this);
            $this->_test_cases = array();
            $this->_old_track_errors = ini_get('track_errors');
            $this->_xdebug_is_enabled = function_exists('xdebug_is_enabled') ?
                    xdebug_is_enabled() : false;
        }

        /**
         *    Accessor for the test name for subclasses.
         *    @return string           Name of the test.
         *    @access public
         */
        function getLabel() {
            return $this->_label;
        }

        /**
         *    Adds a test into the suite. Can be either a group
         *    test or some other unit test.
         *    @param SimpleTestCase $test_case  Suite or individual test
         *                                      case implementing the
         *                                      runnable test interface.
         *    @access public
         */
        function addTestCase(&$test_case) {
            $this->_test_cases[] = &$test_case;
        }

        /**
         *    Adds a test into the suite by class name. The class will
         *    be instantiated as needed.
         *    @param SimpleTestCase $test_case  Suite or individual test
         *                                      case implementing the
         *                                      runnable test interface.
         *    @access public
         */
        function addTestClass($class) {
            if ($this->_getBaseTestCase($class) == 'testsuite' || $this->_getBaseTestCase($class) == 'grouptest') {
                $this->_test_cases[] = &new $class();
            } else {
                $this->_test_cases[] = $class;
            }
        }

        /**
         *    Builds a group test from a library of test cases.
         *    The new group is composed into this one.
         *    @param string $test_file        File name of library with
         *                                    test case classes.
         *    @access public
         */
        function addTestFile($test_file) {
            $existing_classes = get_declared_classes();
            if ($error = $this->_requireWithError($test_file)) {
                $this->addTestCase(new BadTestSuite($test_file, $error));
                return;
            }
            $classes = $this->_selectRunnableTests($existing_classes, get_declared_classes());
            if (count($classes) == 0) {
                $this->addTestCase(new BadTestSuite($test_file, "No runnable test cases in [$test_file]"));
                return;
            }
            $group = &$this->_createGroupFromClasses($test_file, $classes);
            $this->addTestCase($group);
        }

        /**
         *    Requires a source file recording any syntax errors.
         *    @param string $file        File name to require in.
         *    @return string/boolean     An error message on failure or false
         *                               if no errors.
         *    @access private
         */
        function _requireWithError($file) {
            $this->_enableErrorReporting();
            global $CFG; // Moodle patch for $CFG global in unit test files
            include($file);
            $error = isset($php_errormsg) ? $php_errormsg : false;
            $this->_disableErrorReporting();
            $self_inflicted_errors = array(
                    '/Assigning the return value of new by reference/i',
                    '/var: Deprecated/i',
					'/Non-static method/i');
            foreach ($self_inflicted_errors as $pattern) {
				if (preg_match($pattern, $error)) {
					return false;
				}
			}
            return $error;
        }

        /**
         *    Sets up detection of parse errors. Note that XDebug
         *    interferes with this and has to be disabled. This is
         *    to make sure the correct error code is returned
         *    from unattended scripts.
         *    @access private
         */
        function _enableErrorReporting() {
            if ($this->_xdebug_is_enabled) {
                xdebug_disable();
            }
            ini_set('track_errors', true);
        }

        /**
         *    Resets detection of parse errors to their old values.
         *    This is to make sure the correct error code is returned
         *    from unattended scripts.
         *    @access private
         */
        function _disableErrorReporting() {
            ini_set('track_errors', $this->_old_track_errors);
            if ($this->_xdebug_is_enabled) {
                xdebug_enable();
            }
        }

        /**
         *    Calculates the incoming test cases from a before
         *    and after list of loaded classes. Skips abstract
         *    classes.
         *    @param array $existing_classes   Classes before require().
         *    @param array $new_classes        Classes after require().
         *    @return array                    New classes which are test
         *                                     cases that shouldn't be ignored.
         *    @access private
         */
        function _selectRunnableTests($existing_classes, $new_classes) {
            $classes = array();
            foreach ($new_classes as $class) {
                if (in_array($class, $existing_classes)) {
                    continue;
                }
                if ($this->_getBaseTestCase($class)) {
                    $reflection = new SimpleReflection($class);
                    if ($reflection->isAbstract()) {
                        SimpleTest::ignore($class);
                    }
                    $classes[] = $class;
                }
            }
            return $classes;
        }

        /**
         *    Builds a group test from a class list.
         *    @param string $title       Title of new group.
         *    @param array $classes      Test classes.
         *    @return TestSuite          Group loaded with the new
         *                               test cases.
         *    @access private
         */
        function &_createGroupFromClasses($title, $classes) {
            SimpleTest::ignoreParentsIfIgnored($classes);
            $group = &new TestSuite($title);
            foreach ($classes as $class) {
                if (! SimpleTest::isIgnored($class)) {
                    $group->addTestClass($class);
                }
            }
            return $group;
        }

        /**
         *    Test to see if a class is derived from the
         *    SimpleTestCase class.
         *    @param string $class     Class name.
         *    @access private
         */
        function _getBaseTestCase($class) {
            while ($class = get_parent_class($class)) {
                $class = strtolower($class);
                if ($class == 'simpletestcase' || $class == 'testsuite' || $class == 'grouptest') {
                    return $class;
                }
            }
            return false;
        }

        /**
         *    Delegates to a visiting collector to add test
         *    files.
         *    @param string $path                  Path to scan from.
         *    @param SimpleCollector $collector    Directory scanner.
         *    @access public
         */
        function collect($path, &$collector) {
            $collector->collect($this, $path);
        }

        /**
         *    Invokes run() on all of the held test cases, instantiating
         *    them if necessary.
         *    @param SimpleReporter $reporter    Current test reporter.
         *    @access public
         */
        function run(&$reporter) {
            $reporter->paintGroupStart($this->getLabel(), $this->getSize());
            for ($i = 0, $count = count($this->_test_cases); $i < $count; $i++) {
                if (is_string($this->_test_cases[$i])) {
                    $class = $this->_test_cases[$i];
                    $test = &new $class();
                    $test->run($reporter);
                    unset($test);
                } else {
                    $this->_test_cases[$i]->run($reporter);
                }
            }
            $reporter->paintGroupEnd($this->getLabel());
            return $reporter->getStatus();
        }

        /**
         *    Number of contained test cases.
         *    @return integer     Total count of cases in the group.
         *    @access public
         */
        function getSize() {
            $count = 0;
            foreach ($this->_test_cases as $case) {
                if (is_string($case)) {
                    $count++;
                } else {
                    $count += $case->getSize();
                }
            }
            return $count;
        }
    }
    
    /**
     *    @deprecated
     */
    class GroupTest extends TestSuite { }

    /**
     *    This is a failing group test for when a test suite hasn't
     *    loaded properly.
	 *    @package		SimpleTest
	 *    @subpackage	UnitTester
     */
    class BadTestSuite {
        var $_label;
        var $_error;

        /**
         *    Sets the name of the test suite and error message.
         *    @param string $label    Name sent at the start and end
         *                            of the test.
         *    @access public
         */
        function BadTestSuite($label, $error) {
            $this->_label = $label;
            $this->_error = $error;
        }

        /**
         *    Accessor for the test name for subclasses.
         *    @return string           Name of the test.
         *    @access public
         */
        function getLabel() {
            return $this->_label;
        }

        /**
         *    Sends a single error to the reporter.
         *    @param SimpleReporter $reporter    Current test reporter.
         *    @access public
         */
        function run(&$reporter) {
            $reporter->paintGroupStart($this->getLabel(), $this->getSize());
            $reporter->paintFail('Bad TestSuite [' . $this->getLabel() .
                    '] with error [' . $this->_error . ']');
            $reporter->paintGroupEnd($this->getLabel());
            return $reporter->getStatus();
        }

        /**
         *    Number of contained test cases. Always zero.
         *    @return integer     Total count of cases in the group.
         *    @access public
         */
        function getSize() {
            return 0;
        }
    }
    
    /**
     *    @deprecated
     */
    class BadGroupTest extends BadTestSuite { }
?>