Permalink
Fetching contributors…
Cannot retrieve contributors at this time
268 lines (244 sloc) 7.05 KB
<?php
/**
* li₃: the most RAD framework for PHP (http://li3.me)
*
* Copyright 2016, Union of RAD. All rights reserved. This source
* code is distributed under the terms of the BSD 3-Clause License.
* The full license text can be found in the LICENSE.txt file.
*/
namespace lithium\test;
use lithium\aop\Filters;
use lithium\core\Libraries;
use lithium\util\Inflector;
use lithium\core\ClassNotFoundException;
use lithium\template\TemplateException;
/**
* This `Report` object aggregates tests in a group and allows you to run said tests to
* obtain the results and stats (passes, fails, exceptions, skips) of the test run.
*
* While Lithium already comes with a text-based as well as web-based test interface, you
* may use or extend the `Report` class to create your own test report functionality. In
* addition, you can also create your own custom templates for displaying results in a different
* format, such as json.
*
* Example usage, for built-in HTML format:
*
* ```
* $report = new Report([
* 'title' => 'Test Report Title',
* 'group' => new Group(['data' => ['lithium\tests\cases\net\http\MediaTest']]),
* 'format' => 'html'
* ]);
*
* $report->run();
*
* // Get the test stats:
* $report->stats();
*
* // Get test results:
* $report->results
* ```
*
* You may also choose to filter the results of the test runs to obtain additional information.
* For example, say you wish to calculate the cyclomatic complexity of the classes you are testing:
*
* ```
* $report = new Report([
* 'title' => 'Test Report Title',
* 'group' => new Group(['data' => ['lithium\tests\cases\net\http\MediaTest']]),
* 'filters' => ['Complexity']
* ]);
*
* $report->run();
*
* // Get test results, including filter results:
* $report->results
* ```
*
* @see lithium\test\Group
* @see lithium\test\filter
* @see lithium\test\templates
*/
class Report extends \lithium\core\Object {
/**
* Contains an instance of `lithium\test\Group`, which contains all unit tests to be executed
* this test run.
*
* @see lithium\test\Group
* @var object
*/
public $group = null;
/**
* Title of the group being run.
*
* @var string
*/
public $title;
/**
* Group and filter results.
*
* @var array
*/
public $results = ['group' => [], 'filters' => []];
/**
* Start and end timers.
*
* @var array
*/
public $timer = ['start' => null, 'end' => null];
/**
* An array key on fully-namespaced class names of the filter with options to be
* applied for the filter as the value
*
* @var array
*/
protected $_filters = [];
/**
* Constructor.
*
* @param array $config Options array for the test run. Valid options are:
* - `'group'`: The test group with items to be run.
* - `'filters'`: An array of filters that the test output should be run through.
* - `'format'`: The format of the template to use, defaults to `'txt'`.
* - `'reporter'`: The reporter to use.
* @return void
*/
public function __construct(array $config = []) {
$defaults = [
'title' => null,
'group' => null,
'filters' => [],
'format' => 'txt',
'reporter' => null
];
parent::__construct($config + $defaults);
}
/**
* Initializer.
*
* @return void
*/
protected function _init() {
$this->group = $this->_config['group'];
$this->title = $this->_config['title'] ?: $this->_config['title'];
}
/**
* Runs tests.
*
* @return void
*/
public function run() {
$tests = $this->group->tests();
foreach ($this->filters() as $filter => $options) {
$this->results['filters'][$filter] = [];
$tests = $filter::apply($this, $tests, $options['apply']) ?: $tests;
}
$this->results['group'] = $tests->run([
'reporter' => $this->_config['reporter']
]);
foreach ($this->filters() as $filter => $options) {
$this->results['filters'][$filter] = $filter::analyze($this, $options['analyze']);
}
}
/**
* Collects Results from the test filters and aggregates them.
*
* @param string $class Classname of the filter for which to aggregate results.
* @param array $results Array of the filter results for
* later analysis by the filter itself.
* @return void
*/
public function collect($class, $results) {
$this->results['filters'][$class][] = $results;
}
/**
* Return statistics from the test runs.
*
* @return array
*/
public function stats() {
$results = (array) $this->results['group'];
$defaults = [
'asserts' => 0,
'passes' => [],
'fails' => [],
'exceptions' => [],
'errors' => [],
'skips' => []
];
$stats = array_reduce($results, function($stats, $result) use ($defaults) {
$stats = (array) $stats + $defaults;
$result = empty($result[0]) ? [$result] : $result;
foreach ($result as $response) {
if (empty($response['result'])) {
continue;
}
$result = $response['result'];
if (in_array($result, ['fail', 'exception'])) {
$response = array_merge(
['class' => 'unknown', 'method' => 'unknown'], $response
);
$stats['errors'][] = $response;
}
unset($response['file'], $response['result']);
if (in_array($result, ['pass', 'fail'])) {
$stats['asserts']++;
}
if (in_array($result, ['pass', 'fail', 'exception', 'skip'])) {
$stats[Inflector::pluralize($result)][] = $response;
}
}
return $stats;
});
$stats = (array) $stats + $defaults;
$count = array_map(
function($value) { return is_array($value) ? count($value) : $value; }, $stats
);
$success = $count['passes'] === $count['asserts'] && $count['errors'] === 0;
return compact('stats', 'count', 'success');
}
/**
* Renders the test output (e.g. layouts and filter templates).
*
* @param string $template name of the template (i.e. `'layout'`).
* @param string|array $data array from `_data()` method.
* @return string
* @filter
*/
public function render($template, $data = []) {
$config = $this->_config;
if ($template === 'stats' && !$data) {
$data = $this->stats();
}
$template = Libraries::locate('test.templates', $template, [
'filter' => false, 'type' => 'file', 'suffix' => ".{$config['format']}.php"
]);
if ($template === null) {
$message = "Templates for format `{$config['format']}` not found in `test/templates`.";
throw new TemplateException($message);
}
$params = compact('template', 'data', 'config');
return Filters::run(__CLASS__, __FUNCTION__, $params, function($params) {
extract($params['data']);
ob_start();
include $params['template'];
return ob_get_clean();
});
}
public function filters(array $filters = []) {
if ($this->_filters && !$filters) {
return $this->_filters;
}
$filters += (array) $this->_config['filters'];
$results = [];
foreach ($filters as $filter => $options) {
if (!$class = Libraries::locate('test.filter', $filter)) {
throw new ClassNotFoundException("`{$class}` is not a valid test filter.");
}
$options['name'] = strtolower(join('', array_slice(explode("\\", $class), -1)));
$results[$class] = $options + ['apply' => [], 'analyze' => []];
}
return $this->_filters = $results;
}
}
?>