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

[BridgeImplementationTest] Rewritten unit test to check bridges #980

Merged
merged 8 commits into from
Jan 8, 2019
306 changes: 161 additions & 145 deletions tests/BridgeImplementationTest.php
Original file line number Diff line number Diff line change
@@ -1,188 +1,204 @@
<?php

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestResult;
use PHPUnit\Framework\AssertionFailedError;

require_once __DIR__ . '/../lib/rssbridge.php';

/**
* This class checks bridges for implementation details:
*
* - A bridge must not implement public functions other than the ones specified
* by the bridge interfaces. Custom functions must be defined in private or
* protected scope.
* - getName() must return a valid string (non-empty)
* - getURI() must return a valid URI
* - A bridge must define constants for NAME, URI, DESCRIPTION and MAINTAINER,
* CACHE_TIMEOUT and PARAMETERS are optional
*/
final class BridgeImplementationTest extends TestCase {
private function CheckBridgePublicFunctions($bridgeName){

$parent_methods = array();

if(in_array('BridgeInterface', class_parents($bridgeName))) {
$parent_methods = array_merge($parent_methods, get_class_methods('BridgeInterface'));
}

if(in_array('BridgeAbstract', class_parents($bridgeName))) {
$parent_methods = array_merge($parent_methods, get_class_methods('BridgeAbstract'));
}

if(in_array('FeedExpander', class_parents($bridgeName))) {
$parent_methods = array_merge($parent_methods, get_class_methods('FeedExpander'));
}

// Receive all non abstract methods
$methods = array_diff(get_class_methods($bridgeName), $parent_methods);
$method_names = implode(', ', $methods);

$errmsg = $bridgeName
. ' implements additional public method(s): '
. $method_names
. '! Custom functions must be defined in private or protected scope!';

$this->assertEmpty($method_names, $errmsg);
use PHPUnit\Framework\TestCase;

class BridgeImplementationTest extends TestCase {
private $class;
private $obj;

/**
* @dataProvider dataBridgesProvider
*/
public function testClassName($path) {
$this->setBridge($path);
$this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
$this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
$this->assertStringEndsWith('Bridge', $this->class, 'class name must end with "Bridge"');
}
fulmeek marked this conversation as resolved.
Show resolved Hide resolved

private function CheckBridgeGetNameDefaultValue($bridgeName){

if(in_array('BridgeAbstract', class_parents($bridgeName))) { // Is bridge

if(!$this->isFunctionMemberOf($bridgeName, 'getName'))
return;

$bridge = new $bridgeName();
$abstract = new BridgeAbstractTest();

$message = $bridgeName . ': \'getName\' must return a valid name!';

$this->assertNotEmpty(trim($bridge->getName()), $message);

}

/**
* @dataProvider dataBridgesProvider
*/
public function testClassType($path) {
$this->setBridge($path);
$this->assertInstanceOf(BridgeInterface::class, $this->obj);
}

// Checks whether the getURI function returns empty or default values
private function CheckBridgeGetURIDefaultValue($bridgeName){

if(in_array('BridgeAbstract', class_parents($bridgeName))) { // Is bridge

if(!$this->isFunctionMemberOf($bridgeName, 'getURI'))
return;

$bridge = new $bridgeName();
$abstract = new BridgeAbstractTest();

$message = $bridgeName . ': \'getURI\' must return a valid URI!';

$this->assertNotEmpty(trim($bridge->getURI()), $message);

}

/**
* @dataProvider dataBridgesProvider
*/
public function testConstants($path) {
$this->setBridge($path);

$this->assertInternalType('string', $this->obj::NAME, 'class::NAME');
$this->assertNotEmpty($this->obj::NAME, 'class::NAME');
$this->assertInternalType('string', $this->obj::URI, 'class::URI');
$this->assertNotEmpty($this->obj::URI, 'class::URI');
$this->assertInternalType('string', $this->obj::DESCRIPTION, 'class::DESCRIPTION');
$this->assertNotEmpty($this->obj::DESCRIPTION, 'class::DESCRIPTION');
$this->assertInternalType('string', $this->obj::MAINTAINER, 'class::MAINTAINER');
$this->assertNotEmpty($this->obj::MAINTAINER, 'class::MAINTAINER');

$this->assertInternalType('array', $this->obj::PARAMETERS, 'class::PARAMETERS');
$this->assertInternalType('int', $this->obj::CACHE_TIMEOUT, 'class::CACHE_TIMEOUT');
$this->assertGreaterThanOrEqual(0, $this->obj::CACHE_TIMEOUT, 'class::CACHE_TIMEOUT');
}

private function CheckBridgePublicConstants($bridgeName){
/**
* @dataProvider dataBridgesProvider
*/
public function testParameters($path) {
$this->setBridge($path);

$multiMinimum = 2;
if (isset($this->obj::PARAMETERS['global'])) ++$multiMinimum;
$multiContexts = (count($this->obj::PARAMETERS) >= $multiMinimum);
$paramsSeen = array();

$allowedTypes = array(
'text',
'number',
'list',
'checkbox'
);

foreach($this->obj::PARAMETERS as $context => $params) {
if ($multiContexts) {
$this->assertInternalType('string', $context, 'invalid context name');
$this->assertNotEmpty($context, 'empty context name');
}

// Assertion only works for BridgeAbstract!
if(in_array('BridgeAbstract', class_parents($bridgeName))) {
foreach ($paramsSeen as $seen) {
$this->assertNotEquals($seen, $params, 'same set of parameters not allowed');
}
$paramsSeen[] = $params;

$ref = new ReflectionClass($bridgeName);
$constants = $ref->getConstants();
foreach ($params as $field => $options) {
$this->assertInternalType('string', $field, $field . ': invalid id');
$this->assertNotEmpty($field, $field . ':empty id');

$ref = new ReflectionClass('BridgeAbstract');
$parent_constants = $ref->getConstants();
$this->assertInternalType('string', $options['name'], $field . ': invalid name');
$this->assertNotEmpty($options['name'], $field . ': empty name');

foreach($parent_constants as $key => $value) {
if (isset($options['type'])) {
$this->assertInternalType('string', $options['type'], $field . ': invalid type');
$this->assertContains($options['type'], $allowedTypes, $field . ': unknown type');

$this->assertArrayHasKey($key, $constants, 'Constant ' . $key . ' missing in ' . $bridgeName);
if ($options['type'] == 'list') {
$this->assertArrayHasKey('values', $options, $field . ': missing list values');
$this->assertInternalType('array', $options['values'], $field . ': invalid list values');
$this->assertNotEmpty($options['values'], $field . ': empty list values');

// Skip optional constants
if($key !== 'PARAMETERS' && $key !== 'CACHE_TIMEOUT') {
$this->assertNotEquals($value, $constants[$key], 'Constant ' . $key . ' missing in ' . $bridgeName);
foreach ($options['values'] as $valueName => $value) {
$this->assertInternalType('string', $valueName, $field . ': invalid value name');
}
}
}

}

}

}
if (isset($options['required'])) {
$this->assertInternalType('bool', $options['required'], $field . ': invalid required');
}

private function isFunctionMemberOf($bridgeName, $functionName){
if (isset($options['title'])) {
$this->assertInternalType('string', $options['title'], $field . ': invalid title');
$this->assertNotEmpty($options['title'], $field . ': empty title');
}

$bridgeReflector = new ReflectionClass($bridgeName);
$bridgeMethods = $bridgeReflector->GetMethods();
$bridgeHasMethod = false;
if (isset($options['pattern'])) {
$this->assertInternalType('string', $options['pattern'], $field . ': invalid pattern');
$this->assertNotEquals('', $options['pattern'], $field . ': empty pattern');
logmanoriginal marked this conversation as resolved.
Show resolved Hide resolved
}

foreach($bridgeMethods as $method) {
if (isset($options['exampleValue'])) {
if (is_string($options['exampleValue']))
$this->assertNotEquals('', $options['exampleValue'], $field . ': empty exampleValue');
logmanoriginal marked this conversation as resolved.
Show resolved Hide resolved
}

if($method->name === $functionName && $method->class === $bridgeReflector->name) {
return true;
if (isset($options['defaultValue'])) {
if (is_string($options['defaultValue']))
$this->assertNotEquals('', $options['defaultValue'], $field . ': empty defaultValue');
logmanoriginal marked this conversation as resolved.
Show resolved Hide resolved
}
}

}

return false;

$this->assertTrue(true);
}

public function testBridgeImplementation($bridgeName){

require_once('bridges/' . $bridgeName . '.php');

$this->CheckBridgePublicFunctions($bridgeName);
$this->CheckBridgePublicConstants($bridgeName);
$this->CheckBridgeGetNameDefaultValue($bridgeName);
$this->CheckBridgeGetURIDefaultValue($bridgeName);

}

public function count() {
return count(Bridge::getBridgeNames());
}

public function run(TestResult $result = null) {

if ($result === null) {
$result = new TestResult;
/**
* @dataProvider dataBridgesProvider
*/
public function testVisibleMethods($path) {
$allowedBridgeAbstract = get_class_methods(BridgeAbstract::class);
sort($allowedBridgeAbstract);
$allowedFeedExpander = get_class_methods(FeedExpander::class);
sort($allowedFeedExpander);

$this->setBridge($path);

$methods = get_class_methods($this->obj);
sort($methods);
if ($this->obj instanceof FeedExpander) {
$this->assertEquals($allowedFeedExpander, $methods);
} else {
$this->assertEquals($allowedBridgeAbstract, $methods);
}
}

foreach (Bridge::getBridgeNames() as $bridge) {

$bridge .= 'Bridge';
/**
* @dataProvider dataBridgesProvider
*/
public function testMethodValues($path) {
$this->setBridge($path);

$result->startTest($this);
PHP_Timer::start();
$stopTime = null;
$value = $this->obj->getDescription();
$this->assertInternalType('string', $value, '$class->getDescription()');
$this->assertNotEmpty($value, '$class->getDescription()');

try {
$this->testBridgeImplementation($bridge);
} catch (AssertionFailedError $e) {
$value = $this->obj->getMaintainer();
$this->assertInternalType('string', $value, '$class->getMaintainer()');
$this->assertNotEmpty($value, '$class->getMaintainer()');

$stopTime = PHP_Timer::stop();
$result->addFailure($this, $e, $stopTime);
$value = $this->obj->getName();
$this->assertInternalType('string', $value, '$class->getName()');
$this->assertNotEmpty($value, '$class->getName()');

} catch (Exception $e) {
$value = $this->obj->getURI();
$this->assertInternalType('string', $value, '$class->getURI()');
$this->assertNotEmpty($value, '$class->getURI()');

$stopTime = PHP_Timer::stop();
$result->addError($this, $e, $stopTime);
$value = $this->obj->getIcon();
$this->assertInternalType('string', $value, '$class->getIcon()');
}

}
/**
* @dataProvider dataBridgesProvider
*/
public function testUri($path) {
$this->setBridge($path);

if ($stopTime === null) {
$stopTime = PHP_Timer::stop();
}
$this->checkUrl($this->obj::URI);
$this->checkUrl($this->obj->getURI());
}

$result->endTest($this, $stopTime);
////////////////////////////////////////////////////////////////////////////

public function dataBridgesProvider() {
$bridges = array();
foreach (glob(PATH_LIB_BRIDGES . '*.php') as $path) {
$bridges[basename($path, '.php')] = array($path);
}
return $bridges;
}

return $result;
private function setBridge($path) {
require_once $path;
$this->class = basename($path, '.php');
fulmeek marked this conversation as resolved.
Show resolved Hide resolved
$this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
$this->obj = new $this->class();
}
}

class BridgeAbstractTest extends BridgeAbstract {
public function collectData(){}
private function checkUrl($url) {
$this->assertNotFalse(filter_var($url, FILTER_VALIDATE_URL), 'no valid URL: ' . $url);
}
}