Created
December 10, 2015 01:00
-
-
Save jm42/bace243dd56f251bf9f7 to your computer and use it in GitHub Desktop.
Unit testing from documentation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * PHP Doc. Tests | |
| * ============== | |
| * | |
| * Unit testing library that extract tests from documentation. | |
| * | |
| * This is only a POC and should not be used in production. | |
| * | |
| * Under the WTFPL-2.0 license. | |
| */ | |
| class Parser | |
| { | |
| const KW_PREFIX = 'php>'; | |
| public function parse(array $files) | |
| { | |
| $cases = array(); | |
| foreach ($files as $filename) { | |
| $source = @file_get_contents($filename); | |
| $tokens = @token_get_all($source); | |
| if ($tokens === false) { | |
| continue; | |
| } | |
| foreach ($tokens as $token) { | |
| if (is_array($token) && $token[0] === T_DOC_COMMENT) { | |
| if (substr($token[1], 0, 2) === '/*') { | |
| $tests = $this->parseComment($token[1]); | |
| if (count($tests) > 0) { | |
| $cases[] = new TestCase($filename, $tests); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return $cases; | |
| } | |
| protected function parseComment($comment) | |
| { | |
| $lines = explode("\n", $comment); | |
| $tests = array(); | |
| for ($n = 0, $l = count($lines); $n < $l; $n++) { | |
| $curr = $this->parseCommentLine($lines[$n]); | |
| if ($curr[0]) { | |
| $source = $curr[1]; | |
| $expect = null; | |
| if (isset($lines[$n + 1])) { | |
| $next = $this->parseCommentLine($lines[$n + 1]); | |
| if (!$next[0] && !empty($next[1])) { | |
| $expect = $next[1]; $n++; | |
| } | |
| } | |
| $tests[] = new Test($source, $expect); | |
| } | |
| } | |
| return $tests; | |
| } | |
| protected function parseCommentLine($line) | |
| { | |
| $line = ltrim($line); | |
| if (substr($line, 0, 1) == '*') { | |
| $line = ltrim(substr($line, 1)); | |
| } | |
| if (substr($line, 0, strlen(self::KW_PREFIX)) === self::KW_PREFIX) { | |
| return array(true, trim(substr($line, strlen(self::KW_PREFIX)))); | |
| } | |
| return array(false, trim($line)); | |
| } | |
| } | |
| class Test | |
| { | |
| public $source; | |
| public $expect; | |
| public function __construct($source, $expect) | |
| { | |
| $this->source = $source; | |
| $this->expect = $expect; | |
| } | |
| } | |
| class TestCase | |
| { | |
| public $filename; | |
| protected $tests = array(); | |
| public function __construct($filename, array $tests = array()) | |
| { | |
| $this->filename = $filename; | |
| foreach ($tests as $test) { | |
| $this->add($test); | |
| } | |
| } | |
| public function add(Test $test) | |
| { | |
| $this->tests[] = $test; | |
| } | |
| public function getCode() | |
| { | |
| $php = '$__results = array();'; | |
| foreach ($this->tests as $i => $test) { | |
| $source = $test->source; | |
| $expect = $test->expect; | |
| if ($expect !== null) { | |
| if (substr($expect, 0, 6) === 'catch ') { | |
| list($class, $message) = explode(':', substr($expect, 6), 2); | |
| $msg = ''; | |
| if (!empty($message)) { | |
| $msg = 'assert($e->getMessage() === ' . var_export(trim($message), true) . ');'; | |
| } | |
| $php .= 'try { ' . $source . ' } catch (' . $class . ' $e) { ' . $msg . ' }'; | |
| } else { | |
| if (substr($source, -1) === ';') { | |
| $source = substr($source, 0, -1); | |
| } | |
| $php .= '$__results[' . $i . '] = (' . $source . ');'; | |
| $php .= 'assert($__results[' . $i . '] === ' . $expect . ');'; | |
| } | |
| } else { | |
| $php .= $source; | |
| } | |
| } | |
| return $php; | |
| } | |
| } | |
| class TestCaseResult | |
| { | |
| const STATE_SKIPPED = 1; | |
| const STATE_PASSED = 2; | |
| const STATE_FAILED = 3; | |
| const STATE_ERROR = 4; | |
| protected $test; | |
| protected $result; | |
| protected $message; | |
| public function __construct(TestCase $test, $result, $message='') | |
| { | |
| $this->test = $test; | |
| $this->result = $result; | |
| $this->message = $message; | |
| } | |
| public function getMessage() | |
| { | |
| return $this->message; | |
| } | |
| public function isSkipped() | |
| { | |
| return $this->result === self::STATE_SKIPPED; | |
| } | |
| public function isPassed() | |
| { | |
| return $this->result === self::STATE_PASSED; | |
| } | |
| public function isFailed() | |
| { | |
| return $this->result === self::STATE_FAILED; | |
| } | |
| public function isError() | |
| { | |
| return $this->result === self::STATE_ERROR; | |
| } | |
| } | |
| class Runner | |
| { | |
| const TMPL = '<?php require "{filename}"; {code} ?>'; | |
| public function run(TestCase $test) | |
| { | |
| $code = strtr(self::TMPL, array( | |
| '{filename}' => $test->filename, | |
| '{code}' => $test->getCode(), | |
| )); | |
| $error = $this->execute($code); | |
| if (empty($error)) { | |
| $result = TestCaseResult::STATE_PASSED; | |
| $message = ''; | |
| } else { | |
| $result = TestCaseResult::STATE_FAILED; | |
| $message = explode("\n", $error)[1]; | |
| } | |
| return new TestCaseResult($test, $result, $message); | |
| } | |
| protected function execute($php) | |
| { | |
| $descriptorspec = array( | |
| array('pipe', 'r'), | |
| array('pipe', 'w'), | |
| array('pipe', 'a'), | |
| ); | |
| $process = proc_open('php', $descriptorspec, $pipes); | |
| if (is_resource($process)) { | |
| fwrite($pipes[0], $php); | |
| fclose($pipes[0]); | |
| $output = stream_get_contents($pipes[1]); | |
| fclose($pipes[1]); | |
| proc_close($process); | |
| return $output; | |
| } | |
| return null; | |
| } | |
| } | |
| class Reporter | |
| { | |
| protected $count = 0; | |
| protected $errors = array(); | |
| public function start() | |
| { | |
| echo 'Testing...' . PHP_EOL . PHP_EOL; | |
| } | |
| public function end() | |
| { | |
| echo PHP_EOL . PHP_EOL; | |
| if (count($this->errors) === 0) { | |
| echo 'OK (' . $this->count . ' tests)' . PHP_EOL; | |
| } else { | |
| foreach ($this->errors as $error) { | |
| echo $error . PHP_EOL; | |
| } | |
| echo PHP_EOL; | |
| echo 'FAIL (' . $this->count . ' tests, ' . count($this->errors) . ' errors)' . PHP_EOL; | |
| } | |
| } | |
| public function startTest(TestCase $test) | |
| { | |
| $this->count++; | |
| } | |
| public function endTest(TestCase $test, TestCaseResult $result) | |
| { | |
| if ($result->isPassed()) { | |
| echo '.'; | |
| } elseif ($result->isFailed()) { | |
| echo 'F'; | |
| $this->errors[] = $result->getMessage(); | |
| } elseif ($result->isError()) { | |
| echo 'E'; | |
| $this->errors[] = $result->getMessage(); | |
| } | |
| } | |
| } | |
| function main($argv) | |
| { | |
| if (count($argv) == 1) { | |
| echo 'usage: ' . basename($argv[0]) . ' <testfile> [<testfile>]...' . PHP_EOL; | |
| exit(1); | |
| } | |
| $parser = new Parser(); | |
| $runner = new Runner(); | |
| $reporter = new Reporter(); | |
| $reporter->start(); | |
| $files = array_slice($argv, 1); | |
| $tests = $parser->parse($files); | |
| foreach ($tests as $test) { | |
| $reporter->startTest($test); | |
| $result = $runner->run($test); | |
| $reporter->endTest($test, $result); | |
| } | |
| $reporter->end(); | |
| } | |
| main($argv); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * This is my SUM. | |
| * | |
| * php> mysum(1, 2); | |
| * 3 | |
| * php> $sum = mysum(2, 3); | |
| * php> mysum(10, 2) - $sum; | |
| * 7 | |
| * | |
| * @param number $a | |
| * @param number $b | |
| * | |
| * @return number | |
| */ | |
| function mysum($a, $b) | |
| { | |
| return $a + $b; | |
| } | |
| /** | |
| * This is my DIV. | |
| * | |
| * php> mydiv(10, 2); | |
| * 5 | |
| * php> mydiv(10, 0); | |
| * catch UnexpectedValueException: Cannot divide by zero | |
| * | |
| * @param number $a | |
| * @param number $b | |
| * | |
| * @return number | |
| */ | |
| function mydiv($a, $b) | |
| { | |
| if (0 === $b) { | |
| throw new UnexpectedValueException('Cannot divide by zero'); | |
| } | |
| return $a / $b; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment