Add unit testing infrastructure, again straignt from phalanx
authorRobert Sesek <rsesek@bluestatic.org>
Sun, 12 Jun 2011 00:17:55 +0000 (20:17 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Sun, 12 Jun 2011 00:17:55 +0000 (20:17 -0400)
.gitignore
testing/phpunit.xml [new file with mode: 0644]
testing/run_tests.sh [new file with mode: 0755]
testing/test_listener.php [new file with mode: 0644]
testing/test_runner.php [new file with mode: 0644]
testing/tests/base/functions_test.php [new file with mode: 0644]

index 4df8f532b8f3d43de77e2c7754be0bd9b52f6c04..0b3f74bd5dfa44cf1ad6df52c9998354d622e2c4 100644 (file)
@@ -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 (file)
index 0000000..fc4de5b
--- /dev/null
@@ -0,0 +1,38 @@
+<!--
+  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 <http://www.gnu.org/licenses/>.
+-->
+
+<!-- Tested with PHPUnit 3.5.5 -->
+<phpunit backupGlobals="FALSE"
+         backupStaticAttributes="FALSE"
+         colors="TRUE">
+
+    <testsuites>
+        <testsuite name="Hoplite Unit Tests">
+            <directory suffix="_test.php">./tests/</directory>
+        </testsuite>
+    </testsuites>
+
+    <filter>
+        <blacklist>
+            <directory>./</directory>  <!-- Don't get coverage for test files. -->
+        </blacklist>
+    </filter>
+
+    <logging>
+        <log type="coverage-html" target="./unittest_coverage" yui="TRUE" highlight="TRUE"/>
+    </logging>
+</phpunit>
diff --git a/testing/run_tests.sh b/testing/run_tests.sh
new file mode 100755 (executable)
index 0000000..7d0b907
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+
+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 (file)
index 0000000..e9a94bf
--- /dev/null
@@ -0,0 +1,254 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+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)
+// <http://googletest.googlecode.com> 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 (file)
index 0000000..0be30f8
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+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 (file)
index 0000000..a58b965
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+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);
+    }
+}