diff --git a/.htaccess b/.htaccess index fd7bd29..856fd32 100644 --- a/.htaccess +++ b/.htaccess @@ -39,6 +39,14 @@ AddEncoding gzip svgz php_value mbstring.http_input pass php_value mbstring.http_output pass php_flag mbstring.encoding_translation off + + # Assertions. + # By default PHP has these turned on. Production sites should turn these off. + # While assertions can be turned off at run time, we need to have this setting + # in place immediately to catch an assertions thrown before the settings file + # can be loaded. + php_value assert.active 1 + # To enable them, change 0 to 1. Recommended for dev sites. # Requires mod_expires to be enabled. diff --git a/core/lib/Drupal/Component/Fault/Assertion.php b/core/lib/Drupal/Component/Fault/Assertion.php new file mode 100644 index 0000000..4939b96 --- /dev/null +++ b/core/lib/Drupal/Component/Fault/Assertion.php @@ -0,0 +1,121 @@ +errorLocation['line'] + . ' in file ' + . $this->errorLocation['file'] + . ' -- asserted: ' + . $this->code + . ' -- comment: ' . $this->message; + } + + /** + * Implements BaseFaultHandler::verboseResponse. + */ + protected function verboseResponse() { + $this->printHtmlStart(); ?> +

Assertion Failure

+
+ code) : +?> +

Assertion: code; ?>

+ +

WARNING: The Assert statement was passed a non-string + value. Whatever expression was sent to it will be evaluated regardless of + whether assert functions are turned on or off. In order to preserve system + efficiency it is imperative that you encapsulate the expression in a string + to be evaluated by assert rather than passing an expression to it.

+ +

Comment: message; ?>

+ reference['error']) : +?> +

Reference: reference['error']; ?>

+
+ +
+

Failure Location

+

File: errorLocation['file']; ?>

+

Line: errorLocation['line']; ?>

+ errorLocation['class']) : +?> +

Class: + + errorLocation['class']; ?> + +

+

Method: + + errorLocation['method']; ?> + +

+ errorLocation['method']) : +?> +

Function: + + errorLocation['method'] ?> + +

+ +

Global Scope

+ +
+
+

Stack Trace

