Problem/Motivation
\Drupal\KernelTests\KernelTestBase does not respect the site's file_scan_ignore_directories setting when booting Drupal kernel in KernelTestBase::bootKernel().
This can create infinite recursion problem when running kernel tests on some code-base setups using symlinks.
A more detailed description of such a setup can be found on the Drupal Component Scaffold README.md.
Proposed solution
Directories to be ignored are listed in a new ENV variable to be set in phpunit.xml.dist, in a comma-separated format, like:
<php>
<!-- Set error reporting to E_ALL. -->
<ini name="error_reporting" value="32767"/>
<!-- Do not limit the amount of memory tests take to run. -->
<ini name="memory_limit" value="-1"/>
...
<!-- Example SIMPLETEST_IGNORE_DIRECTORIES value: build,dev_module -->
<env name="SIMPLETEST_IGNORE_DIRECTORIES" value=""/>
When executing tests the specified directories will be excluded when collecting tests and extensions.
Remaining tasks
- Submit patch: Done.
User interface changes
None.
API changes
None.
Data model changes
None.
Comments
Comment #2
ademarco commentedComment #3
bircherSetting to "needs review" to run the tests.
This is a pretty clean solution for the bug.
Comment #4
ademarco commentedComment #5
bircherNice, this looks good to me.
Comment #6
polIt's also working for me, thanks !
Comment #7
ademarco commentedWe actually found out that the patch solves the problem only partially: it is not working when using
bootstrap="tests/bootstrap.php"in phpunit.xml as ignored directories are not passed along indrupal_phpunit_find_extension_directories()and neither can they as we obviously don't have Drupal bootstrapped intests/bootstrap.php.We could maybe have a global variable set in phpunit.xml which lists directories to be ignored as a string to be exploded on commas or something, although I kind of don't like it that much.
Marking as "Needs work".
Comment #8
ademarco commentedAt the end I've actually opted for an ENV variable set in phpunit.xml.dist: it's much cleaner plus we don't necessarily have a settings.php available when running tests.
Comment #9
ademarco commentedComment #10
ademarco commentedComment #11
ademarco commentedComment #12
pfrenssenThis still needs tests.
Comment #13
pfrenssenI created a test for this but I cannot replicate the problem locally. I purposefully introduced a circular dependency in a module but this seems to be dealt with inside PHP. It appears to stop recursing after reaching a certain depth. Memory consumption stays within reasonable limits (4MB). The test takes a lot longer to complete though. Note that I only ran
KernelTestBaseTest, it is possible that some other test might fail due to this circular dependency.Comment #15
pfrenssenSeems the circular references have consequences in other places too :D
It's probably not the best idea to introduce a circular reference inside Drupal core. There might always be some kind of tool used in a deployment procedure that cannot handle them correctly. We should probably generate it during the test, and remove it again in the teardown.
Comment #16
pfrenssenSo this issue is a feature request that proposes to add the possibility to filter out extension directories during tests. I think the sole reason to ever want to actually filter out directories is to avoid infinite recursion caused by circular references. This is also the reason mentioned in the issue description.
I think we can approach this from a completely different angle. If we can somehow avoid to follow circular dependencies then this doesn't need manual blacklisting of folders that contain (or are referenced by) the circular symlinks. That means this can become a bugfix.
I've attached an initial idea I had to filter out the circular dependencies by filtering them using a
RecursiveFilterIterator. This doesn't work, it is too late in the process, the infinite recursion will still take place and will then be infinitely filtered by this class. In reality (like I mentioned in my previous comment) on my system this recurses quite deep but then stops at some point. There is no infinite recursion taking place. I don't know why but it might be due to running PHP 7.2 or having xdebug enabled.So instead of filtering we can try to extend the
RecursiveDirectoryIteratordirectly and catch the recursion as it takes place.Comment #17
pfrenssenThis approach seems to work. It obviously needs to be cleaned up. No interdiff since this is a new approach.
@ademarco, @bircher, @pol would this approach be OK to solve the problem without having to manually blacklist paths?
If yes, should we repurpose this issue as a bug report, or create a new issue?
Comment #18
pfrenssenCleaning round.
Comment #21
pfrenssenOK this needs some work on the tests. I am not sure how we can test this effectively. My best guess is to create the symlink during the test setup and then remove it again on teardown.
I was intrigued that the infinite recursion by following the circular symlinks was interrupted on my system after reaching a certain depth. It appears this is a protection built in to the linux kernel itself. It has a hardcoded limit of 40 levels after which it won't go any deeper. Ref. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/...
Comment #22
vdacosta@voidtek.com commentedUpdating the patch.
Comment #23
vdacosta@voidtek.com commentedUpdating the second patch.
Comment #25
dxvargas commentedWe have two very different approaches to fix this issue.
@ademarco:
Patch: https://www.drupal.org/files/issues/2018-07-05/2943172-kernel-test-base-...
It tackles the issue by ensuring the 'file_scan_ignore_directories' setting is obeyed.
It's a direct approach to the issue and it goes in the right direction because this setting should be held in consideration when running tests.
We have a test for 'file_scan_ignore_directories' in core/tests/Drupal/KernelTests/Core/File/ScanDirectoryTest.php but it's only testing mocked settings, we could improve this test.
@pfrenssen
Patch: https://www.drupal.org/files/issues/2018-07-05/2943172-22.patch
It ensures that we avoid infinite loops by using an alternative to the PHP native class 'RecursiveDirectoryIterator'. This has some drawbacks in my opinion:
My preference goes to @ademarco's approach. It's necessary anyway, it's simplest and easier to test.
Comment #26
pfrenssenBTW I noticed that my approach is not 100% reliable. I would like to believe the approach is sound but unfortunately I had some cases were the recursion still happened on different environments. I am not sure what causes it. I had a look at how this is implemented in PHP and it appears there has been quite some churn and bug fixing going on in the iterators in the recent history (ref. https://github.com/php/php-src/commits/master/ext/spl/spl_iterators.c) so it is possible that this works differently depending on the PHP version.
I have no more time to look into this so feel free to ignore my patch and continue with the approach proposed by @ademarco.
Comment #27
pfrenssenComment #28
dxvargas commentedI'm now uploading a patch that includes a test in KernelTestBaseTest, covering the setting "SIMPLETEST_IGNORE_DIRECTORIES".
Comment #29
pfrenssenThis will need to be against 8.7.x since this is a feature request. Assigning to me for review. I'm assuming it is OK to take over the assigned status from @ademarco since he hasn't been active here for half a year :)
Comment #30
pfrenssenThis doesn't seem to be the right location to set this setting. This method is supposed to be only returning the extensions for modules. Or it would have been called
getExtensionsForModulesAndApplyIgnoreDirectoriesSetting().I would move this to the start of the
bootKernel()method where the other settings are being applied. We can also use the built in method::setSetting().I think it is OK to test it like this, since the
getExtensionsForModules()method is responsible for retrieving the extensions. I also think it is OK to make this private method public for testing purposes since this is a very practical way to validate that the problem is fixed without having to refactorKernelTestBase.However I have a few remarks about the test:
@covers ::getExtensionsForModules()since this test does not in fact provide full coverage for the functionality offered by that method.setUp(). This would require moving this to a dedicated class though.file_scan_ignore_directoriesworks. Now it only tests the environment variable.This is using the environment variable directly instead of reading
file_scan_ignore_directories. The environment variable is nice to have, but the main problem as described in the issue summary is that thefile_scan_ignore_directoriessetting is ignored. It still seems to be ignored.Comment #31
pfrenssenCommenting on my own comment. After giving it some more thought, I am thinking that the right place to read the environment variable and adding it to `file_scan_ignore_directories` is probably in the test bootstrap, before
drupal_phpunit_populate_class_loader()is called. This will ensure that this setting will be respected as early as possible.Comment #32
dxvargas commented@pfrenssen thanks for your comments with which I mostly agree.
But I still have some doubts to put here, about third point in #30 and #31.
In
/core/tests/bootstrap.phpwe're not usingDrupal\Core\Site\Settingsyet. We're still in an early stage of the all process. While I don't see any major problem about starting the Settings here, it seems a big change. Do we really want to go on with that?!Anyway,
drupal_phpunit_find_extension_directoriesis using the PHP methodRecursiveDirectoryIteratorso we would still need to configure the filter as it's being done now, the only change would be to read the configuration from the Settings instead of the getenv(), it's a theoretic nuance with no practical impacts.I would still stick to the approach that we have. You say:
Well the main problem is that we can't ignore some directories. While
file_scan_ignore_directoriesis a useful setting we can use inKernelTestBase::bootKernel(), in other places we can still simply use the newSIMPLETEST_IGNORE_DIRECTORIESsetting.SIMPLETEST_IGNORE_DIRECTORIESdoesn't need to be only used once to setfile_scan_ignore_directories. This doesn't go against the vision we had since first moment.Of course, it would be perfect to have
file_scan_ignore_directoriesset with the value inSIMPLETEST_IGNORE_DIRECTORIESand just use that everywhere. But defining Settings in bootstrap.php seems a big price for this.Comment #33
dxvargas commentedI'm now submitting a patch that covers remarks 1 and 2 in #30 from @pfrenssen.
Comment #34
pfrenssenI don't think the current approach is ideal. The goal of this issue is to make sure the
file_scan_ignore_directoriessetting is respected when running tests. The latest patch doesn't fully achieve this, and tries to work around it by introducing a second setting inphpunit.xml. This means we will have two different entry points for ignoring paths. And since one of them is only half working, this will force people to copy any settings they have forfile_scan_ignore_directoriesintoSIMPLETEST_IGNORE_DIRECTORIES.I think the right solution for this problem is to forget about
SIMPLETEST_IGNORE_DIRECTORIEScompletely and just make sure it works withfile_scan_ignore_directoriesinsettings.php. If this requires to refactor the test bootstrap so that the discovery of files is done after the settings are initialized, then this is probably the right path to take.Comment #36
mxr576#34 +1, but in addition, it would be great if ignored paths still could be manipulated with an environment variable as well. It could be super useful in Docker based setups.
Comment #37
mxr576Can't we just retrieve the list of paths that should be excluded from .gitignore files? We would need a single source of truth and for me, it seems reasonable to re-use .gitignore for this.
drupal/core's .gitignore already excludes
node_moduleshttps://github.com/drupal/core/blob/8.7.5/.gitignore#L3 although it does not excludebower_componentsthat the default.settings.php does https://github.com/drupal/drupal/blob/8.7.5/sites/default/default.settin...My idea is to merge drupal/core's .gitignore with the root project's .gitignore to get a list of directories that should be excluded both from test discovery and file scanning.
Comment #38
mxr576... and there is another place where the list of excluded directories has to be kept in sync manually, the phpcs.xml.dist: https://github.com/drupal/core/blob/8.7.5/phpcs.xml.dist#L12-L16
Comment #39
mxr576PHPCS does not support yet to dynamically register excluded paths, but it could be added quite easily, see: https://github.com/squizlabs/PHP_CodeSniffer/issues/2242
Comment #40
mxr576Meh, almost forget about that project-level .gitignores usually excludes contrib modules/themes, so we cannot use the blacklist from .gitignore because contrib modules would be discovered by neither test nor extension discovery in that case.
Comment #41
mxr576POC patch that bootstraps Drupal for PHPUnit to retrieve the list of
file_scan_ignore_directoriesand exclude them from discovery. Feedbacks are warmly welcomed, probably the current solution could be still optimized.I did not use the test from #33 because as I saw
drupal_phpunit_find_extension_directories()do not get called for kernel tests bydrupal_phpunit_populate_class_loader(). So this solution still requires a test coverage.Comment #42
mxr576Comment #43
mxr576Removed composer.lock changes from #42.
Comment #44
mxr5768.7.x backport
Comment #45
mxr576Okay, so I had a realization that you can only add directory names to
file_scan_ignore_directories, you cannot add relative paths or path fragments because this is what\Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIteratoraccepts.I also bumped into #3050881 that suggests to use
RecursiveExtensionFilterIteratorto discover tests files in the bootstrap file. Well, the problem with using just theRecursiveExtensionFilterIteratoris that it also ignores settings infile_scan_ignore_directories. Although, if we would use the\Drupal\Core\Extension\ExtensionDiscoveryto discover directories that could combine the best from both solutions.Here is a WIP patch for 8.7.x branch that boots Drupal and uses
ExtensionDiscoveryto discover tests files. Even if it works we have to figure out how to deprecate some functions properly. Let's see what fails...Comment #46
mxr576Okay, next try.
Comment #47
mxr576Failed tests:
I had to remove an additional include from the related test file for this. The new bootstrap file already includes bootstrap.inc.
Well, I have checked git history and I think this assert can be removed because if we remove it the test case will still remain valid.
and
These were the most interesting ones. The problem caused by that
drupal_get_user_timezone()did not check whether the container was actually initialized before it tried to retrieve the current user or the config.Comment #48
mxr576Well, I guess this has not been caused by my changes.
Comment #49
mxr576Added deprecation notices since #47, also here is a 8.8.x patch as well.
Comment #50
mxr576Comment #51
mxr576Again, it could happen that the container is not initialized when a static service gets used...
Comment #52
mxr576Okay, so for both versions
./scripts/test-site.phpfailed becauseDrupal\Core\Session\AccountInterfacehas not been found. This could have been fixed by addingrequire_once __DIR__ . '/../../autoload.php';to the file. I saw you made an attempt to avoid this in #2926633 but I could not find a better solution. Open for ideas.For 8.8.x, the
DrupalGetProfileLegacyTestfailed because anotherinclude $this->root . '/core/includes/bootstrap.inc';had to be removed from the test.interdiff_49_52.diff contains the Drupal 8.7.x changes the interdiff_50_52.diff contains the Drupal 8.8.x changes.
Comment #53
mxr576One small tweak that is required to solve the actual task we are trying to solve here.
Comment #54
larowlanthese indicate an issue in the test, we shouldn't be changing the runtime code for tests, in these cases typically we setup a mock container in the test
I don't think we should be doing this, this pollutes the global scope during bootstrap. Those functions are loaded into memory and can't be unloaded.
same, I don't think we should have settings populated in unit tests, this is the wrong approach in my opinion
I think this suffers from the same problem @dawehner highlighted in #3050881: Use RecursiveExtensionFilterIterator in drupal_phpunit_find_extension_directories for performance - we're using code under test to drive the tests, which could lead to issues
In my opinion, the solution for your custom projects here is to write your own bootstrap.php, we do the same at PreviousNext, e.g. ours includes elements from #3050881: Use RecursiveExtensionFilterIterator in drupal_phpunit_find_extension_directories for performance but also completely skips scanning contrib and core tests, because we only want to run project-specific tests. You can easily swap this in by using your own phpunit.xml and changing the bootsrap entry.
I'll ping @dawehner and @lendude (phpunit initiative maintainers) for a review.
Comment #55
lendudeI agree with @larowlan that doesn't feel like the right solution for core. If the changes in #53 are indeed what is needed to support this feature, then they feel too invasive and we shouldn't do it. Like @larowlan and @ademarco pointed out, there is an option to get this to work using a custom
bootstrap.php, which is admittedly less obvious then using an existing setting, but perfectly workable I think (so this is really a DX feature).Also, are we really sure we want to re-use an existing setting? Does 'file_scan_ignore_directories' always match the pattern we want to use here? The current use case is to ignore dirs where we look for modules, do we really want to add 'and where we look for tests' to that? Sure, there might be a lot of overlap, but is this always 100%?
The current proposed fix also doesn't line up with the IS, so that needs an update (I spent some time looking for where the env var was being used, before figuring out it isn't used).
Also, this changes a lot of tests, but doesn't add any coverage.
Comment #56
mxr576Thanks for the feedbacks and reviews, I share some of your concerns and I'll provide detailed answers to those tomorrow. I'll also try to update the description to describe in a better way the core problem that we are trying to solve here.
Even if the
ExtensionDiscovery+file_scan_ignore_directoriesapproach will not be the one that solves this problem or Drupal core won't support this development model we need a solution now and I see we are not the only ones, so I am leaving this updated patch here for now.Comment #60
ieguskiza commentedFollowing the work on #33, would it not be possible to set file_scan_ignore_directories during environment setup? I attach a patch and interdiff with an example
Comment #61
anmolgoyal74 commentedFixed CS issues.