kahlan/kahlan

View on GitHub
src/Suite.php

Summary

Maintainability
A
3 hrs
Test Coverage
A
98%
<?php
namespace Kahlan;

use Closure;
use Exception;
use InvalidArgumentException;
use Kahlan\Analysis\Debugger;
use Kahlan\Block;
use Kahlan\Block\Group;
use Kahlan\Filter\Filters;

class Suite
{
    /**
     * The PHP constraint to respect
     *
     * @var string
     */
    public static $PHP = PHP_MAJOR_VERSION;

    /**
     * @var bool
     * @internal
     */
    public $loaded = false;

    /**
     * Blocks stack.
     *
     * @var Block[]
     */
    protected static $_blocks = [];

    /**
     * Store all hashed references.
     *
     * @var array
     */
    protected static $_registered = [];

    /**
     * The return status value (`0` for success).
     *
     * @var integer
     */
    protected $_status = 0;

    /**
     * The root group block of the test suite.
     *
     * @var Group
     */
    protected $_root = null;

    /**
     * The reporters container.
     *
     * @var Reporters
     */
    protected $_reporters = null;

    /**
     * Array of fully-namespaced class name to clear on each `it()`.
     *
     * @var array
     */
    protected $_autoclear = [];

    /**
     * Set the number of fails allowed before aborting. `0` mean no fast fail.
     *
     * @see ::failfast()
     * @var integer
     */
    protected $_ff = 0;

    /**
     * Count the number of failure or exception.
     *
     * @see ::failfast()
     * @var integer
     */
    protected $_failures = 0;

    /**
     * The execution summary instance.
     *
     * @var object
     */
    protected $_summary = null;

    /**
     * A regexp pattern used to removes useless traces to focus on the one
     * related to a spec file.
     *
     * @var string
     */
    protected $_backtraceFocus = null;

    /**
     * The Constructor.
     */
    public function __construct()
    {
        $this->_summary = new Summary();
        $this->_root = new Group(['suite' => $this]);
    }

    /**
     * Gets children.
     *
     * @return array The array of children instances.
     */
    public function root()
    {
        return $this->_root;
    }

    /**
     * Increment the number of failures.
     */
    public function failure()
    {
        $this->_failures++;
    }

    /**
     * Returns `true` if the suite reach the number of allowed failure by the fail-fast parameter.
     *
     * @return boolean;
     */
    public function failfast()
    {
        return $this->_ff && $this->_failures >= $this->_ff;
    }

    /**
     * Overrides the default error handler
     *
     * @param boolean $enable  If `true` override the default error handler,
     *                         if `false` restore the default handler.
     * @param array   $options An options array. Available options are:
     *                         - 'handler': An error handler closure.
     *
     */
    protected function _errorHandler($enable, $options = [])
    {
        $defaults = ['handler' => null];
        $options += $defaults;
        if (!$enable) {
            return restore_error_handler();
        }
        $handler = function ($code, $message, $file, $line = 0, $args = []) {
            if (!(error_reporting() & $code)) {
                return;
            }
            $trace = debug_backtrace();
            $trace = array_slice($trace, 1, count($trace));
            $message = "`" . Debugger::errorType($code) . "` {$message}";
            $code = 0;
            $exception = compact('code', 'message', 'file', 'line', 'trace');
            throw new PhpErrorException($exception);
        };
        $options['handler'] = $options['handler'] ?: $handler;
        set_error_handler($options['handler'], error_reporting());
    }

    /**
     * Gets/sets the regexp pattern used to removes useless traces to focus on the one
     * related to a spec file.
     *
     * @param  string $pattern A wildcard pattern (i.e. `fnmatch()` style).
     * @return string          The focus regexp.
     */
    public function backtraceFocus($pattern = null)
    {
        if ($pattern === null) {
            return $this->_backtraceFocus;
        }
        $patterns = is_array($pattern) ? $pattern : [$pattern];
        foreach ($patterns as $key => $value) {
            $patterns[$key] = preg_quote($value, '~');
        }
        $pattern = join('|', $patterns);
        return $this->_backtraceFocus = strtr($pattern, ['\*' => '.*', '\?' => '.']);
    }

    /**
     * Runs all specs.
     *
     * @param  array     $options Run options.
     *
     * @return boolean            The result array.
     * @throws Exception
     */
    public function run($options = [])
    {
        $defaults = [
            'reporters' => null,
            'autoclear' => [],
            'ff'        => 0,
            'part'      => '1/1'
        ];
        $options += $defaults;

        $this->_reporters = $options['reporters'];
        $this->_autoclear = (array) $options['autoclear'];
        $this->_ff = $options['ff'];

        [$index, $total] = explode('/', $options['part']) + [null, null];

        $this->root()->partition($index, $total);

        $this->report('start', ['total' => $this->active()]);

        $this->_errorHandler(true, $options);

        $passed = $this->root()->process();

        $this->_errorHandler(false);

        $this->summary()->memoryUsage(memory_get_peak_usage());

        $this->report('end', $this->summary());
        $this->_status = $passed ? 0 : 1;
        return $passed;
    }

    /**
     * Run a block's closure.
     *
     * @param  Block   $block   The block instance.
     * @param  Closure $closure The closure.
     * @param  string  $type    The closure type.
     * @return mixed
     */
    public function runBlock($block, $closure, $type)
    {
        return Filters::run($this, 'runBlock', [$block, $closure, $type], function ($next, $block, $closure, $type) {
            return call_user_func_array($closure, []);
        });
    }

    /**
     * Gets specs excecution results.
     *
     * @return array
     */
    public function summary()
    {
        return $this->_summary;
    }

    /**
     * Gets number of total specs.
     *
     * @return integer
     */
    public function total()
    {
        $stats = $this->root()->stats();
        return $stats['normal'] + $stats['focused'] + $stats['excluded'];
    }

    /**
     * Gets number of active specs.
     *
     * @return integer
     */
    public function active()
    {
        $stats = $this->root()->stats();
        return $this->root()->focused() ? $stats['focused'] : $stats['normal'];
    }

    /**
     * Gets/sets exit status code according passed results.
     *
     * @param  integer $status If set force a specific status to be returned.
     *
     * @return boolean         Returns `0` if no error occurred, `1` otherwise.
     */
    public function status($status = null)
    {
        if (func_num_args()) {
            $this->_status = $status;
            return $this;
        }
        if ($this->root()->focused()) {
            return 1;
        }
        return $this->_status;
    }

    /**
     * Send some data to reporters.
     *
     * @param string $type The message type.
     * @param mixed  $data The message data.
     */
    public function report($type, $data)
    {
        if (!$reporters = $this->reporters()) {
            return;
        }
        $reporters->dispatch($type, $data);
    }

    /**
     * Gets the reporters container.
     *
     * @return object
     */
    public function reporters()
    {
        return $this->_reporters;
    }

    /**
     * Autoclears plugins.
     */
    public function autoclear()
    {
        foreach ($this->_autoclear as $plugin) {
            if (is_object($plugin)) {
                if (method_exists($plugin, 'clear')) {
                    $plugin->clear();
                }
            } elseif (method_exists($plugin, 'reset')) {
                $plugin::reset();
            }
        }
    }

    /**
     * Triggers the `stop` event.
     */
    public function stop()
    {
        $this->report('stop', $this->summary());
    }

    /**
     * Generates a hash from an instance or a string.
     *
     * @param  mixed $reference An instance or a fully namespaced class name.
     *
     * @return string           A string hash.
     * @throws InvalidArgumentException
     */
    public static function hash($reference)
    {
        if (is_object($reference)) {
            return spl_object_hash($reference);
        }
        if (is_string($reference)) {
            return $reference;
        }
        throw new InvalidArgumentException("Error, the passed argument is not hashable.");
    }

    /**
     * Registers a hash. [Mainly used for optimization]
     *
     * @param mixed $hash A hash to register.
     */
    public static function register($hash)
    {
        static::$_registered[$hash] = true;
    }

    /**
     * Gets registered hashes. [Mainly used for optimizations]
     *
     * @param  string     $hash The hash to look up. If none return all registered hashes.
     *
     * @return array|bool
     */
    public static function registered($hash = null)
    {
        if (!func_num_args()) {
            return static::$_registered;
        }

        return isset(static::$_registered[$hash]);
    }

    /**
     * Clears the registered hash.
     */
    public static function reset()
    {
        static::$_registered = [];
    }

    /**
     * Push a block in the stack.
     *
     * @param Block A block instance to push instance.
     */
    public static function push($block)
    {
        static::$_blocks[] = $block;
    }

    /**
     * Get the active scope instance.
     *
     * @return Scope The object instance or `null` if there's no active instance.
     */
    public static function current()
    {
        return end(static::$_blocks);
    }

    /**
     * Pop a block from the stack.
     *
     * @return Block The popped block instance .
     */
    public static function pop()
    {
        return array_pop(static::$_blocks);
    }
}