+ + trace); ?> + trace as &$level) : + unset($level['args']); + endforeach; + var_dump($this->trace); +endif; ?> +
+printHtmlEnd(); + } + +} diff --git a/core/lib/Drupal/Component/Fault/BaseFaultHandler.php b/core/lib/Drupal/Component/Fault/BaseFaultHandler.php new file mode 100644 index 0000000..c8052b3 --- /dev/null +++ b/core/lib/Drupal/Component/Fault/BaseFaultHandler.php @@ -0,0 +1,374 @@ + '', + 'line' => '', + 'class' => '', + 'method' => '' + ]; + + /** + * Reference information for the fault. + * + * @var array + */ + protected $reference = [ + 'error' => '', + 'class' => '', + 'method' => '' + ]; + + /** + * Fault code. + * + * @var string + */ + protected $code; + + /** + * Fault message. + * + * @var string + */ + protected $message; + + /** + * Fault backtrace. + * + * @var array + */ + protected $trace; + + /** + * The verbose HTML response method. + */ + abstract protected function verboseResponse(); + + /** + * The log message string for the Fault. + */ + abstract protected function composeLogEntry(); + + /** + * Return a Fault Response object. + * + * The FaultSetup class does the favor of normalizing the argument order from + * the three possible. + */ + public function __construct($file, $line, $code, $message, $trace, $option = NULL) { + // Raising an assertion while evaluating an assertion will + // cause a segmentation fault in the PHP engine. This assertion however + // cannot be fulfilled by a call under that circumstance UNLESS invoked + // by a handle function other than the one in FaultSetup. + assert('\\Drupal\\Component\\Fault\\Assertion::validCaller()', 'This class can only be used by other classes in the Fault Component'); + + // Find Drupal root from here - we don't know if DRUPAL_ROOT is defined. + $this->root = dirname(dirname(dirname(dirname(dirname(__DIR__))))); + + // Remove the root from the error file string to reduce verbosity. There is + // no security advantage in doing this. + $this->errorLocation['file'] = substr($file, strlen($this->root)); + $this->errorLocation['line'] = $line; + + // For assertions 'code' means the PHP string of code that evaluated to + // 'false' and triggered the assert failure. For errors and exceptions this + // is an error code with an associated PHP constant such as E_ERROR. + $this->code = $code; + $this->parseTrace($trace, $file, $line); + $this->parseMessage($message); + + } + + /** + * Parses out any link at the start of the message. + * + * Break down the error message string, extracting any reference link that it + * might contain at its start. + */ + protected function parseMessage($message) { + if (strpos($message, 'http') === 0) { + $this->reference['error'] = substr($message, 0, strpos($message, ' ')); + $this->message = substr($message, strpos($message, ' ') + 1); + } + elseif (strpos($message, 'api://') === 0) { + $this->reference['error'] = str_replace('api://', static::API, substr($message, 0, strpos($message, ' '))); + $this->message = substr($message, strpos($message, ' ') + 1); + } + elseif (strpos($message, 'node://') === 0) { + $this->reference['error'] = str_replace('node://', 'http://www.drupal.org/node/', substr($message, 0, strpos($message, ' '))); + $this->message = substr($message, strpos($message, ' ') + 1); + } + else { + $this->message = $message; + } + } + + /** + * Parse out the trace to find the class and method location of the fault. + */ + protected function parseTrace(array $trace, $file, $line) { + + while ($frame = array_shift($trace)) { + if (isset($frame['file']) && isset($frame['line']) && $frame['file'] === $file && $frame['line'] === $line) { + break; + } + } + + if (count($trace) > 0) { + if (isset($trace[0]['class'])) { + $this->errorLocation['class'] = $trace[0]['class']; + } + $this->errorLocation['method'] = $trace[0]['function']; + } + + if ($this->errorLocation['class']) { + $this->reference['class'] = $this->getApiPageForClass($this->errorLocation['file'], $this->errorLocation['class']); + $this->reference['method'] = $this->getApiPageForMethod($this->errorLocation['file'], $this->errorLocation['class'], $this->errorLocation['method']); + } + else { + $this->reference['method'] = $this->getApiPageForFunction($file, $function); + } + + $this->trace = $trace; + } + + /** + * Return the Drupal API path for a class. + */ + protected function getApiPageForClass($file, $class) { + return static::API + . str_replace("/", '!', $file) + . '/class/' . $class . '/8'; + } + + /** + * Return the Drupal API path for a method. + */ + protected function getApiPageForMethod($file, $class, $method) { + return static::API + . str_replace("/", '!', $file) + . '/function/' . $class + . '%3A%3A' . $method . '/8'; + } + + /** + * Return the Drupal API path for a function. + */ + protected function getApiPageForFunction($file, $function) { + return static::API + . str_replace("/", '!', $file) + . '/function/' . $function . '/8'; + } + + /** + * Resolve the fault. + */ + public function resolve() { + $this->clearBuffers(); + $this->log(); + + if ($this->isXmlHttpRequest()) { + $this->jsonRespond(); + } + elseif (PHP_SAPI === 'cli' && !isset($_SERVER['DRUPAL_FAULT_COMPONENT_IN_TEST_MODE'])) { + $this->terminalRespond(); + } + else { + $this->htmlRespond(); + } + } + + /** + * Send Fault response to a Javascript. + */ + protected function jsonRespond() { + $this->sendHeaders('application/json; charset=utf-8'); + echo $this->composeLogEntry(); + } + + /** + * Send the terminal response directly to stdout. + */ + protected function terminalRespond() { + $stdout = fopen('php://stdout', 'w'); + fwrite($stdout, $this->composeLogEntry()); + fclose($stout); + } + + /** + * Log the error both to the PHP engine log and to the system log. + */ + protected function log() { + if (!isset($_SERVER['DRUPAL_FAULT_COMPONENT_IN_TEST_MODE'])) { + $entry = addslashes($this->composeLogEntry()); + openlog('Drupal 8', LOG_PERROR, LOG_USER); + syslog(LOG_ERR, $entry); + closelog(); + error_log($entry); + } + } + + /** + * Determine if this script was requested by Javascript. + */ + protected function isXmlHttpRequest() { + return (isset($_SERVER) && isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'); + } + + /** + * Clear and disable the output buffers if not testing. + */ + protected function clearBuffers() { + if (!isset($_SERVER['DRUPAL_FAULT_COMPONENT_IN_TEST_MODE'])) { + while (@ob_get_level()) { + @ob_end_clean(); + } + } + } + + /** + * Respond to a fault encountered while processing an HTTP request for HTML. + */ + protected function htmlRespond() { + + $this->sendHeaders('text/html; charset=utf-8'); + + // PHP's error display level determines how much information we give. + if (ini_get('display_errors')) { + $this->verboseResponse(); + } + else { + $this->quietResponse(); + } + } + + /** + * Send a quiet response. + */ + protected function quietResponse() { + + $this->printHtmlStart(); + print <<System Error +

The system has encountered an error.

+HTML; + $this->printHtmlEnd(); + } + + /** + * Print the start of an HTML response. + */ + protected function printHtmlStart() { + echo << + + + System Error + + + +
+
+HTML; + + } + + /** + * Send the conclusion of the output. + */ + protected function printHtmlEnd() { + echo '
'; + } + + /** + * Send the correct response headers for a fault response. + */ + protected function sendHeaders($type) { + if (headers_sent()) { + return; + } + + header('Content-Type: ' . $type); + + // Somewhat redundant - browsers shouldn't cache 500 class responses anyway. + header("Cache-Control: no-cache, must-revalidate"); + header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); + header($_SERVER["SERVER_PROTOCOL"] . " 503 Service Unavailable"); + } + +} diff --git a/core/lib/Drupal/Component/Fault/FaultException.php b/core/lib/Drupal/Component/Fault/FaultException.php new file mode 100644 index 0000000..dd8538b --- /dev/null +++ b/core/lib/Drupal/Component/Fault/FaultException.php @@ -0,0 +1,12 @@ +resolve(); + } + +} diff --git a/core/lib/Drupal/Core/Cache/ApcuBackend.php b/core/lib/Drupal/Core/Cache/ApcuBackend.php index 9bac79e..c8b763d 100644 --- a/core/lib/Drupal/Core/Cache/ApcuBackend.php +++ b/core/lib/Drupal/Core/Cache/ApcuBackend.php @@ -166,7 +166,7 @@ protected function prepareItem($cache, $allow_invalid) { * {@inheritdoc} */ public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = array()) { - Cache::validateTags($tags); + assert('count($tags) === 0 || \\Drupal\\Component\\Fault\\Assertion::collectionOfStrings($tags)', 'One or more invalid Cache Tags in passed array'); $tags = array_unique($tags); $cache = new \stdClass(); $cache->cid = $cid; diff --git a/core/lib/Drupal/Core/Cache/Cache.php b/core/lib/Drupal/Core/Cache/Cache.php index d149592..d8898a5 100644 --- a/core/lib/Drupal/Core/Cache/Cache.php +++ b/core/lib/Drupal/Core/Cache/Cache.php @@ -37,7 +37,7 @@ public static function mergeContexts() { $cache_contexts = array_merge($cache_contexts, $contexts); } $cache_contexts = array_unique($cache_contexts); - \Drupal::service('cache_contexts_manager')->validateTokens($cache_contexts); + assert('\\Drupal::service(\'cache_contexts_manager\')->assertValidTokens($cache_contexts)', 'One or more invalid tokens passed.'); sort($cache_contexts); return $cache_contexts; } @@ -66,7 +66,7 @@ public static function mergeTags() { $cache_tags = array_merge($cache_tags, $tags); } $cache_tags = array_unique($cache_tags); - static::validateTags($cache_tags); + assert('count($cache_tags) === 0 || \\Drupal\\Component\\Fault\\Assertion::collectionOfStrings($cache_tags)', 'One or more invalid Cache Tags in passed array'); sort($cache_tags); return $cache_tags; } @@ -110,6 +110,8 @@ public static function mergeMaxAges() { * An array of cache tags. * * @throws \LogicException + * + * @deprecated use assert('count($tags) === 0 || \\Drupal\\Component\\Fault\\Assertions::collectionOfStrings($tags)'); */ public static function validateTags(array $tags) { if (empty($tags)) { diff --git a/core/lib/Drupal/Core/Cache/CacheCollector.php b/core/lib/Drupal/Core/Cache/CacheCollector.php index a6d8ab5..c2add52 100644 --- a/core/lib/Drupal/Core/Cache/CacheCollector.php +++ b/core/lib/Drupal/Core/Cache/CacheCollector.php @@ -115,7 +115,7 @@ * (optional) The tags to specify for the cache item. */ public function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, array $tags = array()) { - Cache::validateTags($tags); + assert('count($tags) === 0 || \\Drupal\\Component\\Fault\\Assertion::collectionOfStrings($tags)', 'One or more invalid Cache Tags in passed array'); $this->cid = $cid; $this->cache = $cache; $this->tags = $tags; diff --git a/core/lib/Drupal/Core/Cache/CacheContextsManager.php b/core/lib/Drupal/Core/Cache/CacheContextsManager.php index 67857cb..6e74617 100644 --- a/core/lib/Drupal/Core/Cache/CacheContextsManager.php +++ b/core/lib/Drupal/Core/Cache/CacheContextsManager.php @@ -271,4 +271,16 @@ public function validateTokens(array $context_tokens = []) { } } + /** + * Wrapper for the above for use with the assert statement. + */ + public function assertValidTokens(array $context_tokens = []) { + try { + $this->validateTokens($context_tokens); + } catch ( \LogicException $e) { + return FALSE; + } + return TRUE; + } + } diff --git a/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php b/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php index 64a8eb0..f76c4fa 100644 --- a/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php +++ b/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php @@ -28,7 +28,7 @@ class CacheTagsInvalidator implements CacheTagsInvalidatorInterface { */ public function invalidateTags(array $tags) { // Validate the tags. - Cache::validateTags($tags); + assert('count($tags) === 0 || \\Drupal\\Component\\Fault\\Assertion::collectionOfStrings($tags)', 'One or more invalid Cache Tags in passed array'); // Notify all added cache tags invalidators. foreach ($this->invalidators as $invalidator) { diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php index 81e83bb..ce12687 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php @@ -150,7 +150,7 @@ protected function prepareItem($cache, $allow_invalid) { * Implements Drupal\Core\Cache\CacheBackendInterface::set(). */ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array()) { - Cache::validateTags($tags); + assert('count($tags) === 0 || \\Drupal\\Component\\Fault\\Assertion::collectionOfStrings($tags)', 'One or more invalid Cache Tags in passed array'); $tags = array_unique($tags); // Sort the cache tags so that they are stored consistently in the database. sort($tags); @@ -210,7 +210,7 @@ public function setMultiple(array $items) { 'tags' => array(), ); - Cache::validateTags($item['tags']); + assert('count($item[\'tags\']) === 0 || \\Drupal\\Component\\Fault\\Assertion::collectionOfStrings($item[\'tags\'])', 'One or more invalid Cache Tags in passed array'); $item['tags'] = array_unique($item['tags']); // Sort the cache tags so that they are stored consistently in the DB. sort($item['tags']); diff --git a/core/lib/Drupal/Core/Cache/MemoryBackend.php b/core/lib/Drupal/Core/Cache/MemoryBackend.php index 63b56c2..801e695 100644 --- a/core/lib/Drupal/Core/Cache/MemoryBackend.php +++ b/core/lib/Drupal/Core/Cache/MemoryBackend.php @@ -107,7 +107,7 @@ protected function prepareItem($cache, $allow_invalid) { * Implements Drupal\Core\Cache\CacheBackendInterface::set(). */ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array()) { - Cache::validateTags($tags); + assert('count($tags) === 0 || \\Drupal\\Component\\Fault\\Assertion::collectionOfStrings($tags)', 'One or more invalid Cache Tags in passed array'); $tags = array_unique($tags); // Sort the cache tags so that they are stored consistently in the database. sort($tags); diff --git a/core/lib/Drupal/Core/Cache/PhpBackend.php b/core/lib/Drupal/Core/Cache/PhpBackend.php index 761e394..03a2290 100644 --- a/core/lib/Drupal/Core/Cache/PhpBackend.php +++ b/core/lib/Drupal/Core/Cache/PhpBackend.php @@ -148,7 +148,7 @@ protected function prepareItem($cache, $allow_invalid) { * {@inheritdoc} */ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array()) { - Cache::validateTags($tags); + assert('count($tags) === 0 || \\Drupal\\Component\\Fault\\Assertion::collectionOfStrings($tags)', 'One or more invalid Cache Tags in passed array'); $item = (object) array( 'cid' => $cid, 'data' => $data, diff --git a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php index 297f971..a932f5c 100644 --- a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php +++ b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php @@ -132,7 +132,7 @@ public function __construct($subdir, \Traversable $namespaces, ModuleHandlerInte * definitions should be cleared along with other, related cache entries. */ public function setCacheBackend(CacheBackendInterface $cache_backend, $cache_key, array $cache_tags = array()) { - Cache::validateTags($cache_tags); + assert('count($cache_tags) === 0 || \\Drupal\\Component\\Fault\\Assertion::collectionOfStrings($cache_tags)', 'One or more invalid Cache Tags in passed array'); $this->cacheBackend = $cache_backend; $this->cacheKey = $cache_key; $this->cacheTags = $cache_tags; diff --git a/core/modules/simpletest/src/AssertionTestingTrait.php b/core/modules/simpletest/src/AssertionTestingTrait.php new file mode 100644 index 0000000..003e17d --- /dev/null +++ b/core/modules/simpletest/src/AssertionTestingTrait.php @@ -0,0 +1,51 @@ +startAssertionHandling(); + * parent::setUp(); + * } + * @endcode + * + * @code + * public function tearDown() { + * $this->stopAssertionHandling(); + * $this->assertAssertionNotRaised(); + * parent::setUp(); + * } + * @endcode + */ +trait AssertionTestingTrait { + use BaseAssertionTestingTrait; + /** + * {@inheritdoc} + */ + protected function assertAssertionsRaised() { + $this->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/modules/system/src/Tests/Cache/GenericCacheBackendUnitTestBase.php b/core/modules/system/src/Tests/Cache/GenericCacheBackendUnitTestBase.php index cc0d9df..006812b 100644 --- a/core/modules/system/src/Tests/Cache/GenericCacheBackendUnitTestBase.php +++ b/core/modules/system/src/Tests/Cache/GenericCacheBackendUnitTestBase.php @@ -10,6 +10,8 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\simpletest\KernelTestBase; +use Drupal\simpletest\AssertionTestingTrait; +use Drupal\Tests\AssertionException; /** * Tests any cache backend. @@ -22,6 +24,7 @@ * For a full working implementation. */ abstract class GenericCacheBackendUnitTestBase extends KernelTestBase { + use AssertionTestingTrait; /** * Array of objects implementing Drupal\Core\Cache\CacheBackendInterface. @@ -107,6 +110,7 @@ protected function getCacheBackend($bin = null) { } protected function setUp() { + $this->startAssertionHandling(); $this->cachebackends = array(); $this->defaultValue = $this->randomMachineName(10); @@ -116,6 +120,9 @@ protected function setUp() { } protected function tearDown() { + $this->stopAssertionHandling(); + $this->assertAssertionNotRaised(); + // Destruct the registered backend, each test will get a fresh instance, // properly emptying it here ensure that on persistent data backends they // will come up empty the next test. @@ -219,12 +226,14 @@ public function testSetGet() { $this->assertFalse($backend->get('test8')); // Calling ::set() with invalid cache tags. + $this->dieOnRaise = TRUE; try { $backend->set('exception_test', 'value', Cache::PERMANENT, ['node' => [3, 5, 7]]); - $this->fail('::set() was called with invalid cache tags, no exception was thrown.'); + $this->fail('::set() was called with invalid cache tags, no assertion raised.'); } - catch (\LogicException $e) { - $this->pass('::set() was called with invalid cache tags, an exception was thrown.'); + catch ( AssertionException $e) { + $this->pass('::set() was called with invalid cache tags, an assertion was raised.'); + $this->dieOnRaise = FALSE; } } @@ -413,6 +422,7 @@ public function testSetMultiple() { $this->assertEqual($cached['cid_5']->data, $items['cid_5']['data'], 'New cache item set correctly.'); // Calling ::setMultiple() with invalid cache tags. + $this->dieOnRaise = TRUE; try { $items = [ 'exception_test_1' => array('data' => 1, 'tags' => []), @@ -420,10 +430,11 @@ public function testSetMultiple() { 'exception_test_3' => array('data' => 3, 'tags' => ['node' => [3, 5, 7]]), ]; $backend->setMultiple($items); - $this->fail('::setMultiple() was called with invalid cache tags, no exception was thrown.'); + $this->fail('::setMultiple() was called with invalid cache tags, no assertion was raised.'); } - catch (\LogicException $e) { - $this->pass('::setMultiple() was called with invalid cache tags, an exception was thrown.'); + catch (AssertionException $e) { + $this->pass('::setMultiple() was called with invalid cache tags, an assertion was raised.'); + $this->dieOnRaise = FALSE; } } diff --git a/core/tests/Drupal/Tests/AssertionException.php b/core/tests/Drupal/Tests/AssertionException.php new file mode 100644 index 0000000..837f11c --- /dev/null +++ b/core/tests/Drupal/Tests/AssertionException.php @@ -0,0 +1,24 @@ +startAssertionHandling(); + * parent::setUp(); + * } + * @endcode + */ +trait AssertionTestingTrait { + use BaseAssertionTestingTrait; + /** + * {@inheritdoc} + * + * Drupal Unit test has no tear down and since this trait will most + * frequently be applied to its children we just go ahead and define this + * method. Reminder - if you need to define this method in your test you'll + * need to alias this function when you bind in the trait. + */ + protected function tearDown() { + $this->stopAssertionHandling(); + $this->assertEmpty( + $this->assertionsRaised, + 'Unaccounted for assert fails found at test conclusion: ' . implode(', ', $this->assertionsRaised) + ); + } + + /** + * {@inheritdoc} + */ + protected function assertAssertionsRaised() { + $this->assertEquals(func_get_args(), $this->assertionsRaised); + $this->assertionsRaised = []; + } + + /** + * {@inheritdoc} + */ + protected function assertAssertionNotRaised() { + $this->assertEmpty($this->assertionsRaised); + } + +} diff --git a/core/tests/Drupal/Tests/BaseAssertionTestingTrait.php b/core/tests/Drupal/Tests/BaseAssertionTestingTrait.php new file mode 100644 index 0000000..10f20d9 --- /dev/null +++ b/core/tests/Drupal/Tests/BaseAssertionTestingTrait.php @@ -0,0 +1,128 @@ +dieOnRaise) { + throw new AssertionException($message); + } + + // Otherwise we log the assertion as thrown and let the code continue. + $this->assertionsRaised[] = $message; + + // Inform PHP we've successfully completed our handling of the assert fail. + return TRUE; + } + + /** + * Start assertion handling for the test. + */ + 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() { + // If an error was expected and indeed thrown there will be no chance to + // clear out the assertion log before we reach this function. In order for + // those tests to work set a flag. + if ($this->thereWillBeErrors) { + $this->assertionsRaised = []; + $this->thereWillBeErrors = FALSE; + } + + $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/tests/Drupal/Tests/Component/Fault/AssertionHandlerTest.php b/core/tests/Drupal/Tests/Component/Fault/AssertionHandlerTest.php new file mode 100644 index 0000000..78fc25f --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Fault/AssertionHandlerTest.php @@ -0,0 +1,65 @@ +startAssertionHandling(); + parent::setUp(); + } + + /** + * Test the assert handler. + * + * The json response mode is used since it's output is easiest to check. + */ + public function testAssertHandling() { + // Flag to the AssertionHandler that it is to spit out HTML anyway, leave + // the buffers alone, and not log anything. + $_SERVER['DRUPAL_FAULT_COMPONENT_IN_TEST_MODE'] = TRUE; + + // JSON Response. + $_SERVER['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'; + + $response = new AssertionHandler(__FILE__, 42, 'FALSE', 'node://66 test2', [ + [ + 'file' => __FILE__, + 'line' => 42, + 'function' => 'assert', + 'class' => 'Moo', + ], + [ + 'file' => 'check', + 'line' => 12, + 'function' => 'clear', + 'class' => 'Woo' + ] + ]); + $this->assertAssertionsRaised('This class can only be used by other classes in the Fault Component'); + + ob_start(); + $response->resolve(); + $this->assertEquals('Assert Failure line 42 in file /core/tests/Drupal/Tests/Component/Fault/AssertionHandlerTest.php -- asserted: FALSE -- comment: test2', + ob_get_clean() + ); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Fault/AssertionTest.php b/core/tests/Drupal/Tests/Component/Fault/AssertionTest.php new file mode 100644 index 0000000..db765ae --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Fault/AssertionTest.php @@ -0,0 +1,244 @@ + 'foo'], + ['function' => 'moo'] + ]); + } + + /** + * Test no Caller exists situation. + * + * @expectedException \Drupal\Component\Fault\FaultException + * + * @expectedExceptionMessage No caller exists. + */ + public function testValidCallerExceptionNotGlobal() { + Assertion::validCaller('', [ + ['function' => 'foo'], + ['function' => 'assert'] + ]); + } + + /** + * Test the analysis of stackframes. + */ + public function testValidCallerAnalysis() { + // Call in same class. + $this->assertTrue( + Assertion::validCaller('', [ + ['function' => 'foo'], + ['function' => 'assert'], + ['function' => 'callee', 'class' => 'A\\B\\C'], + ['function' => 'caller', 'class' => 'A\\B\\C'] + ]) + ); + + // Call in same namespace. + $this->assertTrue( + Assertion::validCaller('', [ + ['function' => 'foo'], + ['function' => 'assert'], + ['function' => 'callee', 'class' => 'A\\B\\C'], + ['function' => 'caller', 'class' => 'A\\B\\D'] + ]) + ); + + // Call in child namespace. + $this->assertTrue( + Assertion::validCaller('', [ + ['function' => 'foo'], + ['function' => 'assert'], + ['function' => 'callee', 'class' => 'A\\B\\C'], + ['function' => 'caller', 'class' => 'A\\B\\D\\E'] + ]) + ); + + // Call in parent namespace. + $this->assertFalse( + Assertion::validCaller('', [ + ['function' => 'foo'], + ['function' => 'assert'], + ['function' => 'callee', 'class' => 'A\\B\\C'], + ['function' => 'caller', 'class' => 'A\\D'] + ]) + ); + + // Call from global namespace. + $this->assertFalse( + Assertion::validCaller('', [ + ['function' => 'foo'], + ['function' => 'assert'], + ['function' => 'callee', 'class' => 'A\\B\\C'], + ['function' => 'caller', 'class' => 'D'] + ]) + ); + + // A child class will have to be in the same namepace to function. + $this->assertTrue( + Assertion::validCaller('', [ + ['function' => 'foo'], + ['function' => 'assert'], + ['function' => 'callee', 'class' => 'A\\B\\C'], + ['function' => 'callee', 'class' => 'A\\B\\D'], + ['function' => 'callee', 'class' => 'A\\B\\E'], + ['function' => 'caller', 'class' => 'A\\B\\F'] + ]) + ); + + // Or in a child namespace. The scope governance is that of the class + // making the assertion. + $this->assertTrue( + Assertion::validCaller('', [ + ['function' => 'foo'], + ['function' => 'assert'], + ['function' => 'callee', 'class' => 'A\\B\\C'], + ['function' => 'callee', 'class' => 'A\\B\\D\\E'], + ['function' => 'callee', 'class' => 'A\\B\\F\\G\\E'], + ['function' => 'caller', 'class' => 'A\\B\\F\\G'] + ]) + ); + + // An extender class from a foreign namespace allows calls from the original + // namespace. + $this->assertTrue( + Assertion::validCaller('', [ + ['function' => 'foo'], + ['function' => 'assert'], + ['function' => 'callee', 'class' => 'A\\B\\C'], + ['function' => 'callee', 'class' => 'A\\D\\E'], + ['function' => 'caller', 'class' => 'A\\B\\F\\G'] + ]) + ); + + // But will not allow calls from a new namespace. + $this->assertFalse( + Assertion::validCaller('', [ + ['function' => 'foo'], + ['function' => 'assert'], + ['function' => 'callee', 'class' => 'A\\B\\C'], + ['function' => 'callee', 'class' => 'A\\D\\E'], + ['function' => 'caller', 'class' => 'A\\F\\G'] + ]) + ); + + // Test scope argument. + $this->assertTrue( + Assertion::validCaller('A\\F', [ + ['function' => 'foo'], + ['function' => 'assert'], + ['function' => 'callee', 'class' => 'A\\B\\C'], + ['function' => 'callee', 'class' => 'A\\D\\E'], + ['function' => 'caller', 'class' => 'A\\F\\G'] + ]) + ); + } + + /** + * Test the collectionOf method. + */ + public function testCollectionOf() { + // We don't need a test mock - the internal ArrayObject will work just fine. + $this->assertTrue( + Assertion::collectionOf([ + new ArrayObject(), + new ArrayObject() + ], 'ArrayObject') + ); + + $this->assertFalse( + Assertion::collectionOf([ + new ArrayObject(), + [] + ], 'ArrayObject') + ); + + $this->assertTrue( + Assertion::collectionOf(new ArrayObject([ + new ArrayObject(), + new ArrayObject() + ]), 'ArrayObject') + ); + + $this->assertFalse( + Assertion::collectionOf(new ArrayObject([ + new ArrayObject(), + [] + ]), 'ArrayObject') + ); + + // Non traversables fail. + $this->assertFalse( + Assertion::collectionOf('string', 'ArrayObject') + ); + + $this->assertFalse( + Assertion::collectionOf(new \stdClass(), 'ArrayObject') + ); + + // Empty collections fail. + $this->assertFalse( + Assertion::collectionOf([], 'ArrayObject') + ); + + } + + /** + * Test CollectionOfStrings method. + */ + public function testCollectionOfStrings() { + $this->assertTrue(Assertion::collectionOfStrings([ + 'foo', + 'boo', + new ToStringMock('doo') + ])); + + $this->assertTrue(Assertion::collectionOfStrings(new ArrayObject([ + 'foo', + 'boo', + new ToStringMock('doo') + ]))); + + $this->assertFalse(Assertion::collectionOfStrings([ + 'foo', + 1, + new ToStringMock('doo') + ])); + + $this->assertFalse(Assertion::collectionOfStrings(new ArrayObject([ + 'foo', + 1, + new ToStringMock('doo') + ]))); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Fault/FaultSetupTest.php b/core/tests/Drupal/Tests/Component/Fault/FaultSetupTest.php new file mode 100644 index 0000000..be39e46 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Fault/FaultSetupTest.php @@ -0,0 +1,44 @@ +assertEquals(['Drupal\\Component\\Fault\\FaultSetup', 'handleAssert'], assert_options(ASSERT_CALLBACK)); + $this->assertEquals(1, assert_options(ASSERT_BAIL)); + + // Reset and disable assertions momentarily. + assert_options(ASSERT_CALLBACK, NULL); + assert_options(ASSERT_ACTIVE, 0); + + // Now test to see if the method does nothing as it should when assert + // active = 0 + FaultSetup::start(); + $this->assertEquals(0, assert_options(ASSERT_ACTIVE)); + $this->assertEquals(NULL, assert_options(ASSERT_CALLBACK)); + + // Restore test environment defaults. + assert_options(ASSERT_ACTIVE, 1); + assert_options(ASSERT_BAIL, 0); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheTagsInvalidatorTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheTagsInvalidatorTest.php index 27c70e5..f650412 100644 --- a/core/tests/Drupal/Tests/Core/Cache/CacheTagsInvalidatorTest.php +++ b/core/tests/Drupal/Tests/Core/Cache/CacheTagsInvalidatorTest.php @@ -19,17 +19,6 @@ class CacheTagsInvalidatorTest extends UnitTestCase { /** * @covers ::invalidateTags - * - * @expectedException \LogicException - * @expectedExceptionMessage Cache tags must be strings, array given. - */ - public function testInvalidateTagsWithInvalidTags() { - $cache_tags_invalidator = new CacheTagsInvalidator(); - $cache_tags_invalidator->invalidateTags(['node' => [2, 3, 5, 8, 13]]); - } - - /** - * @covers ::invalidateTags * @covers ::addInvalidator */ public function testInvalidateTags() { diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheTest.php index c2e61a1..2cdc030 100644 --- a/core/tests/Drupal/Tests/Core/Cache/CacheTest.php +++ b/core/tests/Drupal/Tests/Core/Cache/CacheTest.php @@ -18,48 +18,6 @@ class CacheTest extends UnitTestCase { /** - * Provides a list of cache tags arrays. - * - * @return array - */ - public function validateTagsProvider() { - return [ - [[], FALSE], - [['foo'], FALSE], - [['foo', 'bar'], FALSE], - [['foo', 'bar', 'llama:2001988', 'baz', 'llama:14031991'], FALSE], - // Invalid. - [[FALSE], 'Cache tags must be strings, boolean given.'], - [[TRUE], 'Cache tags must be strings, boolean given.'], - [['foo', FALSE], 'Cache tags must be strings, boolean given.'], - [[NULL], 'Cache tags must be strings, NULL given.'], - [['foo', NULL], 'Cache tags must be strings, NULL given.'], - [[1337], 'Cache tags must be strings, integer given.'], - [['foo', 1337], 'Cache tags must be strings, integer given.'], - [[3.14], 'Cache tags must be strings, double given.'], - [['foo', 3.14], 'Cache tags must be strings, double given.'], - [[[]], 'Cache tags must be strings, array given.'], - [['foo', []], 'Cache tags must be strings, array given.'], - [['foo', ['bar']], 'Cache tags must be strings, array given.'], - [[new \stdClass()], 'Cache tags must be strings, object given.'], - [['foo', new \stdClass()], 'Cache tags must be strings, object given.'], - ]; - } - - /** - * @covers ::validateTags - * - * @dataProvider validateTagsProvider - */ - public function testValidateTags(array $tags, $expected_exception_message) { - if ($expected_exception_message !== FALSE) { - $this->setExpectedException('LogicException', $expected_exception_message); - } - // If it doesn't throw an exception, validateTags() returns NULL. - $this->assertNull(Cache::validateTags($tags)); - } - - /** * Provides a list of pairs of cache tags arrays to be merged. * * @return array diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheableMetadataTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheableMetadataTest.php index ccbb7d6..4d55851 100644 --- a/core/tests/Drupal/Tests/Core/Cache/CacheableMetadataTest.php +++ b/core/tests/Drupal/Tests/Core/Cache/CacheableMetadataTest.php @@ -35,6 +35,7 @@ public function testMerge(CacheableMetadata $a, CacheableMetadata $b, CacheableM $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\CacheContextsManager') ->disableOriginalConstructor() ->getMock(); + $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE); $container = new ContainerBuilder(); $container->set('cache_contexts_manager', $cache_contexts_manager); \Drupal::setContainer($container); diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php index c6a217a..9e22b21 100644 --- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php +++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php @@ -56,6 +56,7 @@ public function testMerge(BubbleableMetadata $a, CacheableMetadata $b, Bubbleabl $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\CacheContextsManager') ->disableOriginalConstructor() ->getMock(); + $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE); $container = new ContainerBuilder(); $container->set('cache_contexts_manager', $cache_contexts_manager); $container->set('renderer', $renderer); diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php index 903adce..aa2b57d 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php @@ -104,6 +104,7 @@ protected function setUp() { $this->cacheContextsManager = $this->getMockBuilder('Drupal\Core\Cache\CacheContextsManager') ->disableOriginalConstructor() ->getMock(); + $this->cacheContextsManager->method('assertValidTokens')->willReturn(TRUE); $this->cacheContextsManager->expects($this->any()) ->method('convertTokensToKeys') ->willReturnCallback(function($context_tokens) { diff --git a/core/tests/Drupal/Tests/ToStringMock.php b/core/tests/Drupal/Tests/ToStringMock.php new file mode 100644 index 0000000..d1089c1 --- /dev/null +++ b/core/tests/Drupal/Tests/ToStringMock.php @@ -0,0 +1,33 @@ +string = strval($string); + } + + /** + * {@inheritdoc} + */ + public function __toString() { + return $this->string; + } + +} diff --git a/index.php b/index.php index 53392f9..3379865 100644 --- a/index.php +++ b/index.php @@ -10,11 +10,13 @@ use Drupal\Core\DrupalKernel; use Drupal\Core\Site\Settings; +use Drupal\Component\Fault\FaultSetup; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; $autoloader = require_once 'autoload.php'; +FaultSetup::start(); try { $request = Request::createFromGlobals();