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..51e0fe9 --- /dev/null +++ b/core/lib/Drupal/Component/Fault/Assertion.php @@ -0,0 +1,132 @@ +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']); + } + var_dump($this->trace); +endif; ?> + + '', + 'line' => '', + 'class' => '', + 'method' => '' + ]; + + /** + * Reference information for the fault. + */ + protected $reference = [ + 'error' => '', + 'class' => '', + 'method' => '' + ]; + + /** + * Fault code. + */ + protected $code; + + /** + * Fault message. + */ + protected $message; + + /** + * Fault backtrace. + */ + 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) { + // Remember that 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 maps straightforwardly. + $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; + + // Parse out the trace to find the class and method location of the + // fault and remove the section of the trace from this namespace. + $this->parseTrace($trace, $file, $line); + + // Now break down the error message string, extracting any reference link + // that it might contain at its start. + $this->parseMessage($message); + + } + + /** + * Parse out any link at the start of the message. + */ + protected function parseMessage($message) { + + // First look for http which indicates the author of the fault throw has + // a page in mind to show explaining what's going on - this will occur for + // third party modules. + if (strpos($message, 'http') === 0) { + $this->reference['error'] = substr($message, 0, strpos($message, ' ')); + $this->message = substr($message, strpos($message, ' ') + 1); + } + // API: which is shorthand for the online api reference. + 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); + } + // node: which is shorthand for a drupal issue node. + 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); + } + // At this point we presume that there is no error link to present, so pass + // along whatever message we got. + else { + $this->message = $message; + } + } + + /** + * Make sure the backtrace starts from the class and method of the fault. + */ + protected function parseTrace(array $trace, $file, $line) { + + // Traverse the stack until we find the error point. + // Works with assertion, need to test for exceptions and errors. + while ($frame = array_shift($trace)) { + if (isset($frame['file']) && isset($frame['line']) && $frame['file'] === $file && $frame['line'] === $line) { + break; + } + } + + // Now set the pointers to the class and method of the fault. + if (count($trace) > 0) { + if (isset($trace[0]['class'])) { + $this->errorLocation['class'] = $trace[0]['class']; + } + $this->errorLocation['method'] = $trace[0]['function']; + } + + // Assemble the path to the API page for the class and method of the fault. + 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']); + } + // Or just the 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(); + } + } + + /** + * A quiet response. + * + * Since, in theory, faults can happen in production, the developer can + * provide an html file to be displayed in place of the system default. It + * must be an HTML file - in order to prevent further errors we load the file + * with file_get_contents and echo out whatever is in it. + */ + protected function quietResponse() { + $this->printHtmlStart(); + print << +

System Error

+

The system has encountered an error and the administrator has configured + the server not to publicly report the nature of the error, though it has + been logged.

