Report.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\test;
  9. use lithium\core\Libraries;
  10. use lithium\util\Inflector;
  11. use lithium\core\ClassNotFoundException;
  12. /**
  13. * This `Report` object aggregates tests in a group and allows you to run said tests to
  14. * obtain the results and stats (passes, fails, exceptions, skips) of the test run.
  15. *
  16. * While Lithium already comes with a text-based as well as web-based test interface, you
  17. * may use or extend the `Report` class to create your own test report functionality. In
  18. * addition, you can also create your own custom templates for displaying results in a different
  19. * format, such as json.
  20. *
  21. * Example usage, for built-in HTML format:
  22. *
  23. * {{{
  24. * $report = new Report(array(
  25. * 'title' => 'Test Report Title',
  26. * 'group' => new Group(array('data' => array('lithium\tests\cases\net\http\MediaTest'))),
  27. * 'format' => 'html'
  28. * ));
  29. *
  30. * $report->run();
  31. *
  32. * // Get the test stats:
  33. * $report->stats();
  34. *
  35. * // Get test results:
  36. * $report->results
  37. * }}}
  38. *
  39. * You may also choose to filter the results of the test runs to obtain additional information.
  40. * For example, say you wish to calculate the cyclomatic complexity of the classes you are testing:
  41. *
  42. * {{{
  43. * $report = new Report(array(
  44. * 'title' => 'Test Report Title',
  45. * 'group' => new Group(array('data' => array('lithium\tests\cases\net\http\MediaTest'))),
  46. * 'filters' => array('Complexity')
  47. * ));
  48. *
  49. * $report->run();
  50. *
  51. * // Get test results, including filter results:
  52. * $report->results
  53. * }}}
  54. *
  55. * @see lithium\test\Group
  56. * @see lithium\test\filter
  57. * @see lithium\test\templates
  58. */
  59. class Report extends \lithium\core\Object {
  60. /**
  61. * Contains an instance of `lithium\test\Group`, which contains all unit tests to be executed
  62. * this test run.
  63. *
  64. * @see lithium\test\Group
  65. * @var object
  66. */
  67. public $group = null;
  68. /**
  69. * Title of the group being run.
  70. *
  71. * @var string
  72. */
  73. public $title;
  74. /**
  75. * Group and filter results.
  76. *
  77. * @var array
  78. */
  79. public $results = array('group' => array(), 'filters' => array());
  80. /**
  81. * Start and end timers.
  82. *
  83. * @var array
  84. */
  85. public $timer = array('start' => null, 'end' => null);
  86. /**
  87. * An array key on fully-namespaced class names of the filter with options to be
  88. * applied for the filter as the value
  89. *
  90. * @var array
  91. */
  92. protected $_filters = array();
  93. /**
  94. * Constructor.
  95. *
  96. * @param array $config Options array for the test run. Valid options are:
  97. * - `'group'`: The test group with items to be run.
  98. * - `'filters'`: An array of filters that the test output should be run through.
  99. * - `'format'`: The format of the template to use, defaults to `'txt'`.
  100. * - `'reporter'`: The reporter to use.
  101. */
  102. public function __construct(array $config = array()) {
  103. $defaults = array(
  104. 'title' => null,
  105. 'group' => null,
  106. 'filters' => array(),
  107. 'format' => 'txt',
  108. 'reporter' => null
  109. );
  110. parent::__construct($config + $defaults);
  111. }
  112. /**
  113. * Initializer.
  114. *
  115. * @return void
  116. */
  117. protected function _init() {
  118. $this->group = $this->_config['group'];
  119. $this->title = $this->_config['title'] ?: $this->_config['title'];
  120. }
  121. /**
  122. * Runs tests.
  123. *
  124. * @return void
  125. */
  126. public function run() {
  127. $tests = $this->group->tests();
  128. foreach ($this->filters() as $filter => $options) {
  129. $this->results['filters'][$filter] = array();
  130. $tests = $filter::apply($this, $tests, $options['apply']) ?: $tests;
  131. }
  132. $this->results['group'] = $tests->run(array(
  133. 'reporter' => $this->_config['reporter']
  134. ));
  135. foreach ($this->filters() as $filter => $options) {
  136. $this->results['filters'][$filter] = $filter::analyze($this, $options['analyze']);
  137. }
  138. }
  139. /**
  140. * Collects Results from the test filters and aggregates them.
  141. *
  142. * @param string $class Classname of the filter for which to aggregate results.
  143. * @param array $results Array of the filter results for
  144. * later analysis by the filter itself.
  145. * @return void
  146. */
  147. public function collect($class, $results) {
  148. $this->results['filters'][$class][] = $results;
  149. }
  150. /**
  151. * Return statistics from the test runs.
  152. *
  153. * @return array
  154. */
  155. public function stats() {
  156. $results = (array) $this->results['group'];
  157. $defaults = array(
  158. 'asserts' => 0,
  159. 'passes' => array(),
  160. 'fails' => array(),
  161. 'exceptions' => array(),
  162. 'errors' => array(),
  163. 'skips' => array()
  164. );
  165. $stats = array_reduce($results, function($stats, $result) use ($defaults) {
  166. $stats = (array) $stats + $defaults;
  167. $result = empty($result[0]) ? array($result) : $result;
  168. foreach ($result as $response) {
  169. if (empty($response['result'])) {
  170. continue;
  171. }
  172. $result = $response['result'];
  173. if (in_array($result, array('fail', 'exception'))) {
  174. $response = array_merge(
  175. array('class' => 'unknown', 'method' => 'unknown'), $response
  176. );
  177. $stats['errors'][] = $response;
  178. }
  179. unset($response['file'], $response['result']);
  180. if (in_array($result, array('pass', 'fail'))) {
  181. $stats['asserts']++;
  182. }
  183. if (in_array($result, array('pass', 'fail', 'exception', 'skip'))) {
  184. $stats[Inflector::pluralize($result)][] = $response;
  185. }
  186. }
  187. return $stats;
  188. });
  189. $stats = (array) $stats + $defaults;
  190. $count = array_map(
  191. function($value) { return is_array($value) ? count($value) : $value; }, $stats
  192. );
  193. $success = $count['passes'] === $count['asserts'] && $count['errors'] === 0;
  194. return compact('stats', 'count', 'success');
  195. }
  196. /**
  197. * Renders the test output (e.g. layouts and filter templates).
  198. *
  199. * @param string $template name of the template (i.e. `'layout'`).
  200. * @param string|array $data array from `_data()` method.
  201. * @return string
  202. * @filter
  203. */
  204. public function render($template, $data = array()) {
  205. $config = $this->_config;
  206. if ($template === "stats" && !$data) {
  207. $data = $this->stats();
  208. }
  209. $template = Libraries::locate('test.templates', $template, array(
  210. 'filter' => false, 'type' => 'file', 'suffix' => ".{$config['format']}.php"
  211. ));
  212. $params = compact('template', 'data', 'config');
  213. return $this->_filter(__METHOD__, $params, function($self, $params, $chain) {
  214. extract($params['data']);
  215. ob_start();
  216. include $params['template'];
  217. return ob_get_clean();
  218. });
  219. }
  220. public function filters(array $filters = array()) {
  221. if ($this->_filters && !$filters) {
  222. return $this->_filters;
  223. }
  224. $filters += (array) $this->_config['filters'];
  225. $results = array();
  226. foreach ($filters as $filter => $options) {
  227. if (!$class = Libraries::locate('test.filter', $filter)) {
  228. throw new ClassNotFoundException("`{$class}` is not a valid test filter.");
  229. }
  230. $options['name'] = strtolower(join('', array_slice(explode("\\", $class), -1)));
  231. $results[$class] = $options + array('apply' => array(), 'analyze' => array());
  232. }
  233. return $this->_filters = $results;
  234. }
  235. }
  236. ?>