Problem Motivation

Whenever possible, we need to have PHP 5.x and 7 behave the same way during testing. PHP 7 allows assertions to be thrown as exceptions, and PHP 5 allows it as well albeit with a bit more work. By having both versions handle runtime assertions in the same manner unit tests can be simplified.

Proposed Solution

Create a static function in the Assertion component to setup consistent assert failure handling across both versions of the language. In PHP 7 simply set assert.exception to TRUE. In PHP 5 an AssertionError class must be declared in the global namespace, thrown using an assert callback, and E_WARNING errors suppressed.

This function will be called from DrupalKernel::bootstrap when simpletest is running a webtest - assertions will be enabled at that point as well insuring webtests are always conducted with assertions on. PHPUnit's bootstrap will also call the function, but it will not enable assertions and instead fallback to the server default since it doesn't read in the .htaccess setting either. For most deployments the default is TRUE. Finally it is called from settings.local.php so that development environments will have assertions turned on.

With this in place testing for a runtime assertion failure in PHPUnit is done this way.

/**
 * @expectedException AssertionError
 */

Simpletest requires a manual capture with a try/catch block.

CommentFileSizeAuthor
#63 2536560-63.patch7.9 KBstefan.r
#63 interdiff-62-63.txt5.01 KBstefan.r
#62 interdiff.txt3.96 KBAki Tendo
#62 2536560-62.diff7.92 KBAki Tendo
#57 2536560-57.patch6.99 KBdamiankloip
#54 2536560-53.patch6.56 KBdamiankloip
#50 sanity.diff178 bytesAki Tendo
#49 2536560-49.diff6.97 KBAki Tendo
#47 2536560-47.diff7.64 KBAki Tendo
#42 2536560-42.patch6.01 KBstefan.r
#42 interdiff-41-42.txt1.01 KBstefan.r
#41 2536560-41.patch5.93 KBstefan.r
#41 interdiff-28-41.txt5.07 KBstefan.r
#28 2536560-28.diff6.73 KBAki Tendo
#24 2536560-24.diff15.75 KBAki Tendo
#23 2536560-23.diff10.54 KBAki Tendo
#19 2536560-19.diff10.54 KBAki Tendo
#16 2536560-16.diff27.12 KBAki Tendo
#14 2536560-14.diff4.93 KBAki Tendo
#13 php56recoverablelist.txt8.64 KBAki Tendo
#7 2536560-7.diff3.42 KBAki Tendo
#6 2536560-6.diff3.33 KBAki Tendo
#3 2536560-3.diff2.43 KBAki Tendo
#1 2536560-1.diff2.4 KBAki Tendo
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Aki Tendo’s picture

Status: Active » Needs review
FileSize
2.4 KB

Ok, This should pass.

Status: Needs review » Needs work

The last submitted patch, 1: 2536560-1.diff, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
2.43 KB

Group annotation missing.

Crell’s picture

Making the file general to PHP 7 seems like an over-reach. That needs more discussion from the RMs, and right now I'd just be happy to get the annotations patch committed and call it a day.

Not that it shouldn't happen, but beware the scope creep...

Aki Tendo’s picture

Issue summary: View changes
Issue tags: +Needs framework manager review

Ok, We have a few paths we can go down here, each with upside and downside.

1. We go the route seen at comment # 308 in the assert tools patch, linking the shim code from settings.local.php, simpletest.module and the PHPUnit bootstrap.

https://www.drupal.org/node/2408013#comment-10135006

Pro: This is the smallest baby-step.
Con: Assertion failures that evaluate before the loading of the simpletest module or settings.local.php file will be errors in PHP 5 and exceptions in PHP 7. Stopping this requires an earlier include point.

2. We go the route seen in the patch already submitted here, patching into index.php file even before the autoloader runs.

Pro: This will ensure that Assertion behavior is consistent system-wide.
Con: Runs against the minimalist grain desired for the index.

---

Outside of the AssertionException issue it's desirable I think to convert E_RECOVERABLE_ERROR to TypeException. This either requires a set_error_handler call and then the existing Drupal error handlers have to be registered to not overwrite it like so

set_error_handler('_drupal_error_handle', E_ALL ^ E_RECOVERABLE_ERROR);

Or the conversion of E_RECOVERABLE can be done at the start of drupal_handle_Error.

There will be unit tests that need adjusting as well that are already trying to account for PHP 5 and 7's difference, but long term that should make those tests more stable.

