Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TestCaseRunner [WIP] #397

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions demo/TestFour.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);


class TestFour extends Tester\TestCase
{
public function testMe()
{
Tester\Assert::true(true);
echo __FUNCTION__ . ',';
}


public function testMe2()
{
echo __FUNCTION__ . ',';
}
}
13 changes: 13 additions & 0 deletions demo/TestOne.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);


class TestOne extends Tester\TestCase
{
public function testMe()
{
Tester\Assert::true(true);
echo __FUNCTION__ . ',';
}
}
13 changes: 13 additions & 0 deletions demo/TestThree.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);


class TestThree extends Tester\TestCase
{
public function testMe()
{
Tester\Assert::true(true);
echo __FUNCTION__ . ',';
}
}
13 changes: 13 additions & 0 deletions demo/TestTwo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);


class TestTwo extends Tester\TestCase
{
public function testMe()
{
Tester\Assert::true(true);
echo __FUNCTION__ . ',';
}
}
10 changes: 10 additions & 0 deletions demo/runner.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

require __DIR__ . '/../src/bootstrap.php';


(new Tester\TestCaseRunner)
->findTests(__DIR__ . '/Test*.php')
->run();
27 changes: 27 additions & 0 deletions demo2/runner.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

require __DIR__ . '/../src/bootstrap.php';


class MyTest extends Tester\TestCase
{
public function testMe1()
{
Tester\Assert::true(true);
echo __FUNCTION__ . ',';
}


public function testMe2()
{
echo __FUNCTION__ . ',';
}
}


(new Tester\TestCaseRunner)
->run(new MyTest);

// this could be the recommended way to run single test, that will replace @testcase
61 changes: 41 additions & 20 deletions src/Framework/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@
*/
class TestCase
{
/** @internal */
public const
LIST_METHODS = 'nette-tester-list-methods',
METHOD_PATTERN = '#^test[A-Z0-9_]#';
public const LIST_METHODS = 'nette-tester-list-methods';
private const METHOD_PATTERN = '#^test[A-Z0-9_]#';

/** @var bool */
private $handleErrors = false;
Expand All @@ -36,25 +34,48 @@ public function run(): void
throw new \LogicException('Calling TestCase::run($method) is deprecated. Use TestCase::runTest($method) instead.');
}

$methods = array_values(preg_grep(self::METHOD_PATTERN, array_map(function (\ReflectionMethod $rm): string {
return $rm->getName();
}, (new \ReflectionObject($this))->getMethods())));

if (isset($_SERVER['argv']) && ($tmp = preg_filter('#--method=([\w-]+)$#Ai', '$1', $_SERVER['argv']))) {
$method = reset($tmp);
if ($method === self::LIST_METHODS) {
Environment::$checkAssertions = false;
header('Content-Type: text/plain');
echo '[' . implode(',', $methods) . ']';
return;
}
if ($this->runFromCli()) {
return;
}

foreach ($this::findMethods() as $method) {
$this->runTest($method);
}
}

} else {
foreach ($methods as $method) {
$this->runTest($method);
}

public static function findMethods(): array
{
$methods = (new \ReflectionClass(static::class))->getMethods();
return array_values(preg_grep(self::METHOD_PATTERN, array_map(function (\ReflectionMethod $rm): string {
return $rm->getName();
}, $methods)));
}


private function runFromCli(): bool
{
$args = preg_filter('#--method=([\w-]+)$#Ai', '$1', $_SERVER['argv'] ?? []);
$arg = reset($args);
if (!$arg) {
return false;
} elseif ($arg === self::LIST_METHODS) {
Environment::$checkAssertions = false;
header('Content-Type: text/plain');
echo '[' . implode(',', $this::findMethods()) . ']';
} elseif ($arg) {
$this->runTest($arg);
}
return true;
}


public static function parseOutput(string $output): ?array
{
if (!preg_match('#\[([^[]*)]#', (string) strrchr($output, '['), $m)) {
return null;
}
return $m[1] ? explode(',', $m[1]) : [];
}


Expand Down
91 changes: 91 additions & 0 deletions src/Framework/TestCaseRunner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

/**
* This file is part of the Nette Tester.
* Copyright (c) 2009 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Tester;


/**
* Runner of TestCase.
*/
class TestCaseRunner
{
/** @var array */
private $classes = [];


public function findTests(string $fileMask): self
{
$files = [];
foreach (glob($fileMask) as $file) {
require_once $file;
$files[realpath($file)] = true;
}
foreach (get_declared_classes() as $class) {
$rc = new \ReflectionClass($class);
if ($rc->isSubclassOf(TestCase::class) && isset($files[$rc->getFileName()])) {
$this->classes[] = $class;
}
}
return $this;
}


public function run(TestCase $test = null): void
{
if ($this->runFromCli($test)) {
return;

} elseif ($test) {
$test->run();

} else {
foreach ($this->classes as $class) {
$test = $this->createInstance($class);
$test->run();
}
}
}


private function runFromCli(TestCase $test = null): bool
{
$args = preg_filter('#--method=([\w:-]+)$#Ai', '$1', $_SERVER['argv'] ?? []);
$arg = reset($args);

if ($arg) {
[$class, $method] = explode('::', $arg);
$test = $test ?: $this->createInstance($class);
$test->runTest($method);
return true;

} elseif (getenv(Environment::RUNNER)) {
Environment::$checkAssertions = false;
$methods = [];
$classes = $test ? [get_class($test)] : $this->classes;
foreach ($classes as $class) {
foreach ($class::findMethods() as $method) {
$methods[] = $class . '::' . $method;
}
}
header('Content-Type: text/plain');
echo '[' . implode(',', $methods) . ']';
exit(Runner\Job::CODE_TESTCASE);

} else {
return false;
}
}


protected function createInstance(string $class): TestCase
{
// TOO: can be altered via setFactory(callable)
return new $class;
}
}
1 change: 1 addition & 0 deletions src/Runner/Job.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Job
CODE_OK = 0,
CODE_SKIP = 177,
CODE_FAIL = 178,
CODE_TESTCASE = 179,
CODE_ERROR = 255;

/** waiting time between process activity check in microseconds */
Expand Down
27 changes: 21 additions & 6 deletions src/Runner/TestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,30 +164,45 @@ private function initiateTestCase(Test $test, $foo, PhpInterpreter $interpreter)
return $test->withResult($job->getExitCode() === Job::CODE_SKIP ? Test::SKIPPED : Test::FAILED, $job->getTest()->stdout);
}

if (!preg_match('#\[([^[]*)]#', (string) strrchr($job->getTest()->stdout, '['), $m)) {
$methods = TestCase::parseOutput($job->getTest()->stdout);
if ($methods === null) {
return $test->withResult(Test::FAILED, "Cannot list TestCase methods in file '{$test->getFile()}'. Do you call TestCase::run() in it?");
} elseif (!strlen($m[1])) {
} elseif (!$methods) {
return $test->withResult(Test::SKIPPED, "TestCase in file '{$test->getFile()}' does not contain test methods.");
}

return array_map(function (string $method) use ($test): Test {
return $test->withArguments(['method' => $method]);
}, explode(',', $m[1]));
}, $methods);
}


private function assessExitCode(Job $job, $code): ?Test
{
$test = $job->getTest();
$code = (int) $code;
if ($job->getExitCode() === Job::CODE_SKIP) {
$message = preg_match('#.*Skipped:\n(.*?)\z#s', $output = $job->getTest()->stdout, $m)
$message = preg_match('#.*Skipped:\n(.*?)\z#s', $output = $test->stdout, $m)
? $m[1]
: $output;
return $job->getTest()->withResult(Test::SKIPPED, trim($message));
return $test->withResult(Test::SKIPPED, trim($message));

} elseif ($job->getExitCode() === Job::CODE_TESTCASE) {
$methods = TestCase::parseOutput($test->stdout);
if ($methods === null) {
return $test->withResult(Test::FAILED, "Cannot read TestCaseRunner output in file '{$test->getFile()}'.");
} elseif (!$methods) {
return $test->withResult(Test::SKIPPED, "TestCaseRunner in file '{$test->getFile()}' does not contain any test.");
}
foreach ($methods as $method) {
$testVariety = $test->withArguments(['method' => $method]);
$this->runner->prepareTest($testVariety);
$this->runner->addJob(new Job($testVariety, $this->runner->getInterpreter(), $this->runner->getEnvironmentVariables()));
}

} elseif ($job->getExitCode() !== $code) {
$message = $job->getExitCode() !== Job::CODE_FAIL ? "Exited with error code {$job->getExitCode()} (expected $code)" : '';
return $job->getTest()->withResult(Test::FAILED, trim($message . "\n" . $job->getTest()->stdout));
return $test->withResult(Test::FAILED, trim($message . "\n" . $test->stdout));
}
return null;
}
Expand Down
1 change: 1 addition & 0 deletions src/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
require __DIR__ . '/Framework/Dumper.php';
require __DIR__ . '/Framework/FileMock.php';
require __DIR__ . '/Framework/TestCase.php';
require __DIR__ . '/Framework/TestCaseRunner.php';
require __DIR__ . '/Framework/DomQuery.php';
require __DIR__ . '/Framework/FileMutator.php';
require __DIR__ . '/CodeCoverage/Collector.php';
Expand Down
1 change: 1 addition & 0 deletions src/tester.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
require __DIR__ . '/Framework/Dumper.php';
require __DIR__ . '/Framework/DataProvider.php';
require __DIR__ . '/Framework/TestCase.php';
require __DIR__ . '/Framework/TestCaseRunner.php';
require __DIR__ . '/CodeCoverage/PhpParser.php';
require __DIR__ . '/CodeCoverage/Generators/AbstractGenerator.php';
require __DIR__ . '/CodeCoverage/Generators/HtmlGenerator.php';
Expand Down