Commit cc34748c authored by alexpott's avatar alexpott

Issue #2408013 by Aki Tendo, alexpott, Fabianx, dawehner, Crell, jhodgdon, Wim...

Issue #2408013 by Aki Tendo, alexpott, Fabianx, dawehner, Crell, jhodgdon, Wim Leers, aspilicious: Adding Assertions to Drupal - Test Tools
parent 4b36aab5
......@@ -28,13 +28,14 @@ DirectoryIndex index.php index.html index.htm
AddType image/svg+xml svg svgz
AddEncoding gzip svgz
# Override PHP settings that cannot be changed at runtime. See
# Most of the following PHP settings cannot be changed at runtime. See
# sites/default/default.settings.php and
# Drupal\Core\DrupalKernel::bootEnvironment() for settings that can be
# changed at runtime.
# PHP 5, Apache 1 and 2.
<IfModule mod_php5.c>
php_value assert.active 0
php_flag session.auto_start off
php_value mbstring.http_input pass
php_value mbstring.http_output pass
......
......@@ -57,6 +57,7 @@
* - @link queue Queue API @endlink
* - @link typed_data Typed Data @endlink
* - @link testing Automated tests @endlink
* - @link php_assert PHP Runtime Assert Statements @endlink
* - @link third_party Integrating third-party applications @endlink
*
* @section more_info Further information
......@@ -982,6 +983,55 @@
* @}
*/
/**
* @defgroup php_assert PHP Runtime Assert Statements
* @{
* Use of the assert() statement in Drupal.
*
* Unit tests also use the term "assertion" to refer to test conditions, so to
* avoid confusion the term "runtime assertion" will be used for the assert()
* statement throughout the documentation.
*
* A runtime assertion is a statement that is expected to always be true at
* the point in the code it appears at. They are tested using PHP's internal
* @link http://www.php.net/assert assert() @endlink statement. If an
* assertion is ever FALSE it indicates an error in the code or in module or
* theme configuration files. User-provided configuration files should be
* verified with standard control structures at all times, not just checked in
* development environments with assert() statements on.
*
* When runtime assertions fail in PHP 7 an \AssertionException is thrown.
* Drupal uses an assertion callback to do the same in PHP 5.x so that unit
* tests involving runtime assertions will work uniformly across both versions.
*
* The Drupal project primarily uses runtime assertions to enforce the
* expectations of the API by failing when incorrect calls are made by code
* under development. While PHP type hinting does this for objects and arrays,
* runtime assertions do this for scalars (strings, integers, floats, etc.) and
* complex data structures such as cache and render arrays. They ensure that
* methods' return values are the documented datatypes. They also verify that
* objects have been properly configured and set up by the service container.
* Runtime assertions are checked throughout development. They supplement unit
* tests by checking scenarios that do not have unit tests written for them,
* and by testing the API calls made by all the code in the system.
*
* When using assert() keep the following in mind:
* - Runtime assertions are disabled by default in production and enabled in
* development, so they can't be used as control structures. Use exceptions
* for errors that can occur in production no matter how unlikely they are.
* - Assert() functions in a buggy manner prior to PHP 7. If you do not use a
* string for the first argument of the statement but instead use a function
* call or expression then that code will be evaluated even when runtime
* assertions are turned off. To avoid this you must use a string as the
* first argument, and assert will pass this string to the eval() statement.
* - Since runtime assertion strings are parsed by eval() use caution when
* using them to work with data that may be unsanitized.
*
* See https://www.drupal.org/node/2492225 for more information on runtime
* assertions.
* @}
*/
/**
* @defgroup info_types Information types
* @{
......
<?php
/**
* @file
* Contains \Drupal\Component\Assertion\Inspector.
*/
namespace Drupal\Component\Assertion;
use Traversable;
/**
* Generic inspections for the assert() statement.
*
* This is a static function collection for inspecting variable contents. All
* functions in this collection check a variable against an assertion about its
* structure.
*
* Example call:
* @code
* assert('Drupal\\Component\\Assertion\\Inspector::assertAllStrings($array)');
* @endcode
*
* @ingroup php_assert
*/
class Inspector {
/**
* Asserts argument can be traversed with foreach.
*
* @param mixed $traversable
* Variable to be examined.
*
* @return bool
* TRUE if $traversable can be traversed with foreach.
*/
public static function assertTraversable($traversable) {
return is_array($traversable) || $traversable instanceof Traversable;
}
/**
* Asserts callback returns TRUE for each member of a traversable.
*
* This is less memory intensive than using array_filter() to build a second
* array and then comparing the arrays. Many of the other functions in this
* collection alias this function passing a specific callback to make the
* code more readable.
*
* @param callable $callable
* Callback function.
* @param mixed $traversable
* Variable to be examined.
*
* @return bool
* TRUE if $traversable can be traversed and $callable returns TRUE on
* all members.
*
* @see http://php.net/manual/language.types.callable.php
*/
public static function assertAll(callable $callable, $traversable) {
if (static::assertTraversable($traversable)) {
foreach ($traversable as $member) {
if (!$callable($member)) {
return FALSE;
}
}
return TRUE;
}
return FALSE;
}
/**
* Asserts that all members are strings.
*
* @param mixed $traversable
* Variable to be examined.
*
* @return bool
* TRUE if $traversable can be traversed and all members are strings.
*/
public static function assertAllStrings($traversable) {
return static::assertAll('is_string', $traversable);
}
/**
* Asserts all members are strings or objects with magic __toString() method.
*
* @param mixed $traversable
* Variable to be examined.
*
* @return bool
* TRUE if $traversable can be traversed and all members are strings or
* objects with __toString().
*/
public static function assertAllStringable($traversable) {
if (static::assertTraversable($traversable)) {
foreach ($traversable as $member) {
if (!(is_string($member) || (is_object($member) && method_exists($member, '__toString')))) {
return FALSE;
}
}
return TRUE;
}
return FALSE;
}
/**
* Asserts that all members are arrays.
*
* @param mixed $traversable
* Variable to be examined.
*
* @return bool
* TRUE if $traversable can be traversed and all members are arrays.
*/
public static function assertAllArrays($traversable) {
return static::assertAll('is_array', $traversable);
}
/**
* Asserts that the array is strict.
*
* What PHP calls arrays are more formally called maps in most other
* programming languages. A map is a datatype that associates values to keys.
* The term 'strict array' here refers to a 0-indexed array in the classic
* sense found in programming languages other than PHP.
*
* @param mixed $array
* Variable to be examined.
*
* @return bool
* TRUE if $traversable is a 0-indexed array.
*
* @see http://php.net/manual/language.types.array.php
*/
public static function assertStrictArray($array) {
if (!is_array($array)) {
return FALSE;
}
$i = 0;
foreach (array_keys($array) as $key) {
if ($i !== $key) {
return FALSE;
}
$i++;
}
return TRUE;
}
/**
* Asserts all members are strict arrays.
*
* @param mixed $traversable
* Variable to be examined.
*
* @return bool
* TRUE if $traversable can be traversed and all members are strict arrays.
*
* @see ::assertStrictArray
*/
public static function assertAllStrictArrays($traversable) {
return static::assertAll([__CLASS__, 'assertStrictArray'], $traversable);
}
/**
* Asserts all given keys exist in every member array.
*
* Drupal has several data structure arrays that require certain keys be set.
* You can overload this function to specify a list of required keys. All
* of the keys must be set for this method to return TRUE.
*
* As an example, this assertion tests for the keys of a theme registry.
*
* @code
* assert('Drupal\\Component\\Assertion\\Inspector::assertAllHaveKey(
* $arrayToTest, "type", "theme path", "function", "template", "variables", "render element", "preprocess functions")');
* @endcode
*
* Note: If a method requires certain keys to be present it will usually be
* specific about the data types for the values of those keys. Therefore it
* will be best to write a specific test for it. Such tests are either bound
* to the object that uses them, or are collected into one assertion set for
* the package.
*
* @param mixed $traversable
* Variable to be examined.
* @param string ...
* Keys to be searched for.
*
* @return bool
* TRUE if $traversable can be traversed and all members have all keys.
*/
public static function assertAllHaveKey() {
$args = func_get_args();
$traversable = array_shift($args);
if (static::assertTraversable($traversable)) {
foreach ($traversable as $member) {
foreach ($args as $key) {
if (!array_key_exists($key, $member)) {
return FALSE;
}
}
}
return TRUE;
}
return FALSE;
}
/**
* Asserts that all members are integer values.
*
* @param mixed $traversable
* Variable to be examined.
*
* @return bool
* TRUE if $traversable can be traversed and all members are integers.
*/
public static function assertAllIntegers($traversable) {
return static::assertAll('is_int', $traversable);
}
/**
* Asserts that all members are float values.
*
* @param mixed $traversable
* Variable to be examined.
*
* @return bool
* TRUE if $traversable can be traversed and all members are floating point
* numbers.
*/
public static function assertAllFloat($traversable) {
return static::assertAll('is_float', $traversable);
}
/**
* Asserts that all members are callable.
*
* @param mixed $traversable
* Variable to be examined.
*
* @return bool
* TRUE if $traversable can be traversed and all members are callable.
*/
public static function assertAllCallable($traversable) {
return static::assertAll('is_callable', $traversable);
}
/**
* Asserts that all members are not empty.
*
* @param mixed $traversable
* Variable to be examined.
*
* @return bool
* TRUE if $traversable can be traversed and all members not empty.
*/
public static function assertAllNotEmpty($traversable) {
if (static::assertTraversable($traversable)) {
foreach ($traversable as $member) {
if (empty($member)) {
return FALSE;
}
}
return TRUE;
}
return FALSE;
}
/**
* Asserts all members are numeric data types or strings castable to such.
*
* @param mixed $traversable
* Variable to be examined.
*
* @return bool
* TRUE if $traversable can be traversed and all members are numeric.
*/
public static function assertAllNumeric($traversable) {
return static::assertAll('is_numeric', $traversable);
}
/**
* Asserts that all members are strings that contain the specified string.
*
* This runs faster than the regular expression equivalent.
*
* @param string $pattern
* The needle to find.
* @param mixed $traversable
* Variable to examine.
* @param bool $case_sensitive
* TRUE to use strstr(), FALSE to use stristr() which is case insensitive.
*
* @return bool
* TRUE if $traversable can be traversed and all members are strings
* containing $pattern.
*/
public static function assertAllMatch($pattern, $traversable, $case_sensitive = FALSE) {
if (static::assertTraversable($traversable)) {
if ($case_sensitive) {
foreach ($traversable as $member) {
if (!(is_string($member) && strstr($member, $pattern))) {
return FALSE;
}
}
}
else {
foreach ($traversable as $member) {
if (!(is_string($member) && stristr($member, $pattern))) {
return FALSE;
}
}
}
return TRUE;
}
return FALSE;
}
/**
* Asserts that all members are strings matching a regular expression.
*
* @param string $pattern
* Regular expression string to find.
* @param mixed $traversable
* Variable to be examined.
*
* @return bool
* TRUE if $traversable can be traversed and all members are strings
* matching $pattern.
*/
public static function assertAllRegularExpressionMatch($pattern, $traversable) {
if (static::assertTraversable($traversable)) {
foreach ($traversable as $member) {
if (!is_string($member)) {
return FALSE;
}
if (!preg_match($pattern, $member)) {
return FALSE;
}
}
return TRUE;
}
return FALSE;
}
/**
* Asserts that all members are objects.
*
* When testing if a collection is composed of objects those objects should
* be given a common interface to implement and the test should be written to
* search for just that interface. While this method will allow tests for
* just object status or for multiple classes and interfaces this was done to
* allow tests to be written for existing code without altering it. Only use
* this method in that manner when testing code from third party vendors.
*
* Here are some examples:
* @code
* // Just test all are objects, like a cache.
* assert('Drupal\\Component\\Assertion\\Inspector::assertAllObjects(
* $collection');
*
* // Test if traversable objects (arrays won't pass this)
* assert('Drupal\\Component\\Assertion\\Inspector::assertAllObjects(
* $collection', \'\\Traversable\');
*
* // Test for the Foo class or Bar\None interface
* assert('Drupal\\Component\\Assertion\\Inspector::assertAllObjects(
* $collection', \'\\Foo\', \'\\Bar\\None\'');
* @endcode
*
* @param mixed $traversable
* Variable to be examined.
* @param string ...
* Classes and interfaces to test objects against.
*
* @return bool
* TRUE if $traversable can be traversed and all members are objects with
* at least one of the listed classes or interfaces.
*/
public static function assertAllObjects() {
$args = func_get_args();
$traversable = array_shift($args);
if (static::assertTraversable($traversable)) {
foreach ($traversable as $member) {
if (count($args) > 0) {
foreach ($args as $instance) {
if ($member instanceof $instance) {
// We're continuing to the next member on the outer loop.
// @see http://php.net/continue
continue 2;
}
}
return FALSE;
}
elseif (!is_object($member)) {
return FALSE;
}
}
return TRUE;
}
return FALSE;
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\Component\InspectorTest.
*/
namespace Drupal\Tests\Component\Assertion;
use PHPUnit_Framework_TestCase;
use Drupal\Component\Assertion\Inspector;
/**
* @coversDefaultClass \Drupal\Component\Assertion\Inspector
* @group Inspector
*/
class InspectorTest extends PHPUnit_Framework_TestCase {
/**
* Tests asserting argument is an array or traversable object.
*
* @covers ::assertTraversable
*/
public function testAssertTraversable() {
$this->assertTrue(Inspector::assertTraversable([]));
$this->assertTrue(Inspector::assertTraversable(new \ArrayObject()));
$this->assertFalse(Inspector::assertTraversable(new \stdClass()));
$this->assertFalse(Inspector::assertTraversable('foo'));
}
/**
* Tests asserting all members are strings.
*
* @covers ::assertAllStrings
*/
public function testAssertAllStrings() {
$this->assertTrue(Inspector::assertAllStrings([]));
$this->assertTrue(Inspector::assertAllStrings(['foo', 'bar']));
$this->assertFalse(Inspector::assertAllStrings('foo'));
$this->assertFalse(Inspector::assertAllStrings(['foo', new StringObject()]));
}
/**
* Tests asserting all members are strings or objects with __toString().
*
* @covers ::assertAllStringable
*/
public function testAssertAllStringable() {
$this->assertTrue(Inspector::assertAllStringable([]));
$this->assertTrue(Inspector::assertAllStringable(['foo', 'bar']));
$this->assertFalse(Inspector::assertAllStringable('foo'));
$this->assertTrue(Inspector::assertAllStringable(['foo', new StringObject()]));
}
/**
* Tests asserting all members are arrays.
*
* @covers ::assertAllArrays
*/
public function testAssertAllArrays() {
$this->assertTrue(Inspector::assertAllArrays([]));
$this->assertTrue(Inspector::assertAllArrays([[], []]));
$this->assertFalse(Inspector::assertAllArrays([[], 'foo']));
}
/**
* Tests asserting array is 0-indexed - the strict definition of array.
*
* @covers ::assertStrictArray
*/
public function testAssertStrictArray() {
$this->assertTrue(Inspector::assertStrictArray([]));
$this->assertTrue(Inspector::assertStrictArray(['bar', 'foo']));
$this->assertFalse(Inspector::assertStrictArray(['foo' => 'bar', 'bar' => 'foo']));
}
/**
* Tests asserting all members are strict arrays.
*
* @covers ::assertAllStrictArrays
*/
public function testAssertAllStrictArrays() {
$this->assertTrue(Inspector::assertAllStrictArrays([]));
$this->assertTrue(Inspector::assertAllStrictArrays([[], []]));
$this->assertFalse(Inspector::assertAllStrictArrays([['foo' => 'bar', 'bar' => 'foo']]));
}
/**
* Tests asserting all members have specified keys.
*
* @covers ::assertAllHaveKey
*/
public function testAssertAllHaveKey() {
$this->assertTrue(Inspector::assertAllHaveKey([]));
$this->assertTrue(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']]));
$this->assertTrue(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'foo'));
$this->assertTrue(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'bar', 'foo'));
$this->assertFalse(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'bar', 'foo', 'moo'));
}
/**
* Tests asserting all members are integers.
*
* @covers ::assertAllIntegers
*/
public function testAssertAllIntegers() {
$this->assertTrue(Inspector::assertAllIntegers([]));
$this->assertTrue(Inspector::assertAllIntegers([1, 2, 3]));
$this->assertFalse(Inspector::assertAllIntegers([1, 2, 3.14]));
$this->assertFalse(Inspector::assertAllIntegers([1, '2', 3]));
}
/**
* Tests asserting all members are floating point variables.
*
* @covers ::assertAllFloat
*/
public function testAssertAllFloat() {
$this->assertTrue(Inspector::assertAllFloat([]));
$this->assertTrue(Inspector::assertAllFloat([1.0, 2.1, 3.14]));
$this->assertFalse(Inspector::assertAllFloat([1, 2.1, 3.14]));
$this->assertFalse(Inspector::assertAllFloat([1.0, '2', 3]));
$this->assertFalse(Inspector::assertAllFloat(['Titanic']));
}
/**
* Tests asserting all members are callable.
*
* @covers ::assertAllCallable
*/
public function testAllCallable() {
$this->assertTrue(Inspector::assertAllCallable([
'strchr',
[$this, 'callMe'],
[__CLASS__, 'callMeStatic'],
function() {
return TRUE;
}
]));
$this->assertFalse(Inspector::assertAllCallable([
'strchr',
[$this, 'callMe'],
[__CLASS__, 'callMeStatic'],
function() {
return TRUE;
},
"I'm not callable"
]));