diff --git a/.htaccess b/.htaccess index af418c4..bf725f2 100644 --- a/.htaccess +++ b/.htaccess @@ -28,13 +28,15 @@ 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. +# changed at runtime. See sites/example.settings.local.php for more +# information on assert.active. # PHP 5, Apache 1 and 2. + php_value assert.active 0 php_flag session.auto_start off php_value mbstring.http_input pass php_value mbstring.http_output pass diff --git a/core/core.api.php b/core/core.api.php index 63fd607..5410b89 100644 --- a/core/core.api.php +++ b/core/core.api.php @@ -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 @@ -983,6 +984,54 @@ */ /** + * @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. + * + * The Drupal project primarly 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 are used to + * insure that methods return values in 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. + * See sites/example.settings.local.php for more information. + * - Assert() has functioned in a buggy manner that has been fixed in 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 * @{ * Types of information in Drupal. diff --git a/core/includes/testing.inc b/core/includes/testing.inc new file mode 100644 index 0000000..5163182 --- /dev/null +++ b/core/includes/testing.inc @@ -0,0 +1,39 @@ += 0) { + ini_set('assert.exception', 1); +} +// PHP 5.x, make runtime assertions throw as they would in PHP 7. +else { + class AssertionException extends Exception {} + assert_options(ASSERT_ACTIVE, 1); + assert_options(ASSERT_CALLBACK, function($file, $line, $code, $message = ''){ + if (empty($message)) { + $message = "Assertion Failure in {$file} at {$line}. Failed asserting {$code}"; + } + throw new AssertionException($message); + }); +} diff --git a/core/lib/Drupal/Component/Assertion/Assertion.php b/core/lib/Drupal/Component/Assertion/Assertion.php new file mode 100644 index 0000000..85984b6 --- /dev/null +++ b/core/lib/Drupal/Component/Assertion/Assertion.php @@ -0,0 +1,411 @@ + 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; + } + +} diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index 8ac63df..6fab9c3 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -13,6 +13,7 @@ * @file * Provides testing functionality. */ +require_once DRUPAL_ROOT . '/core/includes/testing.inc'; /** * Implements hook_help(). diff --git a/core/tests/Drupal/Tests/AssertionThrowTest.php b/core/tests/Drupal/Tests/AssertionThrowTest.php new file mode 100644 index 0000000..63537c5 --- /dev/null +++ b/core/tests/Drupal/Tests/AssertionThrowTest.php @@ -0,0 +1,38 @@ +assertTrue(false); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Assertion/AssertionTest.php b/core/tests/Drupal/Tests/Component/Assertion/AssertionTest.php new file mode 100644 index 0000000..e451152 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Assertion/AssertionTest.php @@ -0,0 +1,246 @@ +assertTrue(Assertion::assertTraversable([])); + $this->assertTrue(Assertion::assertTraversable(new ArrayObject())); + $this->assertFalse(Assertion::assertTraversable(new stdClass())); + $this->assertFalse(Assertion::assertTraversable('foo')); + } + + /** + * Tests asserting all members are strings. + * + * @covers ::assertAllStrings + */ + public function testAssertAllStrings() { + $this->assertTrue(Assertion::assertAllStrings([])); + $this->assertTrue(Assertion::assertAllStrings(['foo', 'bar'])); + $this->assertFalse(Assertion::assertAllStrings('foo')); + $this->assertFalse(Assertion::assertAllStrings(['foo', new StringObject()])); + } + + /** + * Tests asserting all members are strings or objects with __toString(). + * + * @covers ::assertAllStringable + */ + public function testAssertAllStringable() { + $this->assertTrue(Assertion::assertAllStringable([])); + $this->assertTrue(Assertion::assertAllStringable(['foo', 'bar'])); + $this->assertFalse(Assertion::assertAllStringable('foo')); + $this->assertTrue(Assertion::assertAllStringable(['foo', new StringObject()])); + } + + /** + * Tests asserting all members are arrays. + * + * @covers ::assertAllAssociativeArrays + */ + public function testAssertAllAssociativeArrays() { + $this->assertTrue(Assertion::assertAllAssociativeArrays([])); + $this->assertTrue(Assertion::assertAllAssociativeArrays([[], []])); + $this->assertFalse(Assertion::assertAllAssociativeArrays([[], 'foo'])); + } + + /** + * Tests asserting array is 0-indexed - the strict definition of array. + * + * @covers ::assertStrictArray + */ + public function testAssertStrictArray() { + $this->assertTrue(Assertion::assertStrictArray([])); + $this->assertTrue(Assertion::assertStrictArray(['bar', 'foo'])); + $this->assertFalse(Assertion::assertStrictArray(['foo' => 'bar', 'bar' => 'foo'])); + } + + /** + * Tests asserting all members are strict arrays. + * + * @covers ::assertAllStrictArrays + */ + public function testAssertAllStrictArrays() { + $this->assertTrue(Assertion::assertAllStrictArrays([])); + $this->assertTrue(Assertion::assertAllStrictArrays([[], []])); + $this->assertFalse(Assertion::assertAllStrictArrays([['foo' => 'bar', 'bar' => 'foo']])); + } + + /** + * Tests asserting all members have specified keys. + * + * @covers ::assertAllHaveKey + */ + public function testAssertAllHaveKey() { + $this->assertTrue(Assertion::assertAllHaveKey([])); + $this->assertTrue(Assertion::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']])); + $this->assertTrue(Assertion::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'foo')); + $this->assertTrue(Assertion::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'bar', 'foo')); + $this->assertFalse(Assertion::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'bar', 'foo', 'moo')); + } + + /** + * Tests asserting all members are integers. + * + * @covers ::assertAllIntegers + */ + public function testAssertAllIntegers() { + $this->assertTrue(Assertion::assertAllIntegers([])); + $this->assertTrue(Assertion::assertAllIntegers([1, 2, 3])); + $this->assertFalse(Assertion::assertAllIntegers([1, 2, 3.14])); + $this->assertFalse(Assertion::assertAllIntegers([1, '2', 3])); + } + + /** + * Tests asserting all members are floating point variables. + * + * @covers ::assertAllFloat + */ + public function testAssertAllFloat() { + $this->assertTrue(Assertion::assertAllFloat([])); + $this->assertTrue(Assertion::assertAllFloat([1.0, 2.1, 3.14])); + $this->assertFalse(Assertion::assertAllFloat([1, 2.1, 3.14])); + $this->assertFalse(Assertion::assertAllFloat([1.0, '2', 3])); + $this->assertFalse(Assertion::assertAllFloat(['Titanic'])); + } + + /** + * Tests asserting all members are callable. + * + * @covers ::assertAllCallable + */ + public function testAllCallable() { + $this->assertTrue(Assertion::assertAllCallable([ + 'strchr', + [$this, 'callMe'], + [__CLASS__, 'callMeStatic'], + function() { + return TRUE; + } + ])); + + $this->assertFalse(Assertion::assertAllCallable([ + 'strchr', + [$this, 'callMe'], + [__CLASS__, 'callMeStatic'], + function() { + return TRUE; + }, + "I'm not callable" + ])); + } + + /** + * Tests asserting all members are !empty(). + * + * @covers ::assertAllNotEmpty + */ + public function testAllNotEmpty() { + $this->assertTrue(Assertion::assertAllNotEmpty([1, 'two'])); + $this->assertFalse(Assertion::assertAllNotEmpty([''])); + } + + /** + * Tests asserting all arguments are numbers or strings castable to numbers. + * + * @covers ::assertAllNumeric + */ + public function testAssertAllNumeric() { + $this->assertTrue(Assertion::assertAllNumeric([1, '2', 3.14])); + $this->assertFalse(Assertion::assertAllNumeric([1, 'two', 3.14])); + } + + /** + * Tests asserting strstr() or stristr() match. + * + * @covers ::assertAllMatch + */ + public function testAssertAllMatch() { + $this->assertTrue(Assertion::assertAllMatch('f', ['fee', 'fi', 'fo'])); + $this->assertTrue(Assertion::assertAllMatch('F', ['fee', 'fi', 'fo'])); + $this->assertTrue(Assertion::assertAllMatch('f', ['fee', 'fi', 'fo'], TRUE)); + $this->assertFalse(Assertion::assertAllMatch('F', ['fee', 'fi', 'fo'], TRUE)); + $this->assertFalse(Assertion::assertAllMatch('e', ['fee', 'fi', 'fo'])); + $this->assertFalse(Assertion::assertAllMatch('1', [12])); + } + + /** + * Tests asserting regular expression match. + * + * @covers ::assertAllRegularExpressionMatch + */ + public function testAssertAllRegularExpressionMatch() { + $this->assertTrue(Assertion::assertAllRegularExpressionMatch('/f/i', ['fee', 'fi', 'fo'])); + $this->assertTrue(Assertion::assertAllRegularExpressionMatch('/F/i', ['fee', 'fi', 'fo'])); + $this->assertTrue(Assertion::assertAllRegularExpressionMatch('/f/', ['fee', 'fi', 'fo'])); + $this->assertFalse(Assertion::assertAllRegularExpressionMatch('/F/', ['fee', 'fi', 'fo'])); + $this->assertFalse(Assertion::assertAllRegularExpressionMatch('/e/', ['fee', 'fi', 'fo'])); + $this->assertFalse(Assertion::assertAllRegularExpressionMatch('/1/', [12])); + } + + /** + * Tests asserting all members are objects. + * + * @covers ::assertAllObjects + */ + public function testAssertAllObjects() { + $this->assertTrue(Assertion::assertAllObjects([new ArrayObject(), new ArrayObject()])); + $this->assertFalse(Assertion::assertAllObjects([new ArrayObject(), new ArrayObject(), 'foo'])); + $this->assertTrue(Assertion::assertAllObjects([new ArrayObject(), new ArrayObject()], '\\Traversable')); + $this->assertFalse(Assertion::assertAllObjects([new ArrayObject(), new ArrayObject(), 'foo'], '\\Traversable')); + $this->assertFalse(Assertion::assertAllObjects([new ArrayObject(), new StringObject()], '\\Traversable')); + $this->assertTrue(Assertion::assertAllObjects([new ArrayObject(), new StringObject()], '\\Traversable', '\\Drupal\\Tests\\Component\\Assertion\\StringObject')); + $this->assertFalse(Assertion::assertAllObjects([new ArrayObject(), new StringObject(), new stdClass()], '\\ArrayObject', '\\Drupal\\Tests\\Component\\Assertion\\StringObject')); + } + + /** + * Test method referenced by ::testAllCallable() + */ + public function callMe() { + return TRUE; + } + + /** + * Test method referenced by ::testAllCallable() + */ + public static function callMeStatic() { + return TRUE; + } + +} + +/** + * Quick class for testing for objects with __toString. + */ +class StringObject { + /** + * {@inheritdoc} + */ + public function __toString() { + return 'foo'; + } + +} diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php index fd980a4..11d9c00 100644 --- a/core/tests/bootstrap.php +++ b/core/tests/bootstrap.php @@ -89,3 +89,7 @@ function drupal_phpunit_register_extension_dirs(Composer\Autoload\ClassLoader $l // Set the default timezone. While this doesn't cause any tests to fail, PHP // complains if 'date.timezone' is not set in php.ini. date_default_timezone_set('UTC'); + +// Set up runtime assertion handling and any other common test environment +// concerns. +require_once __DIR__ . '/../includes/testing.inc'; diff --git a/sites/example.settings.local.php b/sites/example.settings.local.php index 56fed6f..f603a47 100644 --- a/sites/example.settings.local.php +++ b/sites/example.settings.local.php @@ -12,6 +12,11 @@ */ /** + * Set up runtime testing environment. + */ +require_once DRUPAL_ROOT . '/core/includes/testing.inc'; + +/** * Enable local development services. */ $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';