Skip to content

Instantly share code, notes, and snippets.

@jm42
Created December 10, 2015 01:00
Show Gist options
  • Save jm42/bace243dd56f251bf9f7 to your computer and use it in GitHub Desktop.
Save jm42/bace243dd56f251bf9f7 to your computer and use it in GitHub Desktop.
Unit testing from documentation
<?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);
<?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