Problem/Motivation
We can't write tests which involve ExtensionDiscovery because we can't inject vfsStream objects.
Drupal\Core\Extension\ExtensionDiscovery allows you to inject a Drupal root directory and a site path directory when you create it.
However, when you perform a scan(), it uses a static array to cache the results it finds, and then never manages the cache.
This means that if you make another ExtensionDiscovery object with a new root argument but the same site path, the scan will never be performed and the existing cache will be returned to you.
Furthermore, ExtensionDiscovery will also ignore the provided site path if the kernel is present and discoverable under \Drupal.
Proposed resolution
Add a new key to ExtensionDiscovery's cache, which reflects the root directory used for the scan.
Remaining tasks
Follow-ups:
- #2729711: Improve test coverage for ExtensionDiscovery
More test coverage for details of ExtensionDiscovery behavior: Precedence of extensions with same name, the $include_tests parameter, different profile directories, different site paths, different root paths, side effects of subsequent calls, identity of cached extension objects, behavior in a functional Drupal environment (kernel test). See the patch in #87, which was discarded for this issue but may live on in another issue.. ExtensionDiscoveryignores the injected site path if the kernel is present and the container has thesite.pathservice parameter, so: Change this behavior so that the injected site path is always used, and modify callers to always inject the site path.- Clean up
scanDirectory()'s use of the root path. - Create a way for
ExtensionDiscoveryto automatically filter based on the current profile.
User interface changes
API changes
Data model changes
| Comment | File | Size | Author |
|---|---|---|---|
| #95 | 2605654-79.patch | 7.49 KB | donquixote |
Comments
Comment #2
mile23This patch does some coding standards stuff alongside adding a
static::$cacheRootproperty which can store the last root path for comparison.This passes unit tests but let's see whether the testbot complains.
Comment #3
dawehnerComment #4
mile23Here's a test.
See also: #2301873: Use ExtensionDiscovery to register test namespaces for running PHPUnit where we're trying to use
ExtensionDiscoveryto discover PHPUnit tests.Comment #6
mile23Low-impact bugfix changes with added tests.
Comment #7
mile23Still applies, test still passes.
Comment #8
dawehnerWe IMHO use this codestyle everywhere, and well, its also an out of scope change.
Comment #9
mile23That's a coding standards change that doesn't change line numbers. (Adds a space between ? and :)
Comment #10
dawehnerYou can't argue against those numbers:
Comment #11
mile23Hmm. I must have been using an old version of code sniffer standard, because it told me we needed that space. Now it says it shouldn't be there.
Other minor CS fixes in that file.
Comment #12
mile23Comment #14
dawehnerThank you
Comment #16
xjmThere are a lot of out-of-scope docfixes in this patch. Let's move those into a separate issue.
@effulgentsia and I discussed this and agreed that as a straightforward bugfix with no disruption this could go into a patch release. If that's not the case or if there is a strong reason this change needs to go in before RC, please update the summary with that information. Thanks!
Comment #17
mile23There are two potentially out of scope doc changes in this issue, and they are in a file we already touch and don't alter line counts.
The rest of the changes document the correct behavior.
So here's a patch which re-introduces the coding standards errors which are out-of-scope changes.
Note that if this patch passes tests, it's highly likely that the patch in #11, with all the changes, would also be suitable for commit.
Comment #18
mile23Still passes. :-)
Comment #19
mile23Still applies. Running the testbot.
Comment #20
mile23Moving this to major because it's blocking a fix which will enable better unit tests: #2605664: [Needs change record updates] Align TestDiscovery and bootstrap.php's non-permissive loading of PSR-4 namespaces for traits
Comment #21
dawehnerWell, xjm asked for removing those changes, if I understood her correctly
For some reason I hate this super strict tests.
Comment #22
mile23#21.1: You mean the documentation of the code I was working on?
#21.2: What reason do you have for hating the test that proves that a) the code doesn't work before you change it and b) that it works after you change it?
Comment #23
mile23If anyone has a specific problem with this, let me know what I can do, specifically, to change it.
The docblocks come from researching the problem and writing down what I learned, instead of not writing it down.
The tests are.... tests. :-)
Thanks.
Comment #24
dawehnerYeah, well, I just try to write down how I understand #16.
Well, there is this promise that tests make rewriting your code easier. Your super strict tests, like checking that specific code is called exactly N times, maybe even in which
order, doesn't help about it. Just rising a point here, not a reason to block this patch, IMHO.
Comment #25
mile23tl:dr: The test that exists in #17 is the absolute simplest test of the thing I am fixing without re-writing whole other sections of
ExtensionDiscovery.Why might the test be the way it is?
Because in order to test it another way, we have to break outside the scope of this issue and start fixing other bugs, which is a total pain because the whole setup is so fragile.
Why is it fragile? Because no one wrote tests. Or documentation. See #17.
Allow me to illustrate the problem, which is out of scope for this issue, which I'll file an issue about as soon as this class is testable, which it currently isn't because it does not invalidate the cache.
ExtensionDiscovery::scan()figures out all the cool places where extensions might be spending their Christmas vacation. Here's one of them:See how it's an empty string? That empty string gets used as an array key during discovery. Here's what it ends up looking like in a dump of the cache generated during a test:
See how the only extension discovered is an array item with NO KEY OR INDEX? That's because
ExtensionDiscoveryuses the key from$searchdirsas the path and the cache key. And it's an empty string.Which means you can't access it, even during a test.
This could be worked around by omitting the site path during
__construct(), ifExtensionDiscoverydidn't have a dependency onDRUPAL_ROOT, which has to then either be faked or otherwise defined, and ifExtensionDiscoveryactually used the cache in a way that was productive.So:
The test that exists in #17 is the absolute simplest test of the thing I am fixing without re-writing whole other sections of
ExtensionDiscovery.It will enable me (and others if they feel like it) to fix other things, including the rest of the code in
ExtensionDiscovery. See: #2605664-56: [Needs change record updates] Align TestDiscovery and bootstrap.php's non-permissive loading of PSR-4 namespaces for traits for instance. I can't test the improvements to test discovery because of this issue.Maybe some day when
ExtensionDiscoveryisn't brittle as all get-out, we'll be able to gut that test. That'd be awesome, because it is, in fact, annoying to read.Comment #26
mile23So here's the exact same patch as #17 except without the troublesome documentation improvements.
Comment #27
dawehnerSorry for being annoying.
Comment #30
mile23S'ok. I'm being annoying, too.
I wonder why the testbot passes but d.o thinks it failed.
Comment #32
dawehnerTestbot was a bit borked yesterday
Comment #33
mile23Still waiting on this so I can move forward on #2605664: [Needs change record updates] Align TestDiscovery and bootstrap.php's non-permissive loading of PSR-4 namespaces for traits which will enable #2102651: Port file_example module to Drupal 8
Comment #35
mile23Testbot broke in mid-test... Re-running the test and setting back to RTBC.
Comment #39
mile23Not sure why the testbot is unhappy. Setting back to RTBC... I'm sure the testbot will opine.
Comment #40
alexpottThis seems dangerous to me because if you did...
very weird things will happen.
It'd be better to create a cache key to use as a top level key in static $files array. Or try to do away with the static.
Comment #41
mile23Music to my ears.
Comment #42
mile23So if we're messing with the caching expectation, we have to shave a few yaks:
#2186491-17: [meta] D8 Extension System: Discovery/Listing/Info
#2208429: Extension System, Part III: ExtensionList, ModuleExtensionList and ProfileExtensionList
#2503927: Convert ExtensionDiscovery to a service
Comment #43
mile23Alternately, if we add the root directory as a top-level key as suggested by @alexpott in #40, it all works at the expense of being an infinitely large memory hole.
The practical considerations here aren't so bad because we'll mostly just use this to scan one filesystem. Also, in those other issues, it's all set to be re-arranged anyway. Thus we can apply a band-aid here, make the class testable, allow for tests of test discovery in #2301873: Use ExtensionDiscovery to register test namespaces for running PHPUnit and #2605664: [Needs change record updates] Align TestDiscovery and bootstrap.php's non-permissive loading of PSR-4 namespaces for traits and then go drink a beer.
Comment #44
dawehnerI agree, the new approach to key by the root itself, is better.
Comment #45
mile23Running the test again because it's a little stale...
Comment #46
catchThis looks out of scope - what breaks if this hunk is reverted?
Comment #47
catchComment #48
mile23profileDirectorieswill be NULL if it isn't initialized, since it's set by the constructor that defaults it to NULL.!isset()works, too, since it's NULL.I can't remember why I changed it, so clearly it was earth-shatteringly important.
Here we are without that change.
Note that this patch looks like it applies to 8.0.x, 8.1.x, and 8.2.x.
Comment #49
catchThanks. Looks fine otherwise so back to RTBC pending test runs.
Comment #50
alexpottNote that it is perfectly possible to unit test this class by resetting the static using reflection which is exactly what is done in the latest tests in #2329453: Ignore front end vendor folders to improve directory search performance.
Comment #51
mile23Yah but wouldn't it be super awesome to not have to? Like in #2605664-56: [Needs change record updates] Align TestDiscovery and bootstrap.php's non-permissive loading of PSR-4 namespaces for traits maybe.
Comment #52
alexpottYes but this patch only makes it testable if the app root changes - you'd still need to use reflection to clear the static cache if the root doesn't change. Imo making this testable would be better achieved by making the class use StaticStorage once #2260187: [PP-1] Deprecate drupal_static() and drupal_static_reset() lands.
That said adding the app route to static cache key does actually make sense it is just that the issue title is a tiny bit misleading.
Comment #53
alexpottTesting number of calls is always a bit fishy. How about just asserting that is or is not called as expected.
The cache is not invalidated.
Comment #54
mile23+1 on StaticStorage.
::twiddles thumbs while it gets committed::
I suppose this issue should be 8.2.x as well.
Comment #55
alexpottGiven the latest comments on #2260187: [PP-1] Deprecate drupal_static() and drupal_static_reset() I think we should proceed here to add the root directory to key - it makes sense - we just need to address #53 and then I think we're good to go.
I think we can fix this in 8.1.x - it is a bug. And the change is totally BC as it is a very internal change. I don't think this is a major bug though.
Comment #56
donquixote commentedWhy not add a static reset method?
Of course, the additional index
[$this->root]is the right thing to do.I don't really understand what the test is testing, though.
Why can't we, now that the
[$this->root]key is added, write a real tests with a bunch of virtual module files and then verify the result of ->scan() ? Maybe a naive question, sorry :)Comment #57
dawehnerI agree totally with this point. This test feels really not flexible.
Comment #58
mile23Not naive... Kind of the point of the exercise. :-)
Comment #59
donquixote commentedOk. Then why start with these "crazy" tests?
Why not something like here?
I used a swappable scan component for this, but I think the equivalent should be possible with vfsStream.
Or this one, which here tests the real existing files, but could be changed to use vfsStream.
Btw, linking this issue: #2724231: Replacement for ExtensionDiscovery, following SOLID principles
Comment #60
mile23See #25, and the conversation about StaticStorage mentioned in #55 where I'm twiddling my thumbs.
'Crazy' is what being a Drupal core dev is all about. You hold back the nausea as you to make a patch that will be in a scope the maintainers can review, while solving the specific problem you're trying to solve. And then someone says your code is 'crazy' because they forgot what kind of cultural shifts have occurred in Drupal since January. Fragile code leads to fragile tests.
Here's the simplified test, NOW THAT WE'VE DECIDED TO USE THE ROOT AS KEY. :-)
Comment #61
donquixote commentedI don't see you on IRC..
Would you say that a test as proposed in #59 is out of scope?
Imo, if we make the class testable and craft a meaningful test scenario, this does fit within one issue.
Since January? I don't know what you are talking about, I must have missed it :)
I can assure you the "crazy" is not personal. But I don't want to derail the issue. IRC or PM.
My concern is that we are starting with stuff that depends on reflection and/or advanced mocking functionality (hence "crazy"), when instead we could start with a test for the public methods.
And if you want to test the side effect prior to the added
[$this->root]key, what about this:Comment #62
mile23I mean: Before this patch, you couldn't test *other* things that rely on
ExtensionDiscoverywith vfsStream without managing the static. Which would result in 'crazy' tests, resulting inNO_RTBC_MODE. See #51, and the irony. :-)For instance, I was going to write better tests for my patch in #2301873: Use ExtensionDiscovery to register test namespaces for running PHPUnit but I couldn't because of the yak before us here which needs shaving.
AFTER this patch, you will be able to use vfsStream to mock filesystems without reflection, and not care what ExtensionDiscovery is doing.
See #25, as mentioned above.
Test suggestion in #61 is a good idea. Now I'll make the test more complicated to do it. :-)
Comment #64
mile23OK. Added a more black-box-ish test to the scan() method.
This illustrated a dependency between
scan()anddrupal_valid_test_ua(), which meant turning our humble unit test into a KernelTest.Then the tests kept failing on the 'sites/default' testcase. Note that 'sites/default' is not a default search path for
ExtensionDiscovery; it's always provided either by the caller,DrupalKernel::findSitePath(), or\Drupal::service('site.path').Those failures lead to the discovery that
ExtensionDiscoveryignores the$sites_pathif there is a kernel. In that case, it uses\Drupal::service('site.path')instead. During testing, 'site.path' is set to simpletest/[test_id], which doesn't help us at all.Using the service parameter is the exact opposite of what we're trying to accomplish here. So I took out that logic. Installer and simpletest group tests pass locally but now it's time to see what the testbot has to say about this change. Hope status: minimal. But we'll see.
And this, my friends, is why Tests Are Good.
Comment #65
mile23Hah. Forgot the patch.
Comment #66
mile23Comment #68
dawehnerJust some quick feedback.
Mh, when we change this, we should IMHO inject the site path into the ExtensionDiscovery at least for the common usecases, otherwise it will do more now.
Can't you use assertAttribute or so?
Comment #69
donquixote commentedLook at the unit test. Works for me, locally at least.
One thing to fix was to prepend $this->root when doing file_exists(), so we don't depend on include path.
I have not thought much about this..
Generally it seems an injected site path should have precedence. But I don't know what else depends on this misbehavior.
Either way, I think the unit test would work either way, because there is no Kernel.
EDIT: @Mile23 I removed your kernel test simply so testbot can focus on my unit test, and because it seemed your test failed last time.
This does not mean that I want your test out. I don't have a clear opinion on it whatsoever atm.
EDIT: "PHP 5.5 & MySQL 5.5 CI error" -> no idea what this means or what to do about it.
Comment #71
almaudoh commentedThe last time this happened to me, the unit test didn't have a
@groupanotation. Looks like this could be the issue here.Comment #72
donquixote commentedEDIT: Thanks @almaudoh!
Comment #73
donquixote commentedComment #75
donquixote commentedComment #76
donquixote commentedComment #77
mile23From the interdiff:
Right, that's pretty much the essence of the problem. If you don't have a Kernel present, then yay everything works. But if you do, then it ignores your injected site path, which means we can't test with vfsStream unless the kernel is also injected with vfsStream (which it currently can't do, AFAICT).
If we're going to always honor the injected site path (and we should), then the solution, as @dawehner points out, is to find all the places where callers need to inject it.
Based on the test fails in #65, those places seem to be somewhere at an intersection between WebTestBase and the locale module.
Comment #78
donquixote commentedOnly since the patch above.
Before, even this did not work, thanks to the dependence on php include path.
The patch in #75 fixes two bugs and introduces a test that is meaningful and was previously not possible.
Imo, this would already justify a commit. After some cleanup, obviously.
But I don't mind if we want to go the next step and also fix the Kernel / site dir issue. I am looking into it as we speak.
Comment #79
dawehnerI really like this test. Its way less bound to the internal implementation.
Just some cleanup we should do, but I couldn't be bother with posting a review. There is nothing to disagree with this changes, IMHO.
Comment #80
mile23If you move the same test to be subclass
KernelTestBase, you end up with this:So, no, in that circumstance the patch doesn't allow you to inject a filesystem unless the kernel is booted from the same injected filesystem, because:
ExtensionDiscoveryignores the site path if the kernel is present.\Drupalis an anti-pattern. :-)But since we've accomplished the goal of allowing
$rootas a cache key for some use-cases without an ill-effect anywhere else, let's call this done and then start working on the injection part in a follow-up.Comment #81
donquixote commentedI am still working on the next version of this patch, with more coverage of the weird edge cases.
Let's keep this as "Needs work" until then.
We can still decide to move some of this to a new issue. But give me a few hours or a day and then we talk :)
Comment #82
mile23Assign yourself if you'd like.
Comment #83
donquixote commentedI made another observation, which we should think about for a bit:
All the search directories are relative to Drupal root. But the test site directory is a stream url, e.g. 'vfs://root/sites/simpletest/739048'.
If we prepend the root path (fake or not) to this site path, we get a broken path.
This applies to this proposed change:
But also to existing code here:
The effect is that in current code, extensions in the test site directory will not be found, because the is_dir() returns FALSE.
I am going to add a kernel test to document this behavior. So that in the follow-up, we have to change this kernel test. This will illustrate how we are changing the behavior.
Absolutely.
True even without the backslash? (running for cover)
Comment #84
mile23vfsStream works with is_dir() because is_dir() works with streams.
So $this->root would be vfs://whatever/ and then the path would be vfs://whatever/path, and is_dir() and file_exists() and all of them know how that works.
Here's the test in vfsStream: https://github.com/mikey179/vfsStream/blob/master/src/test/php/org/bovig...
Comment #85
donquixote commentedIn a kernel test, the root path would be
'/path/to/drupal'.The check would be
is_dir('/path/to/drupal/vfs://root/sites/simpletest/12345'), so it would fail.The only thing we could inject to make this work is an empty root path.
But this virtual site directory does usually not contain any modules anyway.
Comment #86
donquixote commentedBtw the correct way to fix the site path lookup would be like this:
In your version, you skip the
\Drupal::hasService().I will not fix this part here, but instead provide a test that documents that the existing behavior ignores the injected site path.
Comment #87
donquixote commentedMy IDE always tells me to use static::assert*() here. The methods are indeed static in PhpUnit.
I don't know what is better. Do we have a policy about this?
What's the deal with protected everywhere? People (neither core nor contrib) are not supposed to inherit from this, and we don't want to promise that it won't change.
Thank you very much :)
--------------
New patch still contains the static::
But in the end I don't care that much.
New patch contains a lot more test method which cover noticeable behavior of the ExtensionDiscovery class.
We can decide to move some of this to a separate issue. But imo we should simply commit it now.
Comment #88
donquixote commentedI forgot to mention, I added this:
and the respective test:
I don't know if we want to throw this exception, and if doing so is within scope.
I also don't know if we want this kind of test.
I think it is useful to see in the testing code if we intentionally change behavior.
Comment #89
mile23I think that if you make a change you should cover it with a test.
I'm also sure the goal here is to have
ExtensionDiscoverydo its work without any dependencies, includingbootstrap.incwhich is wheredrupal_get_profile()lives. Having the class explode if it's used without an include file is regression.So let's just go with #79, please, because it satisfies the scope of this issue and is reviewable. And so we can then a) have a baseline with tests, and b) move forward on other ideas in other issues.
In a follow-up, we can figure out how to get around the kernel-sets-site-path issue and others.
Updated issue summary with things we've learned.
Comment #90
mile23Added #2725839: Remove Drupal\Core\Extension\ExtensionDiscovery::getInfoParser() since it is dead code
Comment #91
donquixote commentedI am fine with this.
I disagree with some of your other ideas.
This is only for the sake of discussion, and should not stop the patch going forward.
I think when you introduce new functionality, you should add new tests to cover it, and to detect when the behavior changes.
When you change functionality, there should already be an existing test which expresses the old behavior, and which you then change to reflect the new behavior. Or you say "oops", if you notice that the change is not intended.
The tests I proposed here should have been written when ExtensionDiscovery were first introduced. But they were not. So we either add them here, or in a follow-up.
The proposed tests are still incomplete. There are still cases that are "legitimate use", but that are not covered by tests. So yes, a follow-up is the place.
The exception is thrown in a case where we previously had a fatal error. So not really a regression.
But it is a change which maybe we don't want. Because if we do this, we would also have to do it in other places. So I can live with not doing it.
The benefit of an exception is that it can be tested. But it is not super-important.
It is not the goal of this issue, and was apparently not the goal when ExtensionDiscovery was introduced.
The goal here (and/or in a possible follow-up) is to reflect the existing behavior with tests, even if it is awkward.
If someone makes ExtensionDiscovery work for this scenario outside of a functioning Drupal environment, the test will fail and will need to be changed. The commit will then illustrate that this problem was fixed.
Comment #92
mile23OK, so based on this tenuous three-way consensus, I'm going to mark this RTBC so we can get some guidance or maybe a commit. :-)
This is RTBC for #79, which gives us an extra cache array key based on the root directory, fixes a bug in generating the Extension object based on the root directory, and then tests that the whole thing works as expected.
Expected followups are listed in the issue summary.
Comment #95
donquixote commentedRe-posting #79 by @dawehner.
Comment #96
donquixote commentedSetting to RTBC via #92 (Mile23).
Comment #97
almaudoh commentedComment #98
donquixote commentedComment #99
alexpottCommitted #79 97d1fae and pushed to 8.1.x and 8.2.x. Thanks!
Comment #102
donquixote commentedComment #103
donquixote commented