+ + +HTML; + } + + /** + * The start of an HTML response. + */ + protected function printHtmlStart() { + echo << + + + System Error + + +HTML; + + } + + /** + * 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"); + + // Status return. + 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..4561a0c 100644 --- a/core/lib/Drupal/Core/Cache/ApcuBackend.php +++ b/core/lib/Drupal/Core/Cache/ApcuBackend.php @@ -166,7 +166,8 @@ 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 5541a68..16bdf95 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')->validateTokens($cache_contexts); + assert('\\Drupal::service(\'cache_contexts\')->validateTokens($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; } @@ -102,27 +102,6 @@ public static function mergeMaxAges() { } /** - * Validates an array of cache tags. - * - * Can be called before using cache tags in operations, to ensure validity. - * - * @param string[] $tags - * An array of cache tags. - * - * @throws \LogicException - */ - public static function validateTags(array $tags) { - if (empty($tags)) { - return; - } - foreach ($tags as $value) { - if (!is_string($value)) { - throw new \LogicException('Cache tags must be strings, ' . gettype($value) . ' given.'); - } - } - } - - /** * Build an array of cache tags from a given prefix and an array of suffixes. * * Each suffix will be converted to a cache tag by appending it to the prefix, diff --git a/core/lib/Drupal/Core/Cache/CacheCollector.php b/core/lib/Drupal/Core/Cache/CacheCollector.php index a6d8ab5..01af8d2 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/CacheContexts.php b/core/lib/Drupal/Core/Cache/CacheContexts.php index 5de6527..1f0ea2e 100644 --- a/core/lib/Drupal/Core/Cache/CacheContexts.php +++ b/core/lib/Drupal/Core/Cache/CacheContexts.php @@ -110,9 +110,7 @@ public function convertTokensToKeys(array $context_tokens) { $keys = []; foreach (static::parseTokens($context_tokens) as $context) { list($context_id, $parameter) = $context; - if (!in_array($context_id, $this->contexts)) { - throw new \InvalidArgumentException(SafeMarkup::format('"@context" is not a valid cache context ID.', ['@context' => $context_id])); - } + assert('in_array($context_id, $this->contexts)', 'Not a valid Context ID'); $keys[] = $this->getService($context_id)->getContext($parameter); } return $keys; @@ -235,7 +233,7 @@ public static function parseTokens(array $context_tokens) { */ public function validateTokens(array $context_tokens = []) { if (empty($context_tokens)) { - return; + return TRUE; } // Initialize the set of valid context tokens with the container's contexts. @@ -245,7 +243,7 @@ public function validateTokens(array $context_tokens = []) { foreach ($context_tokens as $context_token) { if (!is_string($context_token)) { - throw new \LogicException(sprintf('Cache contexts must be strings, %s given.', gettype($context_token))); + return FALSE; } if (isset($this->validContextTokens[$context_token])) { @@ -259,16 +257,20 @@ public function validateTokens(array $context_tokens = []) { // minimize the amount of work in future ::validateContexts() calls. $context_id = $context_token; $colon_pos = strpos($context_id, ':'); + if ($colon_pos !== FALSE) { $context_id = substr($context_id, 0, $colon_pos); } - if (isset($this->validContextTokens[$context_id])) { - $this->validContextTokens[$context_token] = TRUE; - } - else { - throw new \LogicException(sprintf('"%s" is not a valid cache context ID.', $context_id)); + + if (!isset($this->validContextTokens[$context_id])) { + return FALSE; } + + $this->validContextTokens[$context_token] = TRUE; + } + + return TRUE; } } diff --git a/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php b/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php index 64a8eb0..fd34968 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 b1144e4..444c1df 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); diff --git a/core/lib/Drupal/Core/Cache/MemoryBackend.php b/core/lib/Drupal/Core/Cache/MemoryBackend.php index 63b56c2..f71bc0e 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..4c790b8 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..27d59b6 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/tests/Drupal/Tests/AssertionException.php b/core/tests/Drupal/Tests/AssertionException.php new file mode 100644 index 0000000..ecc5ed1 --- /dev/null +++ b/core/tests/Drupal/Tests/AssertionException.php @@ -0,0 +1,12 @@ +startAssertionHandling(); + * parent::setUp(); + * } + * @endcode + */ +trait AssertionTestingTrait { + + /** + * Flag assertion handler to throw an exception on raise, ending the test. + */ + protected $dieOnRaise = FALSE; + + /** + * Collections of captured assertions. + */ + protected $assertionsRaised = []; + + /** + * Errors Expected. + * + * This flag prevents the tearDown from checking for unaccounted assertions. + * after an error throw. + */ + protected $thereWillBeErrors = FALSE; + + /** + * 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($message); + } + + // Otherwise we log the assertion as thrown and let the code continue. + // However, be aware we assert that this array is empty during tear down. + // If it isn't the test will fail. + $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_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 = []; + } + + /** + * {@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() { + + // If an error was expected and indeed thrown there will be no chance to + // clear out the assertion log before we reach this function, so set the + // flag to skip the assertions check. + // + // Only use this when testing errors that you are raising yourself with + // trigger error (and that itself should be rare). In other cases go ahead + // and catch the assertion by setting the dieOnRaise flag. + if ($this->thereWillBeErrors) { + $this->assertionsRaised = []; + $this->thereWillBeErrors = FALSE; + } + + $this->assertEmpty( + $this->assertionsRaised, + 'Unaccounted for assert fails found at test conclusion: ' . implode(', ', $this->assertionsRaised) + ); + $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. + */ + protected function assertAssertionsRaised() { + + $this->assertEquals(func_get_args(), $this->assertionsRaised); + $this->assertionsRaised = []; + } + + /** + * Insure no assertions where thrown. + * + * Called during teardown, but you may wish to call it at other times. + */ + protected function assertAssertionNotRaised() { + + $this->assertEmpty($this->assertionsRaised); + } + +} 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..5515ca7 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Fault/AssertionHandlerTest.php @@ -0,0 +1,66 @@ +startAssertionHandling(); + parent::setUp(); + } + + /** + * Test the constructor to make sure it asserts it shouldn't be called. + */ + public function testConstructor() { + // 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; + + ini_set('display_errors', 1); + + // 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..10fc90b --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Fault/AssertionTest.php @@ -0,0 +1,215 @@ + 'foo'], + ['function' => 'moo'] + ]); + } + + /** + * Test the throw of an error when the assert is made outside of any function. + * + * @expectedException \Drupal\Component\Fault\FaultException + * + * @expectedExceptionMessage Why are you asserting this in the global namespace??? + */ + 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') + ); + + } + +} 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..30aea68 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Fault/FaultSetupTest.php @@ -0,0 +1,54 @@ +startAssertionHandling(); + parent::setUp(); + } + + /** + * Test the static start method. + */ + public function testStart() { + // Assertions already on, so the method should detect this and set its + // handler up. It also activates assert_bail. + FaultSetup::start(); + $this->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/CacheContextsTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php index 76251a1..542c182 100644 --- a/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php +++ b/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php @@ -13,6 +13,8 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Tests\UnitTestCase; use Symfony\Component\DependencyInjection\Container; +use Drupal\Tests\AssertionTestingTrait; +use Drupal\Tests\AssertionException; /** * @coversDefaultClass \Drupal\Core\Cache\CacheContexts @@ -20,6 +22,16 @@ */ class CacheContextsTest extends UnitTestCase { + use AssertionTestingTrait; + + /** + * {@inheritdoc} + */ + public function setUp() { + $this->startAssertionHandling(); + parent::setUp(); + } + /** * @covers ::optimizeTokens * @@ -101,13 +113,12 @@ public function testConvertTokensToKeys() { /** * @covers ::convertTokensToKeys * - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage "non-cache-context" is not a valid cache context ID. + * @expectedException \Drupal\Tests\AssertionException */ public function testInvalidContext() { + $this->dieOnRaise = TRUE; $container = $this->getMockContainer(); $cache_contexts = new CacheContexts($container, $this->getContextsFixture()); - $cache_contexts->convertTokensToKeys(["non-cache-context"]); } @@ -173,28 +184,28 @@ protected function getMockContainer() { */ public function validateTokensProvider() { return [ - [[], FALSE], - [['foo'], FALSE], - [['foo', 'foo.bar'], FALSE], - [['foo', 'baz:llama'], FALSE], + [[], TRUE], + [['foo'], TRUE], + [['foo', 'foo.bar'], TRUE], + [['foo', 'baz:llama'], TRUE], // Invalid. - [[FALSE], 'Cache contexts must be strings, boolean given.'], - [[TRUE], 'Cache contexts must be strings, boolean given.'], - [['foo', FALSE], 'Cache contexts must be strings, boolean given.'], - [[NULL], 'Cache contexts must be strings, NULL given.'], - [['foo', NULL], 'Cache contexts must be strings, NULL given.'], - [[1337], 'Cache contexts must be strings, integer given.'], - [['foo', 1337], 'Cache contexts must be strings, integer given.'], - [[3.14], 'Cache contexts must be strings, double given.'], - [['foo', 3.14], 'Cache contexts must be strings, double given.'], - [[[]], 'Cache contexts must be strings, array given.'], - [['foo', []], 'Cache contexts must be strings, array given.'], - [['foo', ['bar']], 'Cache contexts must be strings, array given.'], - [[new \stdClass()], 'Cache contexts must be strings, object given.'], - [['foo', new \stdClass()], 'Cache contexts must be strings, object given.'], + [[FALSE], FALSE], + [[TRUE], FALSE], + [['foo', FALSE], FALSE], + [[NULL], FALSE], + [['foo', NULL], FALSE], + [[1337], FALSE], + [['foo', 1337], FALSE], + [[3.14], FALSE], + [['foo', 3.14], FALSE], + [[[]], FALSE], + [['foo', []], FALSE], + [['foo', ['bar']], FALSE], + [[new \stdClass()], FALSE], + [['foo', new \stdClass()], FALSE], // Non-existing. - [['foo.bar', 'qux'], '"qux" is not a valid cache context ID.'], - [['qux', 'baz'], '"qux" is not a valid cache context ID.'], + [['foo.bar', 'qux'], FALSE], + [['qux', 'baz'], FALSE], ]; } @@ -203,14 +214,16 @@ public function validateTokensProvider() { * * @dataProvider validateTokensProvider */ - public function testValidateContexts(array $contexts, $expected_exception_message) { + public function testValidateContexts(array $contexts, $expected_return) { $container = new ContainerBuilder(); $cache_contexts = new CacheContexts($container, ['foo', 'foo.bar', 'baz']); - if ($expected_exception_message !== FALSE) { - $this->setExpectedException('LogicException', $expected_exception_message); - } - // If it doesn't throw an exception, validateTokens() returns NULL. - $this->assertNull($cache_contexts->validateTokens($contexts)); + // if ($expected_exception_message !== FALSE) { + // $this->dieOnRaise = TRUE; + // $this->setExpectedException('\\Drupal\\Tests\\AssertionException'); + // } + // If it doesn't throw an exception, validateTokens() returns TRUE. + // It has to do this or assertions aimed at it will fail. + $this->assertEquals($expected_return, $cache_contexts->validateTokens($contexts)); } } diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheTagsInvalidatorTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheTagsInvalidatorTest.php index 27c70e5..179688f 100644 --- a/core/tests/Drupal/Tests/Core/Cache/CacheTagsInvalidatorTest.php +++ b/core/tests/Drupal/Tests/Core/Cache/CacheTagsInvalidatorTest.php @@ -10,6 +10,7 @@ use Drupal\Core\Cache\CacheTagsInvalidator; use Drupal\Core\DependencyInjection\Container; use Drupal\Tests\UnitTestCase; +use Drupal\Tests\AssertionTestingTrait; /** * @coversDefaultClass \Drupal\Core\Cache\CacheTagsInvalidator @@ -17,14 +18,21 @@ */ class CacheTagsInvalidatorTest extends UnitTestCase { + use AssertionTestingTrait; + + public function setUp() { + $this->startAssertionHandling(); + parent::setUp(); + } + /** * @covers ::invalidateTags * - * @expectedException \LogicException - * @expectedExceptionMessage Cache tags must be strings, array given. + * @expectedException Drupal\Tests\AssertionException */ public function testInvalidateTagsWithInvalidTags() { $cache_tags_invalidator = new CacheTagsInvalidator(); + $this->dieOnRaise = TRUE; $cache_tags_invalidator->invalidateTags(['node' => [2, 3, 5, 8, 13]]); } 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/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php index 7f24130..2f1a3f8 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php @@ -96,6 +96,7 @@ protected function setUp() { $this->cacheContexts = $this->getMockBuilder('Drupal\Core\Cache\CacheContexts') ->disableOriginalConstructor() ->getMock(); + $this->cacheContexts->method('validateTokens')->willReturn(TRUE); $this->cacheContexts->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..eacea64 --- /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 a44e5c5..e4de701 100644 --- a/index.php +++ b/index.php @@ -10,14 +10,16 @@ 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'; -try { +FaultSetup::start(); +try { $request = Request::createFromGlobals(); $kernel = DrupalKernel::createFromRequest($request, $autoloader, 'prod'); $response = $kernel