diff --git a/.htaccess b/.htaccess index af418c4..d308dd8 100644 --- a/.htaccess +++ b/.htaccess @@ -28,13 +28,28 @@ 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 -# sites/default/default.settings.php and -# Drupal\Core\DrupalKernel::bootEnvironment() for settings that can be -# changed at runtime. - # PHP 5, Apache 1 and 2. + # PHP enables assert statements by default. Drupal uses them to perform + # validations that are only necessary during module and theme development, + # and that slow the system down. Hence the Drupal default is off. + php_value assert.active 0 + + # Assert statements are used to validate the cache. To prevent invalid cache + # entries from being written during development you need to turn assert bail + # on. You need to do this even if you disabled caching as Drupal writes caches + # even when it isn't reading from them. + php_value assert.bail 0 + + # The above two settings can be changed at runtime using assert_options() + # Set them here to enable assert statements before + # Drupal\Core\DrupalKernel::bootEnvironment() runs. + # See sites/example.settings.local.php + # + # Override PHP settings that 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_flag session.auto_start off php_value mbstring.http_input pass php_value mbstring.http_output pass diff --git a/core/lib/Drupal/Component/Assertion/Assertion.php b/core/lib/Drupal/Component/Assertion/Assertion.php new file mode 100644 index 0000000..91b4912 --- /dev/null +++ b/core/lib/Drupal/Component/Assertion/Assertion.php @@ -0,0 +1,122 @@ +startAssertionHandling(); + * parent::setUp(); + * } + * + * public function tearDown() { + * $this->stopAssertionHandling(); + * parent::setUp(); + * } + * @endcode + */ +trait BaseTestingTrait { + + /** + * Flag to throw an exception immediately on failure. + * + * @var bool + */ + protected $dieOnRaise = FALSE; + + /** + * Collections of captured assertions. + * + * @var array + */ + protected $assertionsRaised = []; + + + /** + * Callback handler for assert raises during testing. + */ + public function assertCallbackHandle($file, $line, $code, $message = '') { + + // We print out this warning during test development, and since the + // automated tests run in strict mode it will cause a test failure. + if (!$code) { + print ('Assertions should always be strings! Even though PHP permits + other argument types, those arguments will be evaluated which causes + a loss of performance.'); + } + + // Usually we want to let the code continue evaluating as it is going to + // do when assertions are turned off just to make sure the code doesn't + // enter a fatal condition. However, some assertions are guarding against + // Fatal conditions anyway and there will be no way to recover from these + // failures. When testing these assertions, set the dieOnRaise flag which + // causes the exception throw here. + if ($this->dieOnRaise) { + throw new AssertionException($code . ' ' . $message); + } + + // Otherwise we log the assertion as thrown and let the code continue. + $this->assertionsRaised[] = $code; + + // Inform PHP we've successfully completed our handling of the assert fail. + return TRUE; + } + + /** + * Start assertion handling for the test. + * + * Call this from setUp() + */ + protected function startAssertionHandling() { + assert_options(ASSERT_WARNING, FALSE); + assert_options(ASSERT_BAIL, FALSE); + assert_options(ASSERT_CALLBACK, [$this, 'assertCallbackHandle']); + $this->assertionsRaised = []; + return FALSE; + } + + /** + * Suspend assertion handling. + */ + protected function suspendAssertionHandling() { + assert_options(ASSERT_WARNING, TRUE); + assert_options(ASSERT_CALLBACK, NULL); + $this->assertionsRaised = []; + } + + /** + * Cease handling assertions and clear the way for the next test. + * + * Call this from tearDown() + */ + protected function stopAssertionHandling() { + $this->assertAssertionNotRaised(); + $this->suspendAssertionHandling(); + $this->dieOnRaise = FALSE; + } + + /** + * Check if the assertions specified where raised. + * + * This function can be overloaded. Assertions should be passed in the order + * they are expected to occur. After being accounted for the assertion count + * is reset. + */ + abstract protected function assertAssertionsRaised(); + + /** + * Insure no assertions where thrown. + * + * Called during teardown, but you may wish to call it at other times. + */ + abstract protected function assertAssertionNotRaised(); + +} diff --git a/core/lib/Drupal/Component/Assertion/PHPUnitTestingTrait.php b/core/lib/Drupal/Component/Assertion/PHPUnitTestingTrait.php new file mode 100644 index 0000000..4337060 --- /dev/null +++ b/core/lib/Drupal/Component/Assertion/PHPUnitTestingTrait.php @@ -0,0 +1,31 @@ +assertEquals(func_get_args(), $this->assertionsRaised); + $this->assertionsRaised = []; + } + + /** + * {@inheritdoc} + */ + protected function assertAssertionNotRaised() { + $this->assertEmpty($this->assertionsRaised); + } + +} diff --git a/core/modules/simpletest/src/AssertionTestingTrait.php b/core/modules/simpletest/src/AssertionTestingTrait.php new file mode 100644 index 0000000..d29f9a6 --- /dev/null +++ b/core/modules/simpletest/src/AssertionTestingTrait.php @@ -0,0 +1,33 @@ +assertIdentical(func_get_args(), $this->assertionsRaised, 'Expected Assertions Raised.'); + $this->assertionsRaised = []; + } + + /** + * {@inheritdoc} + */ + protected function assertAssertionNotRaised() { + $this->assertTrue(count($this->assertionsRaised) === 0, 'No Assertions Raised'); + } + +} 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..16a77f6 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Assertion/AssertionTest.php @@ -0,0 +1,194 @@ +assertTrue( + Assertion::allMembersAre('ArrayObject', [ + new ArrayObject(), + new ArrayObject() + ]) + ); + // Traversable of objects. + $this->assertTrue( + Assertion::allMembersAre('ArrayObject', new ArrayObject([ + new ArrayObject(), + new ArrayObject() + ])) + ); + // Empty objects pass regardless - this assertion isn't for checking if + // objects are present. + $this->assertTrue( + Assertion::allMembersAre('foo', []) + ); + + // Non traversables fail. + $this->assertFalse( + Assertion::allMembersAre('foo', 'bar') + ); + + // All members are arrays. + $this->assertTrue( + Assertion::allMembersAre('array', [[], []]) + ); + + $this->assertFalse( + Assertion::allMembersAre('array', ['bar', []]) + ); + + // All members are integers. + foreach (['int', 'integer', 'long'] as $type_alias) { + $this->assertTrue( + Assertion::allMembersAre($type_alias, [1, 2, 3]) + ); + + $this->assertFalse( + Assertion::allMembersAre($type_alias, [1, 2, 3, []]) + ); + } + + // All members are floats. + foreach (['float', 'double', 'real'] as $type_alias) { + $this->assertTrue( + Assertion::allMembersAre($type_alias, [1.1, 2.2, 3.14]) + ); + + $this->assertFalse( + Assertion::allMembersAre($type_alias, [1.2, 2, 3, []]) + ); + } + + // All members can be called. + $this->assertTrue( + Assertion::allMembersAre('callable', [ + 'strchr', + [$this, 'callMe'], + [__CLASS__, 'callMeStatic'], + function() { + return TRUE; + } + ]) + ); + + $this->assertFalse( + Assertion::allMembersAre('callable', [ + 'strchr', + [$this, 'callMe'], + [__CLASS__, 'callMeStatic'], + function() { + return TRUE; + }, + 'bye' + ]) + ); + + // All members are numeric. This includes numeric strings. + $this->assertTrue( + Assertion::allMembersAre('numeric', [1, 1.2, '3']) + ); + + $this->assertFalse( + Assertion::allMembersAre('numeric', [1, 1.2, '3', 'pi']) + ); + + // All members have something. + $this->assertTrue( + Assertion::allMembersAre('not-empty', [1, 'foo']) + ); + + $this->assertFalse( + Assertion::allMembersAre('not-empty', ['']) + ); + + // All members are objects. + $this->assertTrue( + Assertion::allMembersAre('object', [ + new ArrayObject(), + new \stdClass() + ]) + ); + + $this->assertFalse( + Assertion::allMembersAre('ArrayObject', [ + new ArrayObject(), + [] + ]) + ); + + // Create a string mock. + $string_object = new StringObject(); + + // Now test strictly against the string type. + $this->assertTrue( + Assertion::allMembersAre('string', ['bar', 'foo']) + ); + + $this->assertFalse( + Assertion::allMembersAre('string', [$string_object]) + ); + + // But a stringable object should pass stringable. + $this->assertTrue( + Assertion::allMembersAre('stringable', ['bar', $string_object]) + ); + + $this->assertFalse( + Assertion::allMembersAre('stringable', [42]) + ); + + } + + /** + * Call me sometime. + * + * This method exists as part of the is_callable test. + */ + public function callMe() { + return TRUE; + } + + /** + * Call me static. + * + * Again, just a testing method. + */ + 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/Drupal/Tests/Component/Assertion/AssertionTestTraitTest.php b/core/tests/Drupal/Tests/Component/Assertion/AssertionTestTraitTest.php new file mode 100644 index 0000000..27c87a1 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Assertion/AssertionTestTraitTest.php @@ -0,0 +1,84 @@ +startAssertionHandling(); + parent::setUp(); + } + + /** + * {@inheritdoc} + */ + public function tearDown() { + $this->stopAssertionHandling(); + $this->assertAssertionNotRaised(); + parent::setUp(); + } + + /** + * Test the PHPUnit Assert assist trait. + * + * This test is a bit unusual since we're self testing. The traits being + * tested are part of this object. + */ + public function testBasics() { + // Normal assert. + assert('1 > 2'); + $this->assertAssertionsRaised('1 > 2'); + + // Assert without string pitches error. + ob_start(); + assert(1 > 2); + $output = ob_get_clean(); + + $this->assertEquals($output, + 'Assertions should always be strings! Even though PHP permits + other argument types, those arguments will be evaluated which causes + a loss of performance.'); + + $this->assertAssertionsRaised(''); + + // Assertion log should be empty after the above call. + $this->assertAssertionNotRaised(); + + // Test accumulation of assert failures. + assert('1 > 2'); + assert('4 < 2'); + assert('false'); + + $this->assertAssertionsRaised('1 > 2', '4 < 2', 'false'); + } + + /** + * Tests if an exception is thrown when dieOnRaise is set. + * + * @expectedException \Drupal\Component\Assertion\AssertionException + */ + public function testAssertionExceptionThrow() { + $this->dieOnRaise = TRUE; + assert('4 < 2'); + } + +} diff --git a/sites/example.settings.local.php b/sites/example.settings.local.php index 4cc2109..798ff38 100644 --- a/sites/example.settings.local.php +++ b/sites/example.settings.local.php @@ -12,6 +12,20 @@ */ /** + * Assertions + * + * Drupal uses assert statements to perform validations that are only + * necessary during module and theme development, and that slow the system + * down. By default the statements are turned off in the .htaccess file. + * Here we turn them back on which should be sufficient for most projects, + * but assert statements located before the DrupalKernel parses this file + * will not be ran unless you modify the .htaccess file. We also set the + * system to halt on assert fail, just in case. + */ +assert_options(ASSERT_ACTIVE, 1); +assert_options(ASSERT_BAIL, 1); + +/** * Enable local development services. */ $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';