Skip to content

bug: Improve handling exceptions from unsupported PHPUnit versions#1521

Merged
acoulton merged 2 commits intoBehat:masterfrom
acoulton:bug-phpunit-error-handling
Nov 8, 2024
Merged

bug: Improve handling exceptions from unsupported PHPUnit versions#1521
acoulton merged 2 commits intoBehat:masterfrom
acoulton:bug-phpunit-error-handling

Conversation

@acoulton
Copy link
Copy Markdown
Contributor

@acoulton acoulton commented Nov 7, 2024

This is an alternative fix for #1421, based partly on the useful work done by and discussion with @uuf6429 in #1426.

Attempting to render PHPUnit assertion failures is brittle, because:

a) we (intentionally) don't impose any PHPUnit version constraints in composer.json and
b) PHPUnit's code for rendering exceptions is not covered by their BC promise.

I've added tests that cover both the absence of PHPUnit classes, and the presence of a PHPUnit class that works differently to how we expect, causing an error. These demonstrate the current total failure and early termination of Behat in both these scenarios (and show that throwing our own exception here will behave the same, just with a different message).

The second commit updates the PHPUnitExceptionStringer to ensure that we always render something useful for any past or future PHPUnit version. This prioritises communicating that an assertion has failed and in many cases will still have as much detail as usual (see e.g. the examples of the failing int comparison in the feature file).

@acoulton
Copy link
Copy Markdown
Contributor Author

acoulton commented Nov 7, 2024

As the github action logs will eventually be removed, just to record that the previous output (for the entire behat command) was:

With a theoretically-supported PHPUnit that causes errors during stringification

In IncompatibleThrowableToStringMapper.php line 9:

  Some internal problem


behat [-s|--suite SUITE] [-f|--format FORMAT] [-o|--out OUT] [--format-settings FORMAT-SETTINGS] [--init] [--lang LANG] [--name NAME] [--tags TAGS] [--role ROLE] [--story-syntax] [-d|--definitions DEFINITIONS] [--snippets-for [SNIPPETS-FOR]] [--snippets-type SNIPPETS-TYPE] [--append-snippets] [--no-snippets] [--strict] [--order ORDER] [--rerun] [--rerun-only] [--stop-on-failure] [--dry-run] [--] [<paths>]

F

With unsupported PHPUnit

F
Fatal error: Uncaught Error: Class 'PHPUnit_Framework_TestFailure' not found in src/Behat/Testwork/Exception/Stringer/PHPUnitExceptionStringer.php:43
Stack trace:
#0 src/Behat/Testwork/Exception/ExceptionPresenter.php(93): Behat\Testwork\Exception\Stringer\PHPUnitExceptionStringer->stringException(Object(PHPUnit\Framework\ExpectationFailedException), 1)
#1 src/Behat/Behat/Output/Node/EventListener/Statistics/StepStatsListener.php(151): Behat\Testwork\Exception\ExceptionPresenter->presentException(Object(PHPUnit\Framework\ExpectationFailedException))
#2 src/Behat/Behat/Output/Node/EventListener/Statistics/StepStatsListener.php(79): Behat\Behat\Output\Node\EventListener\Statistics\StepStatsListener->captureStepStatsOnAfterEvent(Object(Behat\Behat\EventDispatcher\Event\AfterStepTested))
#3 src/Behat/Testwork/Output/Node/EventListener/ChainEventListener.php(47): Behat\Behat\Output\Node\EventListener\Statistics\StepStatsListener->listenEvent(Object(Behat\Testwork\Output\NodeEventListeningFormatter), Object(Behat\Behat\EventDispatcher\Event\AfterStepTested), 'tester.step_tes...')
#4 src/Behat/Testwork/Output/NodeEventListeningFormatter.php(90): Behat\Testwork\Output\Node\EventListener\ChainEventListener->listenEvent(Object(Behat\Testwork\Output\NodeEventListeningFormatter), Object(Behat\Behat\EventDispatcher\Event\AfterStepTested), 'tester.step_tes...')
#5 vendor/symfony/event-dispatcher/EventDispatcher.php(230): Behat\Testwork\Output\NodeEventListeningFormatter->listenEvent(Object(Behat\Behat\EventDispatcher\Event\AfterStepTested), 'tester.step_tes...', Object(Behat\Testwork\EventDispatcher\TestworkEventDispatcher))
#6 vendor/symfony/event-dispatcher/EventDispatcher.php(59): Symfony\Component\EventDispatcher\EventDispatcher->callListeners(Array, 'tester.step_tes...', Object(Behat\Behat\EventDispatcher\Event\AfterStepTested))

[... stack frames 7 to 31 removed for simplicity...]

#32 bin/behat(34): Symfony\Component\Console\Application->run()
#33 {main}
  thrown in src/Behat/Testwork/Exception/Stringer/PHPUnitExceptionStringer.php on line 43

Note that in the second case the ~36 line exception and stack trace is rendered multiple times - once for each failing scenario.

Copy link
Copy Markdown
Contributor

@carlos-granados carlos-granados left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to do a deep read to get to the bottom of this, looks like a good solution overall, I just added a couple of small comments

Attempting to render PHPUnit assertion failures is brittle, because
a) we (intentionally) don't impose any PHPUnit version constraints
in composer.json and b) PHPUnit's code for rendering exceptions is
not covered by their BC promise.

This update to the PHPUnitExceptionStringer ensures that we always
render something useful - prioritising communicating that an
assertion has failed - even if we don't correctly support the
PHPUnit version in use.
@acoulton acoulton force-pushed the bug-phpunit-error-handling branch from 2b6c86f to 2c7d664 Compare November 8, 2024 09:24
Copy link
Copy Markdown
Contributor

@carlos-granados carlos-granados left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks all good. Would still like @uuf6429 to check this as well

@uuf6429
Copy link
Copy Markdown
Contributor

uuf6429 commented Nov 8, 2024

Looks very good overall @acoulton!

static::assertClassNotLoaded(\PHPUnit\Framework\TestFailure::class);

if (PHP_VERSION_ID < 80400) {
// Trigger loading array_find from symfony/polyfill-php84 before our autoloader tries to use it
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no function autoloading in PHP, so this is totally useless. The function polyfills are loaded eagerly at the time you load the vendor/autoload.php file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof the functions are autoloaded but until you call them they don't try to load the Php84 class that the polyfill actually uses. I know because without this the autoloader failed with an error due to not autoloading the symfony class.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof I was on the move earlier, here is how symfony/polyfill implements array_find:

if (!function_exists('array_find')) {
    function array_find(array $array, callable $callback) { return p\Php84::array_find($array, $callback); }
}

As you can see, if this is the first Php84 polyfill that has been called, it will trigger an autoload of the Php84 class. Not normally an issue, but if you're using array_find inside an autoloader that can be a problem as indeed it did here.

FWIW, it may be just a language thing, or that I am tired after a long week, but I read your comment as quite rude. From my perspective it didn't feel great to have you engage with a PR that has involved a fair bit of effort and thought, and that fixes a longstanding issue, and your only comment is to describe a tiny part of it as "totally useless".

I flag this less for my own sake, more that if we want to get behat moving again I hope we can all try to make contributors feel welcome and valued.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants