Part of the Well Formed Errors Initiative
Problem/Motivation
Even the experienced developers can be stumped or stalled when a large program they are learning throws a cryptic error or exception from an unfamiliar section of the code, especially when the actual failure is much up the stack. PHP's assert statement can be used to prevent the code from reaching that point. Assertions are also a more efficient way to perform sanity checks on the code operation (such as cache tag and context merges) which are unnecessary in production since the faults cannot occur given bug free code and proper module configuration.
What are Assertions? What's the difference between Assertion, Exception and Error?
Assertions get confused with exceptions by novice and even some experienced programmers, so here is a summary of what they are, what they do, and what makes them distinct and useful. A more in depth explanation is in the comments of this issue.
- Assertions document the code and openly state conditions that are impossible given error free code and module configuration. Unlike errors and exceptions, assertions can be disabled, and in this state they have no more effect on the runtime than the comment text (so long as they are passed as strings to be evaluated, not as output from functions or expressions).
- Exceptions deal with user input mistakes, other programs malfunctioning, hardware failures, network outages, etc. If a situation can arise from outside the program, no matter how rare it is in practice, it should be handled by an Exception. Exceptions can be catch by code further up the stack using try/catch blocks. Neither assertions nor errors have this ability.
- Errors either
- Deal with situations which are by nature irresolvable (such as a parse error) and will require system shutdown - hence no opportunity should be given to catch them at all.
- Log circumstances that may be important but do not leave the program in an unstable state - such as the calling of a function scheduled for deprecation.
Keep in mind the following: All bugs are errors, but not all errors are bugs. Assertions catch bugs -- errors in the program code and its configuration files. They are never to be used to catch errors which can arise from the user or environment as these aren't bugs (though failure to properly handle them is often called such).
Proposed resolution
There are two parts to this ticket.
First -- a Fault System. The version in this ticket is a component loaded by the index before the Kernel. This is model for how 8.1 and forward may handle faults - #2465447: Multiple Environment Loading - A breakup of DrupalKernel. Do we want to do this? - but other parts of that discussion lie beyond the scope of fault handling itself as well as the goals of this issue - which is:
- Implement a minimal assert handler that links back to relevant API sections.
- Provide tools to write assert statement aware unit tests
Second -- As a test of how the use of assert statements will affect the code base all existing calls to CachecontextsManager->validateTokens have been placed within assert statements (calling new wrapping function assertValidTokens to get a true/false return need by assert), and all calls to Cache::validateTags have been replaced by assertion that the tags are all strings (which is all validateTags does).
The term "Fault" is a collective for "Any Assert Failure, Error or Uncaught Exception" Exceptions that are caught within the program and handled aren't considered faults - only exceptions that escape to the top of the stack frame to be caught by the final exception handler are faults.
Again in the interest of minimal disruption the code introduced in this issue ticket deals with assert failures only. The brief of changes.
- .htaccess gains a line to turn assertions off since the PHP default is to have them on. This will an exception to the rule that settings that can be changed at runtime are changed at runtime since the assert_active state is a run time changable directive.
- index.php loads FaultSetup immediately after the autoloader is in place but before DrupalKernel is called.
- FaultSetup looks at the value of assert_active and if it sees it set to 1 it will register an assert handle and activate assert_bail. Otherwise it does nothing.
- Drupal\Component\Fault\Assertion is a library of commonly made assertions.
- Drupal\Component\Fault\AssertionHandler handles assert failures. It is the only handler in this patch.
- Drupal\Component\Fault\BaseFaultHandler is the parent of all the handlers, and contains the basic fault display and logging logic. It also uses the stack trace of the fault to determine which Drupal API pages to give links to on the error page. It is self contained with no reliance on any other files.
In addition to the above, these are the tests and the additions to the testing system overall.
- Drupal\Tests\AssertionException Tests will sometimes need to elevate an Assert Raise to an exception to guarantee a test halt since the assertion handling mechanism normally logs the assert raise but allows code flow to continue to simulate what will happen to the code if it is ran with assertions turned off. All tests involving assertions should be conducted with that possibility in mind.
- Drupal\Tests\AssertionTestingTraitStored in the root test namespace since all core and component items needing to test assertion statements *may* use this. If they don't then assertion raises will be transformed into a PHPUnit_Framework_Exception as they are treated as E_Warnings. A class that uses this trait has the ability to more thoroughly interact with assert statements during testing.
- Drupal\Tests\ToStringMock A class that implements __toString. That's it, and I was very surprised this didn't exist. This can be removed from this patch, but the child patch will need it and any assert that checks for string-convertable objects.
- Drupal\Tests\Component\Fault\AssertionHandlerTest checks the javascript output of the handler since that is a simple string.
- Drupal\Tests\Component\Fault\AssertionTestChecks the static assertion object's assertions to make sure they work as intended.
Remaining tasks
Finalize message parsing
Fault messages may be labeled with a single word preceded by an @. This will correspond to a page at https://www.drupal.org/developing/api/faults/assertions/8/. A node id will also be converted.
User interface changes
None.
API changes
No changes, minor additional options opened for developers writing tests.
Beta phase evaluation
Issue category | Task because this is an enhancement and developer's aid only. |
---|---|
Issue priority | Major |
Prioritized changes | Using assert has been verified to give performance gains in #2454643: Optimize cacheability bubbling (Cache::mergeTags(), ::mergeContexts(), BubbleableMetadata), so this minimal complexity version is being built for 8.0.x |
Disruption | Adding assertions will expose additional existing bugs, and already has during testing #2465749: Widespread HTML validation issue - The ID attribute MUST be unique on the page.. Priority will need to be assigned on those bugs. It is hoped that at least a few of these turn out to be the culprits behind the remaining critical bugbears. Note though the code itself isn't creating new bugs, simply exposing them. |