From 63fc7b364e4bb2265f6714fdb1df83fdf8e88198 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sat, 11 Jun 2011 20:17:55 -0400 Subject: [PATCH] Add unit testing infrastructure, again straignt from phalanx --- .gitignore | 3 +- testing/phpunit.xml | 38 ++++ testing/run_tests.sh | 21 +++ testing/test_listener.php | 254 ++++++++++++++++++++++++++ testing/test_runner.php | 42 +++++ testing/tests/base/functions_test.php | 88 +++++++++ 6 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 testing/phpunit.xml create mode 100755 testing/run_tests.sh create mode 100644 testing/test_listener.php create mode 100644 testing/test_runner.php create mode 100644 testing/tests/base/functions_test.php diff --git a/.gitignore b/.gitignore index 4df8f53..0b3f74b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *~ -.DS_Store \ No newline at end of file +.DS_Store +testing/unittest_coverage/ \ No newline at end of file diff --git a/testing/phpunit.xml b/testing/phpunit.xml new file mode 100644 index 0000000..fc4de5b --- /dev/null +++ b/testing/phpunit.xml @@ -0,0 +1,38 @@ + + + + + + + + ./tests/ + + + + + + ./ + + + + + + + diff --git a/testing/run_tests.sh b/testing/run_tests.sh new file mode 100755 index 0000000..7d0b907 --- /dev/null +++ b/testing/run_tests.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# Hoplite +# Copyright (c) 2011 Blue Static +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . + +OLDPWD=$(pwd) +cd $(dirname "$0") +php ./test_runner.php --verbose $@ +cd $OLDPWD diff --git a/testing/test_listener.php b/testing/test_listener.php new file mode 100644 index 0000000..e9a94bf --- /dev/null +++ b/testing/test_listener.php @@ -0,0 +1,254 @@ +. + +namespace hoplite\test; + +// This TestListener is meant to print to stdout and show CLI output for the +// running test suite. It unfortunately conflicts with the standard text runner +// UI, so it must be configured manually (see runner.php for an example). +// +// The format of the output is designed to mimic the Google Test (GTest) +// framework output. +class TestListener extends \PHPUnit_Util_Printer implements \PHPUnit_Framework_TestListener +{ + const COLOR_NONE = 0; + const COLOR_RED = 1; + const COLOR_GREEN = 2; + const COLOR_BLUE = 3; + const COLOR_PURPLE = 4; + const COLOR_CYAN = 5; + + // The start time of the test suite. + private $suite_start_time = 0; + + // The suite depth. + private $suite_depth = 0; + + // The number of errors that occured in a suite. + private $suite_error_counts = 0; + + // Array of failing tests. + private $failing = array(); + + // Array of skipped tests. + private $skipped = array(); + + // Array of incomplete tests. + private $incomplete = array(); + + // An error occurred. + public function addError(\PHPUnit_Framework_Test $test, + \Exception $e, + $time) + { + $this->_Print(NULL, $this->_ErrorLocation($e)); + $this->_Print(' ', $e->GetMessage()); + $this->_Print('[ ERROR ]', $test->ToString() . ' (' . $this->_Round($time) . ' ms)', self::COLOR_RED); + ++$this->suite_error_count; + $this->failing[] = $test->ToString(); + } + + // A failure occurred. + public function addFailure(\PHPUnit_Framework_Test $test, + \PHPUnit_Framework_AssertionFailedError $e, + $time) + { + $this->_Print(NULL, $this->_ErrorLocation($e)); + $this->_Print(' ', $e->GetMessage()); + $this->_Print('[ FAILED ]', $test->ToString() . ' (' . $this->_Round($time) . ' ms)', self::COLOR_RED); + ++$this->suite_error_count; + $this->failing[] = $test->ToString(); + } + + // Incomplete test. + public function addIncompleteTest(\PHPUnit_Framework_Test $test, + \Exception $e, $time) + { + $this->incomplete[] = $test->ToString(); + $this->_Print('INCOMPLETE', $e->GetMessage(), self::COLOR_PURPLE); + } + + // Skipped test. + public function addSkippedTest(\PHPUnit_Framework_Test $test, + \Exception $e, + $time) + { + $this->skipped[] = $test->ToString(); + $this->_Print('SKIPPED', $e->GetMessage(), self::COLOR_BLUE); + } + + // A test suite started. + public function startTestSuite(\PHPUnit_Framework_TestSuite $suite) + { + // Wrap the suite header. + ob_start(); + + $this->_Print($this->_SuiteMarker(), $this->_DescribeSuite($suite), self::COLOR_GREEN); + $this->suite_start_time = microtime(TRUE); + ++$this->suite_depth; + $this->suite_error_count = 0; + + // Wrap the suite contents. + ob_start(); + } + + // A test suite ended. + public function endTestSuite(\PHPUnit_Framework_TestSuite $suite) + { + $main_suite = (--$this->suite_depth == 0); + $color_red = (($main_suite && count($this->failing)) || $this->suite_error_count > 0); + $any_output = ob_get_length(); + + $delta = microtime(TRUE) - $this->suite_start_time; + $this->_Print( + $this->_SuiteMarker(), + $this->_DescribeSuite($suite) . ' (' . $this->_Round($delta) . ' ms total)', + ($color_red ? self::COLOR_RED : self::COLOR_GREEN)); + $this->Write("\n"); + + // If this is the main suite (the one to which all other tests/suites + // are attached), then print the test summary. + if ($main_suite && $color_red) { + $count = count($this->failing); + $tests = $this->_Plural('TEST', $count, TRUE); + $this->Write($this->_Color(" YOU HAVE $count FAILING $tests:\n", self::COLOR_RED)); + foreach ($this->failing as $test) { + $this->Write(" $test\n"); + } + $this->Write("\n"); + } + + $count = count($this->incomplete); + $any_output |= $count > 0; + if ($main_suite && $count) { + $tests = $this->_Plural('TEST', $count, TRUE); + $this->Write($this->_Color(" YOU HAVE $count INCOMPLETE $tests:\n", self::COLOR_PURPLE)); + foreach ($this->incomplete as $test) { + $this->Write(" $test\n"); + } + $this->Write("\n"); + } + + $count = count($this->skipped); + if ($main_suite && $count) { + $tests = $this->_Plural('TEST', $count, TRUE); + $this->Write($this->_Color(" YOU HAVE $count SKIPPED $tests:\n", self::COLOR_BLUE)); + foreach ($this->skipped as $test) { + $this->Write(" $test\n"); + } + $this->Write("\n"); + } + + // Flush the test output. + ob_end_flush(); + + // Flush the suite header. + if ($main_suite || $any_output) + ob_end_flush(); + else + ob_end_clean(); + } + + // A test started. + public function startTest(\PHPUnit_Framework_Test $test) + { + $this->_Print('[ RUN ]', $test->ToString(), self::COLOR_GREEN); + } + + // A test ended. + public function endTest(\PHPUnit_Framework_Test $test, $time) + { + $name = $test->ToString(); + if (in_array($name, $this->skipped) || in_array($name, $this->incomplete)) { + $this->_Print('[ ABORT ]', $name . ' (' . $this->_Round($time) . ' ms)', self::COLOR_CYAN); + } else { + $this->_Print('[ OK ]', $name . ' (' . $this->_Round($time) . ' ms)', self::COLOR_GREEN); + } + } + + // Returns the description for a test suite. + private function _DescribeSuite(\PHPUnit_Framework_TestSuite $suite) + { + $count = $suite->Count(); + return sprintf('%d %s from %s', $count, $this->_Plural('test', $count), $suite->GetName()); + } + + // Returns the test suite marker. + private function _SuiteMarker() + { + if ($this->suite_depth == 0) + return '[==========]'; + else + return '[----------]'; + } + + // Prints a line to output. + private function _Print($column, $annotation, $color = self::COLOR_NONE) + { + $column = $this->_Color($column, $color); + $this->Write("$column $annotation\n"); + } + + // Takes in a float from microtime() and returns it formatted to display as + // milliseconds. + private function _Round($time) + { + return round($time * 1000); + } + + // Returns the error location as a string. + private function _ErrorLocation(\Exception $e) + { + $trace = $e->GetTrace(); + $frame = NULL; + // Find the first frame from non-PHPUnit code, which is where the error + // should have occurred. + foreach ($trace as $f) { + if (isset($f['file']) && strpos($f['file'], 'PHPUnit/Framework') === FALSE) { + $frame = $f; + break; + } + } + if (!$frame) + $frame = $trace[0]; + return $frame['file'] . ':' . $frame['line']; + } + + // Colors |$str| to be a certain |$color|. + private function _Color($str, $color) + { + $color_code = ''; + switch ($color) { + case self::COLOR_RED: $color_code = '0;31'; break; + case self::COLOR_GREEN: $color_code = '0;32'; break; + case self::COLOR_BLUE: $color_code = '0;34'; break; + case self::COLOR_PURPLE: $color_code = '0;35'; break; + case self::COLOR_CYAN: $color_code = '0;36'; break; + } + if ($color == self::COLOR_NONE) { + return $str; + } + return "\x1b[{$color_code}m{$str}\x1b[0m"; + } + + // Returns the plural of the |$word| if |$count| is greater than one. + private function _Plural($word, $count, $capitalize = FALSE) + { + if ($count > 1) + return $word . ($capitalize ? 'S' : 's'); + return $word; + } +} diff --git a/testing/test_runner.php b/testing/test_runner.php new file mode 100644 index 0000000..0be30f8 --- /dev/null +++ b/testing/test_runner.php @@ -0,0 +1,42 @@ +. + +namespace hoplite\test; + +if (!defined('HOPLITE_ROOT')) { + define('HOPLITE_ROOT', dirname(dirname(__FILE__))); + define('TEST_ROOT', dirname(__FILE__)); +} + +// PHPUnit 3.5.5. +require_once 'PHPUnit/Autoload.php'; +require_once TEST_ROOT . '/test_listener.php'; + +class HopliteTestRunner extends \PHPUnit_TextUI_Command +{ + static public function Main($exit = TRUE) + { + $command = new self(); + $command->Run($_SERVER['argv'], $exit); + } + + protected function HandleCustomTestSuite() + { + $this->arguments['printer'] = new TestListener(); + } +} + +HopliteTestRunner::Main(); diff --git a/testing/tests/base/functions_test.php b/testing/tests/base/functions_test.php new file mode 100644 index 0000000..a58b965 --- /dev/null +++ b/testing/tests/base/functions_test.php @@ -0,0 +1,88 @@ +. + +namespace hoplite\test; +use \hoplite\base as base; + +require_once HOPLITE_ROOT . '/base/functions.php'; + +class FunctionsTest extends \PHPUnit_Framework_TestCase +{ + public function testArrayStripEmpty() + { + $array = array(1, 4, 6); + base\ArrayStripEmpty($array); + $this->assertEquals(3, count($array)); + + $array = array(1, 0, 5, ''); + base\ArrayStripEmpty($array); + $this->assertEquals(2, count($array)); + + $array = array('', 'test' => array('', 6)); + base\ArrayStripEmpty($array); + $this->assertEquals(1, count($array)); + $this->assertEquals(1, count($array['test'])); + + $array = array('foo', NULL, 'bar'); + base\ArrayStripEmpty($array); + $this->assertEquals(2, count($array)); + } + + public function testUnderscoreToCamelCase() + { + $str = 'under_score'; + $this->assertEquals('UnderScore', base\UnderscoreToCamelCase($str)); + $this->assertEquals('underScore', base\UnderscoreToCamelCase($str, FALSE)); + + $str = 'many_many_under_scores'; + $this->assertEquals('ManyManyUnderScores', base\UnderscoreToCamelCase($str)); + } + + public function testCamelCaseToUnderscore() + { + $str = 'CamelCase'; + $this->assertEquals('camel_case', base\CamelCaseToUnderscore($str)); + + $str = 'camelCase'; + $this->assertEquals('camel_case', base\CamelCaseToUnderscore($str)); + + $str = 'AVeryLongTitleCase'; + $this->assertEquals('a_very_long_title_case', base\CamelCaseToUnderscore($str)); + } + + protected function _RandomHelper($arg, $lower, $upper) + { + $list = array(); + for ($i = 0; $i < 200; $i++) + { + $rand = base\Random($arg); + $this->assertNotContains($rand, $list, 'Duplicate random string!'); + $list[] = $rand; + $this->assertGreaterThanOrEqual($lower, strlen($rand), 'Random string not in lower bound'); + $this->assertLessThanOrEqual($upper, strlen($rand), 'Random string not in upper bound'); + } + } + + public function testRandomDefault() + { + $this->_RandomHelper(NULL, 20, 100); + } + + public function testRandomArgument() + { + $this->_RandomHelper(20, 20, 20); + } +} -- 2.22.5