Problem/Motivation
Probably something has changed recently either in PHPUnit or somewhere else and we started seeing different issues related to serialization in tests. This particular issue was reported by @quietone in #3192893: [META] Serialization issues in Migration tests, which is now a meta task.
The issue is the following:
If the test is failing with exception, then PHP is trying to serialize a stack trace, which contains functions, methods, classes that have been executed. One of these functions is recognized as Closure (in other words it's anonymous function) in the stack trace (see the screenshot).

This happens because we have array_walk() inside array_walk(), so the deepest one is executed inside the closure.
PHPUnit has a related issue. In that issue it is said that the solution is to not call assertions in a closre. Later comments give examples of the problem which do not include closures and finally a fix is provided by mpyw.
Steps to reproduce
Change a query in any source plugin and run the relevant migration test.
For example,
In d6\Term.php change ->orderBy('td.tid'); to ->orderBy('td.foo');
Then run the d6 term test.
phpunit -c core --debug -v --colors=always core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTaxonomyTermTest.php
You'll see something like:
Drupal\Tests\taxonomy\Kernel\Migrate\d7\MigrateTaxonomyTermTest::testTaxonomyTerms
PHPUnit\Framework\Exception: PHP Fatal error: Uncaught Exception: Serialization of 'Closure' is not allowed in Standard input code:80
Stack trace:
#0 Standard input code(80): serialize(Array)
#1 Standard input code(112): __phpunit_run_isolated_test()
#2 {main}
thrown in Standard input code on line 80
Fatal error: Uncaught Exception: Serialization of 'Closure' is not allowed in Standard input code:80
Stack trace:
#0 Standard input code(80): serialize(Array)
#1 Standard input code(112): __phpunit_run_isolated_test()
#2 {main}
thrown in Standard input code on line 80
/var/www/html/vendor/phpunit/phpunit/src/Util/PHP/AbstractPhpProcess.php:254
/var/www/html/vendor/phpunit/phpunit/src/Util/PHP/AbstractPhpProcess.php:171
/var/www/html/vendor/phpunit/phpunit/src/Framework/TestSuite.php:601
/var/www/html/vendor/phpunit/phpunit/src/TextUI/TestRunner.php:633
/var/www/html/vendor/phpunit/phpunit/src/TextUI/Command.php:204
/var/www/html/vendor/phpunit/phpunit/src/TextUI/Command.php:163Proposed resolution
Fix upstream, https://github.com/sebastianbergmann/comparator/pull/47#issuecomment-117...
See #53 for details.
Workaround
composer require mpyw/phpunit-patch-serializable-comparison
Remaining tasks
Create a PR at https://github.com/sebastianbergmann/comparator/pull/47#issuecomment-117...
https://github.com/sebastianbergmann/comparator/pull/106
| Comment | File | Size | Author |
|---|---|---|---|
| #58 | 3197324-58-stop_migration_message_asserts_triggering_serialization_errors.patch | 1.44 KB | wim leers |
| #48 | 3197324-48.patch | 4.27 KB | quietone |
| #45 | 3197324-45.patch | 894 bytes | danflanagan8 |
| #39 | 3197324-39-fail.patch | 5.68 KB | quietone |
| #39 | 3197324-39-fail-closure.patch | 1.38 KB | quietone |
Comments
Comment #2
matroskeenComment #3
matroskeen3197324-3-error_before.patchshows how the test is failing at the moment;3197324-3-error_after.patchshows how the test will fail with applied3197324-3.patch;3197324-3.patchshould be considered as "Needs review";Comment #6
quietone commentedThe three patches makes it really easy to see what is happening, thanks for that!
Most of the test results for patch, 3197324-3-error_after.patch, are back to being helpful. Yay! However, the Kernel Migration tests however are not. They are rather brief and give no clue that the error is in the query.
I don't know, maybe the other array_walk in executeMigration needs to be changed as well? Or is this error for another issue?
Comment #7
matroskeenAfter some debugging, it seems the issue is different.
Now the reason is not serialization, but unserialization :)
As we already know, when the exception is thrown, it's getting serialized.
But, later on it's getting unserialized in this line:
vendor/phpunit/phpunit/src/Util/PHP/AbstractPhpProcess.php(to be determined: why it happens only for kernel tests)
When this line is executed, the query object is trying to unserialize but fails to retrieve the connection in
__wakeup:I'm still not sure what the fix should be. Will try to take a look again later this week.
Comment #8
wim leersRan into this too. Root cause: PHPUnit's code — see https://github.com/sebastianbergmann/phpunit/issues/4371.
I was seeing
with #3 applied, I'm instead seeing
Comment #9
wim leersI can confirm this. This is the kind of stack trace you get:
The root cause of the problem here is that this call stack I've quoted is a PHPUnit call stack. IOW: this is the PHPUnit process.
But
\Drupal\Tests\migrate\Kernel\MigrateTestBase::createMigrationConnection()generates amigrateDB connection on the fly. So I bet the PHPUnit process's unserialization simply is failing because that PHP process does not know about this dynamically constructed DB connection. The stack trace for that function call:Dug deeper and it appears to be even worse:
This PHPUnit process does not know about any DB connection.
Comment #10
wim leersI think this is happening because
\Drupal\KernelTests\KernelTestBase::$runTestInSeparateProcess(That explains
__phpunit_run_isolated_test()— I think)Comment #12
matroskeenI noticed many references to this problem in other issues and decided to give it another try.
Comment #14
huzookaImho the root issue is that exceptions thrown in migration kernel tests with a SQL connection aren't serializable.
This is a bit different approach than in #12, but I hope you all will love the test 😊.
Self-review:
MigrateExecutable::import()is wrapped into a try catch...... and then
MigrateTestBase::handleExceptiontries to serialize the$exception.If serialization fails, then we try to throw a serializable exception, without the actual trace – but we pass the trace as string to the exception's message.
But if the exception is serializable, then we just re-throw it.
Comment #16
huzookaI have a much simpler and cleaner solution
Comment #17
huzookaComment #18
huzookaSelf-review:
Yes, this seems to be as easy to solve as you see. Using
$this->fail()instead of comparing the catched migration message's#type.I had to catch
\Throwablebecause\Exceptiondoes not apply on\PHPUnit\Framework\ExpectationFailedException.This is basically the same as in #14.
Comment #19
matroskeenThat version looks very promising because @quietone mentioned at some point that it started to happen after changing some PHPUnit asserts.
Looking forward to seeing the test results!
Comment #20
huzooka@Matroskeen, which version?
Comment #21
matroskeenI meant the last patch, added in #18.
Comment #22
huzookaFixed a comment in test.
Comment #23
huzookaComment #24
huzookaNever touch any files after
commit-code-check.shsucceeds.Comment #25
huzookaComment #28
huzookaComment #29
quietone commentedNice to see work here. I applied the #25 and then made an error in \Drupal\Tests\migrate_drupal\Kernl\d7\FieldDiscoveryTest::addAllFieldProcessesAltersData, run the test and got the serialization error.
PHPUnit\Framework\Exception: PHP Fatal error: Uncaught LogicException: The database connection is not serializable. This probably means you are serializing an object that has an indirect reference to the database connection. Adjust your code so that is not necessary. Alternatively, look at DependencySerializationTrait as a temporary solution. in /var/www/html/core/lib/Drupal/Core/Database/Connection.php:1933Comment #30
huzooka@quietone, that means that we cannot continue with the simple fix I guess 🙁
Comment #31
damienmckennaRunning into this with Commerce Migrate tests (#3228297: Update D7 fixtures using updated db-tools command in 9.3) :-\
Comment #32
damienmckennaIn June the following fix was made available: https://github.com/mpyw/phpunit-patch-serializable-comparison
Should we include that as a dev dependency in core?
Comment #33
damienmckennaFWIW the mpyw/phpunit-patch-serializable-comparison library helped solve the problem for Commerce Migrate, with it in place I was able to see that there was a failed test due to a missing dependency.
Comment #34
quietone commentedIf it helps, the error referred to in #33 occurred in \Drupal\migrate\MigrateExecutable::import at
Comment #35
mikelutzComment #37
quietone commentedStill happening and still an absolute pain to deal with.
Comment #38
damienmckennaThe problem showed up in Metatag in #3252159: Fix tests compatibility with Drupal 9.3.x and again was resolved by using the Composer dependency.
So here are patches for 9.3.x and 8.4.x to add mpyw/phpunit-patch-serializable-comparison as a dev dependency.
Comment #39
quietone commentedCreated two patches which will hopefully prove that the patch in #38 will allow a helpful error message to display instead of the about 'closure'. Both patch have the same forced failure in two test files. One patch,3197324-39-fail-closure.patch, has only the changes to the tests and should result in the useless error message. The other patch include the patch from #38 and should result in an error message that is helpful.
Comment #41
quietone commentedThe tests are failing as expected. If you search the results 3197324-39-fail.patch for 'closure' it is not present, unlike for the other patch.
I'll try the patch in #38, the next time I get the 'Exception trace cannot be serialized because of closure' error.
Comment #42
spokjeI like it, but seeing it adds a new core dependency, I suppose we need to do the Core Dependency Criteria Dance?
Comment #43
huzookaThe simplest possible solution is what Sebastian Bergman said: don't call assertion in closures.
Comment #44
quietone commentedYes, he did say that and closed the issue. However, there have been additional comments providing examples of the problem which do not include closures. A later comment in that issue is where mpyw gives a link to a patch they wrote, which is the solution suggested by DamienMcKenna in #38.
Comment #45
danflanagan8Edit: After posting I saw that @matroskeen posted this same fix in #3 (and in #5 on the parent issue). I've tried to read the early part of this issue more closely and I don't really understand why we moved away from the simplest approach. Maybe we can reassess based on the latest comments. Now back to my original comment...
With this in mind, I changed the outer
array_walkto aforeachand that fixes the issue locally for me. There's no interdiff because this is a new approach. It's super simple and introduces no new dependencies.Following the steps to reproduce (mussing the source Term source plugin) without this patch, I get
But after applying my patch I get a flurry of fatal errors, but they are drupal errors you might expect if your source plugin is hosed.
Comment #46
quietone commentedI am not convinced that is sufficient.
With the patch in #45 and a forced failure in \Drupal\Tests\migrate_drupal\Kernel\d7\FieldDiscoveryTest::testAddAllFieldProcessesAlters the error is
With the patch in #38 the error is:
I much prefer the error report using #38. It is showing the exact error I made.
And here is the a patch with the forced test failure.
Comment #47
danflanagan8Thanks, @quietone!
That is an excellent example of where there appears to be no "simple" solution. I thought maybe the culprit was the
sortcalls within$this->assertSame(sort($expected_process_keys), sort($actual));, which is an unusual pattern, but refactoring that did not change anything.I'm sold on the need for a robust solution like the dependency being discussed. I'm going slowly back from this issue now...
:)
Comment #48
quietone commentedRan into this again today.
I went to use drush on 10.0.x and that fails, Drush 11 cannot be composer required/updated after Drupal's bump to Symfony 6, nor does other versions of drush. Migrate Tool isn't compatible either so I went back to just using the migration kernel tests and found myself back here. (Not a great start to the day).
I am raising this to a Major because it is a significant developer-facing bug with no workaround. One could argue the workaround is to use a debugger but that is difficult because the error message doesn't give any clue in finding where the bug is. It is all trial and error.
Uploading a patch for 10.0.x but not running tests.
Comment #49
baysaa commentedI faced this recently and spent a good hour or so trying to debug & search for a fix until I landed on this issue. +1 to #48
Comment #50
mikelutzI'm curious where we are with this one. the patch in #48 doesn't have any description or interdiff, but I'm trying to piece through the work here.
Comment #51
quietone commentedRan into this again today and came here to get the package name to require.
I am not sure what description you are looking for. The goal here is to add a dev dependency on mpyw/phpunit-patch-serializable-comparison so that we get the useful error messages from migration. So, today I was getting
Fatal error: Uncaught Exception: Serialization of 'Closure' i ...instead of helpful RequirementsException message.Comment #52
spokjeThe patch in #48 needs a reroll.
Besides that, if we decide to go this way, which I myself think is a good idea, we would need an entry for the new dev-dependency here: https://www.drupal.org/about/core/policies/core-dependency-policies/depe...
I think we need some core committer/release manager/people with Big Brains approval on it first? Otherwise we would advertise an entry for a not yet added dependency?
*drops EUR 0.02*
Comment #53
mikelutzSorry, @quietone, somehow I missed the last line in #48 saying this was a 10.x patch. I must not have had my coffee yet.
Thankfully @quietone is a provisional release manager, so if we do need to add that dependency, I'm sure she can work through the necessary approvals.
That said, We really shouldn't actually add this package to core. It's a unmaintained one commit hack to work around a bug in https://github.com/sebastianbergmann/comparator. There is a 4 year old issue open in the upstream here https://github.com/sebastianbergmann/comparator/pull/47. The PR there is outdated, it implements Serializable and overrides the serialize() and unserialize() methods, which I believe throws a deprecation in php8.1. It needs to be updated to just use __serialize and __unserialize introduced in php 7.4. The pr hasn't had any maintainer response, despite the project being maintained. The best solution here is to push for a fix upstream. I've added a comment to that PR, to see if we have any chance. Otherwise, it's going to be tough to implement this override in core in a reasonable way. the mpyw package uses composer to autoload his copy of SebastianBergmann\Comparator\ComparisonFailure first, so the autoloader never gets the real copy, but that loads the file whether it's needed or not, and leaves things prone to incompatibility issues if the upstream changes that file, so it's not really a good solution for core. Short of forking the whole comparator library (also far from ideal, for obvious reasons) I think it's going to be pretty tricky, although perhaps @alexpott or @catch have some other idea that I'm not thinking of.
The best solution is to get this fixed upstream, if we possibly can. The existing PR has no issue associated with it, perhaps we could start with creating an issue and an updated PR and see where we can get.
Comment #54
mikelutzWe did get a commitment from the maintainer to at least review a new patch if we create one. https://github.com/sebastianbergmann/comparator/pull/47#issuecomment-117...
Comment #55
quietone commented@mikelutz, thanks for moving this along!
I've updated the proposed resolution and remaining tasks based on #53.
Comment #56
spokjeTried to get the ball roling in https://github.com/sebastianbergmann/comparator/pull/106
Comment #57
donquixote commentedFor the record, I created a solution as part of
https://git.drupalcode.org/project/drupal/-/commit/4cffbd777c827b7fa9b00...
There is a trait that does this:
Yes, the correct thing would be for PhpUnit to handle serialization of exception backtraces.
It already does that in most of its exception classes, just not in ComparisonFailure.
And btw, the problem is not so much in throwing or asserting from within a closure, but having a closure as an argument in one of the calls in the trace. For some reason, the closure in the 'function' part is fine.
Being able to use closures more freely in tests opens some interesting and fun possibilities.
I am not opposed to the "mpyw/phpunit-patch-serializable-comparison" as in the patch here.
The exception cleaner could be more powerful in cleaning up other exceptions that are not covered by that package.
But actually in my experiments, other exceptions typically don't cause the same problems.
I think PhpUnit wraps them in its own exception classes before they would get serialized.
ComparisonFailure is special because it is stored in ExpectationFailedException->comparisonFailure.
Comment #58
wim leersI ran into this again at #3364108: Configuration schema & required keys. This has cost me dozens of hours of wasted time over the past ~3 years 🙈
So, time to fix it forever.
— https://www.drupal.org/pift-ci-job/2752966
To my surprise, it also reveals some rather big problems in the existing migration tests:
🫣
Comment #59
quietone commentedI too would love this fixed!
The before and after results in #57 are for two different tests, are you sure the patch is a fix?
I went and tried this with my goto fail for this and I didn't get any change. :-(. I made an error in the data provider for \Drupal\Tests\migrate_drupal\Kernel\d7\FieldDiscoveryTest::testAddAllFieldProcessesAlters and ran the test with and without #58.
The result before and after is that same:
What test produced the output with 'comment is not enabled in the source site'?
Comment #60
wim leersI had just mixed up the D6 & D7
MigrateFileConfigsTest, but their results are pretty much identical — still, fixed my comment!Yes I'm sure 🤓
Right, that must have a different root cause then. The root cause for that test must be somewhere else than in
MigrateTestBase's asserting the absence of a migration error message.#58 only fixes the case of
$this->assertEquals('status', $type, $message);causing this to happen.See the test results for #58 😊
Comment #61
quietone commented@Wim Leers, thanks for working on this!
It was late last night when I looked at this. I thought I should look again.
The failing tests in #58 are tests that are testing for specific errors in the migrate messages. So, to assert that there are no migrate messages immediately after executing a migration and before the assertions in the test is breaking those tests.
I also am not keen on changing the behavior of
collectMessages. One could argue that messages should be collected for each Migrate Kernel test so that assertions can be made on the count or the message text. However, that is a significant amount of work for the approximately 230 Migration Kernel tests. Fortunately, we have the test run in #58 to show that only 3 tests have migrate error messages. And those 3 are supposed to have errors, I think we have good evidence that collecting messages during Kernel tests isn't needed. That is good news!