As far as I know these are the only two error to exception conversions that are possible, all the other new PHP 7 exceptions will just flat out fail. I haven't finished reading the related RFC's though. If others exist the exception conversion must occur before the drupal error handler fires.

Input sought on this - I'm unsure of the best path here.

Aki Tendo’s picture

Ok, let's see what breaks (The Error handling test I expect, but I'm curious as to what else).

Aki Tendo’s picture

Hard to throw an exception that hasn't been defined.

The last submitted patch, 6: 2536560-6.diff, failed testing.

Aki Tendo’s picture

There is a serious error in _drupal_error_handler_real() 's call to _drupal_log_error(). The second parameter it passes is the expression $error_level == E_RECOVERABLE_ERROR. This is the FALSE flag for drupal_log_error(), and it means this function has been treating all errors as non-fatal except the typeHint failures.

Since this patch is rerouting E_RECOVERABLE_ERROR to an exception throw it makes sense to go ahead and fix this line so that it throws the correct status to the log. Specifically:

$error_level & ~(E_NOTICE + E_USER_NOTICE + E_WARNING + E_USER_WARNING + E_DEPRECATED + E_USER_DEPRECATED + E_STRICT)

This is my intent - Create a bitmask of the nonfatals by adding their constants together, then does a bitwise check against the mask. So if $error_level is not in the set (i.e. is fatal) it returns true. Anyone want to verify that?

Status: Needs review » Needs work

The last submitted patch, 7: 2536560-7.diff, failed testing.

Crell’s picture

Linking to some previous work on making our error handling not suck... which it looks like you already found. :-) Fixing our error handling to not be totally broken feels like it's a separate matter from having a PHP7 shim. If the only way to get both in is to do them together, fine, but that's a framework manager question. As long as both happen before release I'm OK.

One way to simplify the shim is to make it a composer library. No, really. A composer library can declare a file that gets included, period. That would happen as soon as the composer autoload is executed, which is already the first line of index.php. If we're going to do it, go all the way.

Aki Tendo’s picture

Agreed.

Aki Tendo’s picture

FileSize
8.64 KB

Attached is a grep of the PHP 5.6 codebase for E_RECOVERABLE. Addressing that whole list is something I'm not qualified to do alone so I'm going to scale back to just AssertError throws for the first version of the package.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
4.93 KB

Ok, here's the assertion error throw code as a composer require.

Status: Needs review » Needs work

The last submitted patch, 14: 2536560-14.diff, failed testing.

Aki Tendo’s picture

K. Got it now I think.

Aki Tendo’s picture

Status: Needs work » Needs review

::kicks the testbot::

Status: Needs review » Needs work

The last submitted patch, 16: 2536560-16.diff, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
10.54 KB

Fresh startover. It is working locally.

dawehner’s picture

+++ b/core/vendor/aki-tendo/php7exception/tests/AssertionErrorTest.php
@@ -0,0 +1,23 @@
+require_once '../php7exceptions.php';
+assert_options(ASSERT_ACTIVE, 1);

Mh, should this test then run in isolation somehow?

+++ b/core/vendor/aki-tendo/php7exception/php7exceptions.php
@@ -0,0 +1,34 @@
+if (version_compare(PHP_VERSION, '7.0.0-dev') === -1) {
+  class AssertionError extends Exception {}
+
+  // Assertions throws.
+  assert_options(ASSERT_CALLBACK, function($file, $line, $code, $message = '') {
+    if (empty($message)) {
+      $message = "Assertion Failure in {$file} at {$line}. Failed asserting {$code}";
+    }
+    throw new AssertionError($message);
+  });
+}
+

I'm curious how we could minimize the work which needs to be done in case you have assertions off, like in production

Aki Tendo’s picture

1. It should be able to run as long as phpunit is installed. It doesn't need a bootstrap. I could write a drupal companion to this function, but I don't think that's necessary - if this code where to start failing all unit tests running under PHP 5 with @expectedException AssertionError will start failing, so a regression can't sneak in under the radar.

2. I could make the assignment of the closure contingent on the status of assert_options, but this creates the problem of not having the callback in place if they get turned on later, like in settings.local.php. At the moment the whole file is unnecessary in production - but once the E_RECOVERABLE errors are added in then the catching of those throws in a uniform manner will become important.

