Skip to content
This repository was archived by the owner on Apr 14, 2025. It is now read-only.
Merged
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
7 changes: 0 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,3 @@ script:

git:
depth: 1

notifications:
irc:
channels:
- "irc.freenode.net#phpchunkit"
on_success: change

84 changes: 68 additions & 16 deletions src/FileClassesHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,86 @@
*/
class FileClassesHelper
{
public function getFileClasses(string $file) : array
/**
* Extract the classes in the given file
*
* Taken from https://github.com/composer/composer/blob/master/src/Composer/Autoload/ClassMapGenerator.php#L126
*
* @param string $path The file to check
*
* @return array The found classes
*/
public function getFileClasses(string $path) : array
{
// TODO: Figure out a better and/or faster way to do this.
// using php tokens to parse the information out is significantly slower.
$code = @file_get_contents($file);
$extraTypes = PHP_VERSION_ID < 50400 ? '' : '|trait';
if (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.3', '>=')) {
$extraTypes .= '|enum';
}

if (!$code) {
// Use @ here instead of Silencer to actively suppress 'unhelpful' output
// @link https://github.com/composer/composer/pull/4886
$contents = @php_strip_whitespace($path);
if (!$contents) {
return [];
}

preg_match_all('/namespace\s(.*);/', $code, $namespaces);

if (isset($namespaces[1][0])) {
$namespace = $namespaces[1][0];
// return early if there is no chance of matching anything in this file
if (!preg_match('{\b(?:class|interface'.$extraTypes.')\s}i', $contents)) {
return array();
}

if (!isset($namespace)) {
throw new \RuntimeException(sprintf('%s is missing a PHP namespace.'));
// strip heredocs/nowdocs
$contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents);
// strip strings
$contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents);
// strip leading non-php code if needed
if (substr($contents, 0, 2) !== '<?') {
$contents = preg_replace('{^.+?<\?}s', '<?', $contents, 1, $replacements);
if ($replacements === 0) {
return array();
}
}
// strip non-php blocks in the file
$contents = preg_replace('{\?>.+<\?}s', '?><?', $contents);
// strip trailing non-php code if needed
$pos = strrpos($contents, '?>');
if (false !== $pos && false === strpos(substr($contents, $pos), '<?')) {
$contents = substr($contents, 0, $pos);
}

preg_match_all('{
(?:
\b(?<![\$:>])(?P<type>class|interface'.$extraTypes.') \s++ (?P<name>[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+)
| \b(?<![\$:>])(?P<ns>namespace) (?P<nsname>\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;]
)
}ix', $contents, $matches);

preg_match_all('/class\s([a-zA-Z0-9_]+)/', $code, $classes);
$classes = array();
$namespace = '';

if (isset($classes[1])) {
foreach ($classes[1] as $className) {
$classNames[] = $namespace.'\\'.$className;
for ($i = 0, $len = count($matches['type']); $i < $len; $i++) {
if (!empty($matches['ns'][$i])) {
$namespace = str_replace(array(' ', "\t", "\r", "\n"), '', $matches['nsname'][$i]) . '\\';
} else {
$name = $matches['name'][$i];
// skip anon classes extending/implementing
if ($name === 'extends' || $name === 'implements') {
continue;
}
if ($name[0] === ':') {
// This is an XHP class, https://github.com/facebook/xhp
$name = 'xhp'.substr(str_replace(array('-', ':'), array('_', '__'), $name), 1);
} elseif ($matches['type'][$i] === 'enum') {
// In Hack, something like:
// enum Foo: int { HERP = '123'; }
// The regex above captures the colon, which isn't part of
// the class name.
$name = rtrim($name, ':');
}
$classes[] = ltrim($namespace . $name, '\\');
}
}

return $classNames;
return $classes;
}
}
45 changes: 44 additions & 1 deletion src/TestCounter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,35 @@ class TestCounter
*/
private $fileClassesHelper;

/**
* @var array
*/
private $cache = [];

/**
* @var string
*/
private $cachePath = '';

/**
* @param FileClassesHelper $fileClassesHelper
*/
public function __construct(FileClassesHelper $fileClassesHelper)
{
$this->fileClassesHelper = $fileClassesHelper;
$this->cachePath = sprintf('%s/testcounter.cache', sys_get_temp_dir());

$this->loadCache();
}

public function countNumTestsInFile(string $file) : int
{
$cacheKey = $file.@filemtime($file);

if (isset($this->cache[$cacheKey])) {
return $this->cache[$cacheKey];
}

$numTestsInFile = 0;

$classes = $this->fileClassesHelper->getFileClasses($file);
Expand Down Expand Up @@ -70,7 +89,31 @@ public function countNumTestsInFile(string $file) : int
}
}

$this->cache[$cacheKey] = $numTestsInFile;

return $numTestsInFile;
}
}

public function clearCache()
{
@unlink($this->cachePath);
$this->cache = [];
}

protected function loadCache()
{
if (file_exists($this->cachePath)) {
$this->cache = include($this->cachePath);
}
}

protected function writeCache()
{
file_put_contents($this->cachePath, '<?php return '.var_export($this->cache, true).';');
}

public function __destruct()
{
$this->writeCache();
}
}
31 changes: 29 additions & 2 deletions tests/TestCounterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,23 @@ class TestCounterTest extends BaseTest
protected function setUp()
{
$this->fileClassesHelper = $this->createMock(FileClassesHelper::class);
$this->testCounter = new TestCounter($this->fileClassesHelper);
$this->testCounter = new TestCounterStub($this->fileClassesHelper);
}

public function testCountNumTestsInFileCache()
{
$testCounter = new TestCounter($this->fileClassesHelper);
$testCounter->clearCache();

$this->fileClassesHelper->expects($this->exactly(1))
->method('getFileClasses')
->with(__FILE__)
->willReturn([
TestCounterTest::class
]);

$this->assertEquals(9, $testCounter->countNumTestsInFile(__FILE__));
$this->assertEquals(9, $testCounter->countNumTestsInFile(__FILE__));
}

public function testCountNumTestsInFile()
Expand All @@ -33,7 +49,7 @@ public function testCountNumTestsInFile()
TestCounterTest::class
]);

$this->assertEquals(8, $this->testCounter->countNumTestsInFile(__FILE__));
$this->assertEquals(9, $this->testCounter->countNumTestsInFile(__FILE__));
}

public function testCount1()
Expand Down Expand Up @@ -83,3 +99,14 @@ private function nonTestPrivateMethod()
{
}
}

class TestCounterStub extends TestCounter
{
protected function loadCache()
{
}

protected function writeCache()
{
}
}