On the other hand, if you're running PHP 7 this is a dead file, but opcode caching mitigates that. It would be nice if composer could be configured to conditionally load files and only load this when PHP version < 7.

In the end I think the registration of a closure that never gets called in production will have an insignificant impact on performance compared to the gain on not checking cache tags and context tags at all times - so we're giving back a very small piece of the performance gain on that front by doing it this way. I will agree that it's not ideal but all better solutions I can think of introduce setup complexity for site admins and developers I'd rather avoid.

Aki Tendo’s picture

Our other best option is to just go back to the pattern that was nearly RTBC'ed in the assert tools patch - use a require statement in bootstrap.php, simpletest.module and example.settings.local.php. Doing this would require the package to be modified in order to remove it's autoload information, but that autoloading was the point of putting it in the package in the first place.

Do we go that route or leave things as is? I really would like to get this out of the way so the cache optimizations can be brought up to speed.

Aki Tendo’s picture

Re-roll: e64a4c7 also modifies core/composer.lock so the previous patch will no longer work (it was tested just prior to e64a4c7). No operative changes from previous patch.

Aki Tendo’s picture

Title: Implement a compatibility shim for PHP 5.x to PHP 7 - Phase 1, Assertion Handling » Runtime Assertion unit and functional testing
Issue summary: View changes
Parent issue: » #2408013: Adding Assertions to Drupal - Test Tools.
FileSize
15.75 KB

At dawehner's suggestion the package has been linked up to Travis CI and tested with it - which was a good thing since it wasn't running under PHP 7. This has been rectified and the package is now on version 1.0.2

This patch installs the shim, then tests it by going ahead and testing to make sure testing itself is possible in simple test. This also addresses the problems with the automated test runners.

Aki Tendo queued 24: 2536560-24.diff for re-testing.

stefan.r’s picture

Just out of interest, what still needs to happen here?

Aki Tendo’s picture

I'm guessing I need to figure out what the fails in PHP 7 are from. That said, I've looked over the test results and the fails seem unattached to this patch. I'm considering running core itself directly through and see if its passing PHP 7 - last I had checked it was not.

Aki Tendo’s picture

K, new approach that doesn't use an outside package. This has the streamlining in two places since composer doesn't load includes/bootstrap.inc so PHPUnit's bootstrap needs the unifying code - but it will only ever be in these two places since if we discover another needed hook point the new function drupal_register_assert_handle() can be called.

cosmicdreams’s picture

Aki Tendo’s picture

No. The issue here is on how best to unit test runtime assertions and making sure they are running during functional tests regardless of the .htaccess setting.

Aki Tendo’s picture

None of the failures above on 7 are related to this patch at all, unless such notices are being thrown as AssertionErrors. I doubt this - it's undocumented behavior if it is what is happening.

Aki Tendo queued 28: 2536560-28.diff for re-testing.

Aki Tendo’s picture

Queing for retest to see how many of the unrelated PHP 7 funny business errors is still out there and start attacking them so this can run entirely clean.

stefan.r’s picture

  1. +++ b/core/includes/bootstrap.inc
    @@ -1132,3 +1132,25 @@ function _drupal_shutdown_function_handle_exception($exception) {
    + * Unify PHP 5 and 7's assertion handling.
    

    Wonky grammar

  2. +++ b/core/includes/bootstrap.inc
    @@ -1132,3 +1132,25 @@ function _drupal_shutdown_function_handle_exception($exception) {
    +  // PHP 5 - we must establish an AssertionError to throw and a assert callback
    

    an assert

  3. +++ b/core/includes/bootstrap.inc
    @@ -1132,3 +1132,25 @@ function _drupal_shutdown_function_handle_exception($exception) {
    +        $message = "Assertion Failure in {$file} at {$line}. Failed asserting {$code}";
    

    I don't see any tests for this error string so just wondering if this is this the same error string as used in PHP7?

  4. +++ b/core/modules/simpletest/src/Tests/SimpleTestTest.php
    @@ -183,15 +192,15 @@ function stubTest() {
    +    // This causes the twelth of the sixteen passes asserted in
    

    twelfth

  5. +++ b/core/tests/bootstrap.php
    @@ -89,3 +89,20 @@ function drupal_phpunit_register_extension_dirs(Composer\Autoload\ClassLoader $l
    +// PHP 7 - just make sure assert failures are thrown.
    +if (version_compare(PHP_VERSION, '7.0.0-dev') >= 0) {
    +  assert_options(ASSERT_EXCEPTION, 1);
    +}
    +// PHP 5 - we must establish an AssertionError to throw and a assert callback
    +// handle to throw it.
    +elseif (assert_options(ASSERT_CALLBACK) === NULL) {
    +  class AssertionError extends Exception {}
    +
    +  assert_options(ASSERT_CALLBACK, function($file, $line, $code, $message = '') {
    +    if (empty($message)) {
    +      $message = "Assertion Failure in {$file} at {$line}. Failed asserting {$code}";
    +    }
    +    throw new AssertionError($message);
    +  });
    +}
    

    Can we call drupal_register_assert_handle() here instead?

  6. +++ b/core/includes/bootstrap.inc
    @@ -1132,3 +1132,25 @@ function _drupal_shutdown_function_handle_exception($exception) {
    +function drupal_register_assert_handle() {
    
    +++ b/sites/example.settings.local.php
    @@ -28,6 +28,7 @@
    +drupal_register_assert_handle();
    

    Does this have to be a function call necessarily? Would it make sense to make it a variable that flags whether to enable assertions and call drupal_register_assert_handle() right after loading the settings file?

stefan.r’s picture

Status: Needs review » Needs work
alexpott’s picture

+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -870,6 +870,10 @@ public static function bootEnvironment() {
+        assert_options(ASSERT_ACTIVE, 1);
+        drupal_register_assert_handle();

+++ b/sites/example.settings.local.php
@@ -28,6 +28,7 @@
 assert_options(ASSERT_ACTIVE, 1);
+drupal_register_assert_handle();

Why don't we just check on regular runtime if asserts are active and then set the assertion handler. This means if someone sets up assertions in .htaccess or wherever it just works.

stefan.r’s picture

I like that idea! So we only keep the assert_options(ASSERT_ACTIVE, 1); in sites/example.settings.local.php anymore?

alexpott’s picture

@stefan.r exactly!

alexpott’s picture

Assertion testing at the moment is very painful - see #2505497-64: Support render arrays for drupal_set_message() and we still have to work on convincing people what assertions should be used for.

alexpott’s picture

I like the current approach once #34 and #36 have been addressed.

stefan.r’s picture

Status: Needs work » Needs review
FileSize
5.07 KB
5.93 KB
stefan.r’s picture

+++ b/core/includes/bootstrap.inc
@@ -1134,23 +1134,35 @@
-        $message = "Assertion Failure in {$file} at {$line}. Failed asserting {$code}";

I removed this as it made the message incompatible with PHP7. According to the Assertions RFC, the PHP7 behavior is to use the defined assertion message as exception message, and if no message is defined, to use the code. We pass the file and line numbers along in the exception object instead for the error handler to deal with.

+++ b/core/modules/simpletest/src/Tests/SimpleTestTest.php
@@ -164,7 +164,24 @@ function stubTest() {
+    // Check to see that assertion messsages get passed along.

Messages has an s too many there, for us to fix once tests pass

The last submitted patch, 41: 2536560-41.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 42: 2536560-42.patch, failed testing.

Aki Tendo’s picture

@stefan

Can we call drupal_register_assert_handle() here instead?

No. That function is defined in includes/bootstrap.inc and PHP Unit's bootstrap does NOT load that file.

Why don't we just check on regular runtime if asserts are active and then set the assertion handler.

Since assertion status can be changed at runtime we have to insure that the handler is set when assert_active is set. We can do this for drupal code and I can modify the patch appropriately. If someone sets the assert_active flag after the handler's setting code runs then behavior will be the default for the platform, which isn't necessarily a bad thing but something to be aware of.

Before I do a new version though I'd like to know which route is warranted -

1. Use a package as seen in #24, having composer set up the handler as a global property
2. Use drupal call functions as seen in #28

Also a third option -- I know there was talk of allowing modules to have composer files. The most efficient way to do this is to attach the handler package to the simpletest library so that it is loaded only when that module is enabled. Incidently, this would also be a very sneaky way of making a BooBoo or Whoops module since they could use composer to load the files they need to have in place before the main script is underway - essentially composer becomes a form of hook_boot()

I'm not married to any approach but I'd like to know which one is favored before proceeding further.

EDIT: What's important isn't that the handler be registered at all times, just during the unit tests so that the test can be written in a uniform manner. That's a strike against #1. If #3 is possible I like that but #2 is the likely one.

Aki Tendo’s picture

Speaking with xjm in IRC we'll go with #2. I have some ideas on a smoother implementation he helped with.

Aki Tendo’s picture

Did this next patch as a clean rebuild so I can't easily do an interdiff. Here's a rundown of the changes with remarks regarding comments above.

core.api.php
Single change here is to bring the Runtime assertion docs in line with what PHP 7 actually does - they changed the name of the exception from AssertionException to AssertionError at some point.

b/core/lib/Drupal/Component/Assertion/Handle.php
New file. This will be the home of the registering code for the handler allowing it to be lazy loaded. The file does have the quirk of being double namespaced like some tests but this is unavoidable - AssertionError must reside at the root namespace.

@stefan_r -- At #42 you proposed overriding the constructor of Exception for AssertionException. That can't work because $this->file and $this->line are private, further if you do not invoke the parent::__constructor the object doesn't properly register with the runtime as an exception object and everything crashes. I strongly, strongly recommend staying away from modifying how the exception works - it should be enough that the assertions have the same class so that PHPUnit and simpletest can catch them either through annotation or the catch statement. So I did not incorporate this change into what I hope is the final patch.

I did incorporate simply passing the error code along, but I don't think that's going to work - I think PHP 7's default message string includes the line and file but I'm not sure of the exact wording - I am building a PHP 7 environment this weekend to figure it out and get this finalized.

@alexpott

Why don't we just check on regular runtime if asserts are active and then set the assertion handler. This means if someone sets up assertions in .htaccess or wherever it just works.

While I answered this in #45 I also misunderstood the question somewhat, so let me go over this again now that I'm thinking straight.

We can't both set this up at runtime and also lazy load the code. The clean way to run this code at the earliest opportunity is through the package approach seen in the patch at #24. I can modify that package to check if ASSERT_ACTIVE is set, but if it isn't and is then set true later in the code the callback won't be present and this could cause confusion. The ability to set the callback when ASSERT_ACTIVE changes is therefore needed and that in turn requires the code that sets the callback to live in a function (patch at comment #28) or with a static class (patch as of this comment). Neither of these options mesh well with a composer package so when in the runtime do we make the check? As early as possible, but we don't want to mess with index. DrupalKernel is a maze at the moment. In short there's no good place other than in a composer package to insert the assert handle registering code. That's ruled out by not wanting the code to be invoked during production so that leaves us with a function include, or a lazy loaded static class function. I believe the lazy load approach has the least overhead and is best.

It is with these problems in mind that I removed the ASSERT_ACTIVE check in the function so that if it gets called the handler is going to be registered. It does mean that if we turn assertions on at runtime we have to call for the handler. This occurs at three places - DrupalKernel for the webtest runners, PHPUnit's bootstrap, and example.settings.local.php for dev environments. The first and last of these set ASSERT_ACTIVE to TRUE, and PHPUnit bootstrap inherits the php.ini setting which is also TRUE by default. This exposes a corner case of changing the .htaccess setting and not having a handler but I don't believe this is an especially large problem.

The alternative given our current config approach is register the handler at all times making it a wasted action in production since the callback will never be called. This was the primary objection to the patch at #24. There is no current way to have composer conditionally load the file - even if there was how would we signal this? Such auto loads are processed way before any drupal specific work begins.

Let's step back a moment - why are assert statements going into the code in the first place? To make life easier on the module developers first, then the core developers. Setting ASSERT_ACTIVE to TRUE in settings.local.php is going to cover external module developers in full and the work on the internal modules as well. Further, I don't think there is an assert in the code base yet that could be failed before settings.local.php is loaded. Even if there was the default behavior of triggering an E_WARNING error should be sufficient. This patch is only really needed when a unit test needs to be written where the assert statement must fail to fulfill the test; and to make sure runtime assertions are on during the web tests. The former is already a very rare situation - unit testing assert statements is usually akin to unit testing unit tests: redundant. It does come up as can be seen in the presence of such tests in Drupal, but its a corner case to begin with. Finally, with the changes in the current patch PHPUnit will be able to conduct a test, but simpletest will not so that in my mind makes this a very to extremely rare possibility.

I believe the patch as it stands deals with the vast majority of our potential use cases, enough to warrant inclusion after another round of code review for spell checks (assuming the tests themselves come back clean). The few problems that do remain are strongly tied to problems with Drupal's error handling and environment setup system to begin with, and they will go away once those problems are addressed - but they are big problems that have already been pushed out to 8.1 minimum for good reason. What is here I think works for now and keeps things moving forward.

Thoughts?

Status: Needs review » Needs work

The last submitted patch, 47: 2536560-47.diff, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
6.97 KB

Oops.

Aki Tendo’s picture

At this point I'm beginning to doubt 8.0.x is clean under PHP 7. Let's find out.

Aki Tendo’s picture

Ha! Head isn't working in PHP 7 yet. Same number of fails as the sanity check. So, I guess this means this is clear for final review.

stefan.r’s picture

I did incorporate simply passing the error code along, but I don't think that's going to work - I think PHP 7's default message string includes the line and file but I'm not sure of the exact wording - I am building a PHP 7 environment this weekend to figure it out and get this finalized.

Good, that keeps it in line with PHP 7 behavior. Maybe you were confusing the exception message with the warning message?

- For warnings (when the option ASSERT_WARNING is enabled), the error message is: Warning: assert(): assert(FALSE) failed in /in/h1tDW on line 5

- For uncaught exceptions, the error message is: Fatal error: Uncaught AssertionError: assert(FALSE) in /in/QNFhp:10 Stack trace: #0 /in/QNFhp(10): assert(false, 'assert(FALSE)') #1 {main} thrown in /in/QNFhp on line 10

- As to the actual exception message, it is the message defined in the assert() statement, or alternatively the evaluated code (rfc), so the current patch should be OK:

assert_options(ASSERT_EXCEPTION, TRUE);
try {
  assert(FALSE);
}
catch (\AssertionError $e) {
    echo 'Message 1 is: ' . $e->getMessage();
}
try {
  assert(FALSE, 'Custom message');
}
catch (\AssertionError $e) {
    echo 'Message 2 is: ' . $e->getMessage();
}

Outputs this on PHP7: Message 1 is: assert(FALSE)Message 2 is: Custom message

(per http://3v4l.org, which is quite nifty for testing PHP 7 code!)

stefan.r’s picture

  1. +++ b/core/lib/Drupal/Component/Assertion/Handle.php
    @@ -0,0 +1,47 @@
    +namespace {
    +  if(!class_exists('AssertionError', FALSE)) {
    +    class AssertionError extends Exception {}
    +  }
    +}
    

    Missing space before the if opening parenthesis. Does this need to be in its own file? (I don't know, I'm asking)

  2. +++ b/core/lib/Drupal/Component/Assertion/Handle.php
    @@ -0,0 +1,47 @@
    +  /**
    

    missing newline after namespace opening bracket, I think we also don't indent code inside a namespace code block.

  3. +++ b/core/lib/Drupal/Component/Assertion/Handle.php
    @@ -0,0 +1,47 @@
    +    /**
    

    missing newline after class opening bracket

  4. +++ b/core/lib/Drupal/Component/Assertion/Handle.php
    @@ -0,0 +1,47 @@
    +     * Registers an assertion handler in PHP 5, sets exception throwing in PHP 7.
    

    80 cols

  5. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -862,6 +862,11 @@ public static function bootEnvironment() {
    +        // Turn assertions on, sync PHP 5 & 7's handling of them.
    

    The second part of that comment doesn't accurately describe what the line directly underneath does (rather describes the call to the handler). So we should either drop it or move it one line down.

  6. +++ b/sites/example.settings.local.php
    @@ -27,7 +27,8 @@
    +\Drupal\Component\Assertion\Handle::register();
    

    Surely there must be a workaround to do without this? Would be good to hear thoughts from @alexpott/others on this (taking into account the remarks in #47).

  7. @stefan_r -- At #42 you proposed overriding the constructor of Exception for AssertionException. That can't work because $this->file and $this->line are private, further if you do not invoke the parent::__constructor the object doesn't properly register with the runtime as an exception object and everything crashes. I strongly, strongly recommend staying away from modifying how the exception works - it should be enough that the assertions have the same class so that PHPUnit and simpletest can catch them either through annotation or the catch statement. So I did not incorporate this change into what I hope is the final patch.

    Not sure I follow? I did test that piece of code and it worked fine -- $file and $line are protected and the parent constructor _is_ invoked so this should not crash ever. And if it's the constructor signature we're worried about, it's a PHP5-only workaround. See also https://secure.php.net/manual/en/language.exceptions.extending.php

    Just for reference, the code was:

        class AssertionError extends Exception {
          public function __construct($message, $code = 0, Exception $previous = NULL, $file, $line) {
            parent::__construct($message, $code, $previous);
            // Preserve the filename and line number of the assertion failure.
            $this->file = $file;
            $this->line = $line;
          }
        }
    
        assert_options(ASSERT_CALLBACK, function($file, $line, $code, $message = '') {
          if (empty($message)) {
            $message = $code;
          }
          throw new AssertionError($message, 0, NULL, $file, $line);
        });
    

    This is all just so the actual throwable works the same as in PHP7 and passes the right file and line number along (that of the assertion failure instead of that of the callback handle).

damiankloip’s picture

Here is a re-roll.

We have been looking at/testing this as I think we need it for https://github.com/drush-ops/drush/pull/1543, so we can replace the Drupal Container::__sleep() method trigger_error with an assertion.

This doesn't quite work yet, as the patch currently only enables this handler for tests. I think we want this handler enabled all the time? but assertions only enabled for tests maybe? or if it is been opted into via settings. Thoughts? You guys have been looking into this far more than me :)

damiankloip’s picture

I think I can just set whatever I need in the drush command itself. When this is committed, I could maybe call Handle::register function? The part that is problematic for me, is that you are currently not setting assert_options(ASSERT_WARNING, FALSE); also. Otherwise, you still get the warning anyway. So I think maybe this patch needs to add that in? You will still get the exception, which is what we really want.

stefan.r’s picture

@damiankloip you missed the change to example.settings.local.php in the reroll?

Setting ASSERT_WARNING to 0 makes sense, but if people set it to 0 in their PHP settings it will ignore/overwrite the setting... Is there a use case where the exception is not enough and people want the warning as well?

damiankloip’s picture

I didn't mean a setting specifically, I just meant however it is set now, which seems to just be directly in the local settings. Which is fine by me. I would say the exception would be enough. The assert callback is a replacement for that I would say.

Where do you think is the best place to set ASSERT_WARNING? Always, or just in the places that currently set ASSERT_ACTIVE and register()? This is turning into a few pieces. Maybe more should move into Handle::register() ?

It's not a massive issue though, I can just set what I need in the drush command itself, it would just be nice to wait for this patch so I can use the register() helper provided.

Here is correct patch.

Aki Tendo’s picture

Since this patch throws an AssertionError using an ASSERT_CALLBACK the value of ASSERT_WARNING is irrelevant in th e places where the handler is registered. PHP 7 introduces the ASSERT_EXCEPTION flag, and this patch sets that flag to 1 in PHP 7 and doesn't register a handler for it since one isn't needed for the desired effect.

Drush isn't affected by the .htaccess setting that was introduced in the assert tools patch - it will continue to function according to the php.ini default. My thought is that's ok, admin's savvy enough to be using drush should be comfortable making the necessary change to their php.ini file on production boxes to set assert.active to 0.

If drush uses it's own php.ini file then assert.active could be set there for a more uniform default - or the flag could be set according to whether drush is in test mode. But it is that setting, not assert_warning, that should be changed. assert.warning only deals with *how* the message gets delivered to the user.

In an idea world all users would understand that production machines should have assertions turned off, negating the need to have any assert setting declaration in the assert tools patch. That way drupal just goes along with the machine settings as is. But PHP ships with assertions on by default and most users don't change the setting since they don't understand what it is for (understandable). This creates the problem that if Drupal makes full use of assertions for runtime validating then, over time, they'll cause a noticable performance hit. People aren't going to blame their bad setting on this, they're going to blame drupal, fair or no.

damiankloip’s picture

Since this patch throws an AssertionError using an ASSERT_CALLBACK the value of ASSERT_WARNING is irrelevant in the places where the handler is registered.

This doesn't seem to be the case from my testing with drush. With the assert callback registered, a warning is still triggered, as well as the exception. So that how is a problem.

I don't think people using drush will necessarily know about that ini setting. Wouldn't that be overridden by the assert_option call in a sites local.settings.php anyway? I want behaviour for the drush repl to be consistent. As said before, I am fine with just setting these in the drush command itself to do what I need.

Aki Tendo’s picture

Yes, If drush loads settings.local.php that assert_option call goes into effect from that point in the code onward. Turning warnings off when registering an exception thrower seems a reasonable step and necessary to get the tests to behave I'll guess.

damiankloip’s picture

Yes, agreed. I think if we have the exception handler registered, like your Handle class does, we don't need the ASSERT_WARNING.

Drush will bootstrap Drupal, so whichever settings are normally used will be in effect there too. In Drush I think we will need to make sure that handler is registered all the time. That does not affect this though. If the Handle::register method does not disable the trigger_error form ASSERT_WARNING, I can do that too in drush.

Aki Tendo’s picture

Ok, code from 42 ported in and corrected (I was wrong the crashes where off the last two parameters being non-optional but following parameters that are optional - the fix is to make all of them optional). As for #53

1. Drupal currently has no code on the root namespace - so autoloading this from its own file would require a reconfig on composer. That's way out of scope here.

2-5 fixed

6. The consensus seems to be we don't want this handler registered if it won't be used for performance reasons.

@damian This patch now turns assert.warning off when Handle::register is called.

stefan.r’s picture

Aki Tendo’s picture

Looks great to me. Can someone who hasn't touched the code give this a final RBTC please?

damiankloip’s picture

This looks good to me also, and confirmed my code is much happier. The only question (I think) is whether it makes sense to have everything (including the enabling of assertions) in the register() method too?

Otherwise, this is good to go.

EDIT: Forget that - I think the flexibility and separation of the handler and the setting is good. I think I can RTBC this one, I did minimal on the actual patch. I just chimed in with my problems etc... :)

damiankloip’s picture

Status: Needs review » Reviewed & tested by the community
dawehner’s picture

  1. +++ b/core/lib/Drupal/Component/Assertion/Handle.php
    @@ -0,0 +1,75 @@
    +<?php
    +/**
    

    Nitpick on commit: new line needed

  2. +++ b/core/lib/Drupal/Component/Assertion/Handle.php
    @@ -0,0 +1,75 @@
    +    /**
    +     * {@inheritdoc}
    +     */
    +    public function __construct($message = '', $code = 0, Exception $previous = NULL, $file = '', $line = 0) {
    +      parent::__construct($message, $code, $previous);
    +      // Preserve the filename and line number of the assertion failure.
    +      $this->file = $file;
    +      $this->line = $line;
    +    }
    +
    

    wow, PHP is just a shitty language. ... given that $file and $line is not part of the original constructor

Aki Tendo’s picture

Normally file and line get set to the point of the throw by the engine. Wanting to change these parameters is a bit of an oddball situation - I can see why they didn't expect it and therefore didn't make it an argument of the default constructor.

Aki Tendo’s picture

Issue summary: View changes

Updating Issue summary to document final course of action at a glance without needing to read through the whole thread to figure out what happened to the patch, in case we need to revisit this at some later date.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This is only changing code that runs during tests and therefore is a permitted change during beta and this is necessary to test assertions in Drupal. Committed 5bb2889 and pushed to 8.0.x. Thanks!

  • alexpott committed 5bb2889 on 8.0.x
    Issue #2536560 by Aki Tendo, stefan.r, damiankloip: Runtime Assertion...
Fabianx’s picture

Not sure that is a bug or a feature, but the following fails:

  assert(FALSE);

with an error:

Missing argument 4 for Drupal\Component\Assertion\Handle::Drupal\Component\Assertion\{closure}()

The following would fix it:

diff --git a/core/lib/Drupal/Component/Assertion/Handle.php b/core/lib/Drupal/Component/Assertion/Handle.php
index 3912156..2f4ee3e 100644
--- a/core/lib/Drupal/Component/Assertion/Handle.php
+++ b/core/lib/Drupal/Component/Assertion/Handle.php
@@ -57,7 +57,7 @@ public static function register() {
 
     if (version_compare(PHP_VERSION, '7.0.0-dev') < 0) {
       // PHP 5 - create a handler to throw the exception directly.
-      assert_options(ASSERT_CALLBACK, function($file, $line, $code, $message) {
+      assert_options(ASSERT_CALLBACK, function($file, $line, $code, $message = '') {
         if (empty($message)) {
           $message = $code;
         }

But not sure if we require a message in Drupal 8.

stefan.r’s picture

@Fabianx that sounds like a bug and the fix makes sense, will post a follow-up

stefan.r’s picture

Seems the default argument used to exist in #42 but it got lost in #62 :/

stefan.r’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.