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..
  • ExtensionDiscovery ignores the injected site path if the kernel is present and the container has the site.path service 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 ExtensionDiscovery to automatically filter based on the current profile.

User interface changes

API changes

Data model changes

Comments

Mile23 created an issue. See original summary.

mile23’s picture

Issue summary: View changes
Status: Active » Needs review
StatusFileSize
new3.65 KB

This patch does some coding standards stuff alongside adding a static::$cacheRoot property which can store the last root path for comparison.

This passes unit tests but let's see whether the testbot complains.

dawehner’s picture

Issue tags: +Needs tests
mile23’s picture

Title: Drupal\Core\Extension\ExtensionDiscovery partially ignores site_path argument, other stuff too. » Drupal\Core\Extension\ExtensionDiscovery never invalidates its file cache
Issue tags: -Needs tests
StatusFileSize
new4.74 KB
new8.39 KB

Here's a test.

See also: #2301873: Use ExtensionDiscovery to register test namespaces for running PHPUnit where we're trying to use ExtensionDiscovery to discover PHPUnit tests.

The last submitted patch, 4: 2605654_4_test_only.patch, failed testing.

mile23’s picture

Issue tags: +rc eligible

Low-impact bugfix changes with added tests.

mile23’s picture

Still applies, test still passes.

dawehner’s picture

Issue tags: -rc eligible

Low-impact bugfix changes with added tests.

This tag is for non-invasive patches which solely make coding standards, testing, and docs improvements.

+++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
@@ -187,7 +205,7 @@ public function scan($type, $include_tests = NULL) {
-      $searchdirs[static::ORIGIN_SITE] = $this->sitePath ?: DrupalKernel::findSitePath(Request::createFromGlobals());
+      $searchdirs[static::ORIGIN_SITE] = $this->sitePath ? : DrupalKernel::findSitePath(Request::createFromGlobals());

We IMHO use this codestyle everywhere, and well, its also an out of scope change.

mile23’s picture

Issue tags: +rc target triage

That's a coding standards change that doesn't change line numbers. (Adds a space between ? and :)

dawehner’s picture

Status: Needs review » Needs work

You can't argue against those numbers:

~/w/d/core (8.0.x) $ ag "\? \:" | wc -l
       4
~/w/d/core (8.0.x) $ ag "\?\:" | wc -l
     414
mile23’s picture

StatusFileSize
new8.32 KB
new1.83 KB

Hmm. 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.

mile23’s picture

Status: Needs work » Needs review

The last submitted patch, 4: 2605654_4_test_only.patch, failed testing.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Thank you

The last submitted patch, 4: 2605654_4_test_only.patch, failed testing.

xjm’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: -rc target triage

There 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!

mile23’s picture

Status: Needs work » Needs review
StatusFileSize
new7.57 KB
new1.02 KB

There 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.

mile23’s picture

Still passes. :-)

mile23’s picture

Still applies. Running the testbot.

mile23’s picture

Priority: Normal » Major
dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -103,17 +110,27 @@ class ExtensionDiscovery {
    -   *   Whether file cache should be used.
    -   * @param string[] $profile_directories
    -   *   The available profile directories
    -   * @param string $site_path
    -   *   The path to the site.
    +   *   (optional) Whether file cache should be used. Defaults to TRUE.
    +   * @param string[]|null $profile_directories
    +   *   (optional) The available profile directories. Defaults to NULL, which
    +   *   will cause this object to search for profile directories based on
    +   *   settings.
    +   * @param string|null $site_path
    +   *   (optional) The site path within the Drupal installation. Defaults to
    +   *   NULL, which will cause this object to determine the site based on the
    +   *   request information.
        */
    

    Well, xjm asked for removing those changes, if I understood her correctly

  2. +++ b/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryTest.php
    @@ -0,0 +1,133 @@
    +    $mock_extension_discovery->expects($this->exactly(4))
    +      ->method('scanDirectory')
    +      ->willReturn($expected);
    +    // Use up our 4 calls to scanDirectory().
    ...
    +    // The scanDirectory() method gets called 4 times while traversing the file
    +    // system once, one each for ORIGIN_CORE, ORIGIN_SITE_ALL, etc.
    +    $test1_mock_extension_discovery->expects($this->exactly(4))
    +      ->method('scanDirectory')
    +      ->willReturn($expected);
    +
    +    // Use up our 4 calls to scanDirectory().
    +    $test1_mock_extension_discovery->scan('module', FALSE);
    

    For some reason I hate this super strict tests.

mile23’s picture

#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?

mile23’s picture

Issue summary: View changes

If 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.

dawehner’s picture

#21.1: You mean the documentation of the code I was working on?

Yeah, well, I just try to write down how I understand #16.

#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?

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.

mile23’s picture

tl: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:

$searchdirs[static::ORIGIN_ROOT] = '';

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:

Array
(
    [core] => Array
        (
            [0] => Array
                (
                )

        )

    [sites/all] => Array
        (
            [0] => Array
                (
                )

        )

    [] => Array
        (
            [0] => Array
                (
                    [module] => Array
                        (
                            [vfs://test_a/modules/module_a/module_a.info.yml] => Drupal\Core\Extension\Extension Object
                                (
                                    [type:protected] => module
                                    [pathname:protected] => modules/module_a/module_a.info.yml
                                    [filename:protected] => 
                                    [splFileInfo:protected] => 
                                    [root:protected] => vfs://test_a
                                    [subpath] => modules/module_a
                                    [origin] => 
                                )

                        )

                )

        )

    [sites/default] => Array
        (
            [0] => Array
                (
                )

        )

)

See how the only extension discovered is an array item with NO KEY OR INDEX? That's because ExtensionDiscovery uses the key from $searchdirs as 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(), if ExtensionDiscovery didn't have a dependency on DRUPAL_ROOT, which has to then either be faked or otherwise defined, and if ExtensionDiscovery actually 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 ExtensionDiscovery isn'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.

mile23’s picture

StatusFileSize
new6.72 KB
new1.22 KB

So here's the exact same patch as #17 except without the troublesome documentation improvements.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Sorry for being annoying.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 26: 2605654_26.patch, failed testing.

The last submitted patch, 26: 2605654_26.patch, failed testing.

mile23’s picture

Status: Needs work » Needs review

S'ok. I'm being annoying, too.

I wonder why the testbot passes but d.o thinks it failed.

Status: Needs review » Needs work

The last submitted patch, 26: 2605654_26.patch, failed testing.

dawehner’s picture

Status: Needs work » Reviewed & tested by the community

Testbot was a bit borked yesterday

mile23’s picture

Title: Drupal\Core\Extension\ExtensionDiscovery never invalidates its file cache » Make Drupal\Core\Extension\ExtensionDiscovery testable

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 26: 2605654_26.patch, failed testing.

mile23’s picture

Status: Needs work » Reviewed & tested by the community

Testbot broke in mid-test... Re-running the test and setting back to RTBC.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 26: 2605654_26.patch, failed testing.

The last submitted patch, 26: 2605654_26.patch, failed testing.

The last submitted patch, 26: 2605654_26.patch, failed testing.

mile23’s picture

Status: Needs work » Reviewed & tested by the community

Not sure why the testbot is unhappy. Setting back to RTBC... I'm sure the testbot will opine.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

This seems dangerous to me because if you did...

$a = new ExtensionDiscovery('root/a');
$b = new ExtensionDiscovery('root/b');

$a ->scan('module');
$b->scan('module');

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.

mile23’s picture

do away with the static.

Music to my ears.

mile23’s picture

mile23’s picture

Status: Needs work » Needs review
StatusFileSize
new6.76 KB
new2 KB

Alternately, 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.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

I agree, the new approach to key by the root itself, is better.

mile23’s picture

Running the test again because it's a little stale...

catch’s picture

+++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
@@ -165,8 +165,9 @@ public function __construct($root, $use_file_cache = TRUE, $profile_directories
+    if (($this->profileDirectories === NULL) && $type != 'profile') {

This looks out of scope - what breaks if this hunk is reverted?

catch’s picture

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

StatusFileSize
new6.08 KB
new971 bytes

profileDirectories will 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.

catch’s picture

Status: Needs review » Reviewed & tested by the community

Thanks. Looks fine otherwise so back to RTBC pending test runs.

alexpott’s picture

Note 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.

mile23’s picture

alexpott’s picture

Yes 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.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryTest.php
    @@ -0,0 +1,133 @@
    +    // The scanDirectory() method gets called 4 times while traversing the file
    +    // system once, one each for ORIGIN_CORE, ORIGIN_SITE_ALL, etc.
    +    $mock_extension_discovery->expects($this->exactly(4))
    

    Testing number of calls is always a bit fishy. How about just asserting that is or is not called as expected.

  2. +++ b/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryTest.php
    @@ -0,0 +1,133 @@
    +    // Now use test2 in order to check that the cache was invalidated.
    

    The cache is not invalidated.

mile23’s picture

Version: 8.0.x-dev » 8.2.x-dev

+1 on StaticStorage.

::twiddles thumbs while it gets committed::

I suppose this issue should be 8.2.x as well.

alexpott’s picture

Version: 8.2.x-dev » 8.1.x-dev
Priority: Major » Normal

Given 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.

donquixote’s picture

Note that it is perfectly possible to unit test this class by resetting the static using reflection

Why not add a static reset method?

  /**
   * Resets the static cache.
   */
  static function staticReset() {
    self::$files = [];
  }

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 :)

dawehner’s picture

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 :)

I agree totally with this point. This test feels really not flexible.

mile23’s picture

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 :)

Not naive... Kind of the point of the exercise. :-)

donquixote’s picture

Not naive... Kind of the point of the exercise. :-)

Ok. 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

mile23’s picture

Title: Make Drupal\Core\Extension\ExtensionDiscovery testable » Make Drupal\Core\Extension\ExtensionDiscovery more testable
Status: Needs work » Needs review
StatusFileSize
new4.97 KB
new6.74 KB

Ok. Then why start with these "crazy" tests?

See #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. :-)

donquixote’s picture

I don't see you on IRC..

'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.

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.

what kind of cultural shifts have occurred in Drupal since January

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:

$discovery_a = new ExtensionDiscovery($root_a);
$extensions_a = $discovery_a->scan('profile');

$discovery_b = new ExtensionDiscovery($root_b);
$extensions_b = $discovery_b->scan('profile');

static::assertNotSame($extensions_a['minimal'], $extensions_b['minimal']);
mile23’s picture

Assigned: Unassigned » mile23
Status: Needs review » Needs work

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.

I mean: Before this patch, you couldn't test *other* things that rely on ExtensionDiscovery with vfsStream without managing the static. Which would result in 'crazy' tests, resulting in NO_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.

My concern is that we are starting with stuff that depends on reflection and/or advanced mocking functionality (hence "crazy")

See #25, as mentioned above.

Test suggestion in #61 is a good idea. Now I'll make the test more complicated to do it. :-)

The last submitted patch, 60: 2605654_60.patch, failed testing.

mile23’s picture

Status: Needs work » Needs review

OK. Added a more black-box-ish test to the scan() method.

This illustrated a dependency between scan() and drupal_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 ExtensionDiscovery ignores the $sites_path if 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.

mile23’s picture

StatusFileSize
new8.99 KB
new11.56 KB

Hah. Forgot the patch.

mile23’s picture

Title: Make Drupal\Core\Extension\ExtensionDiscovery more testable » Modify Drupal\Core\Extension\ExtensionDiscovery to allow for scanning multiple file systems, enabling vfsStream testing
Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 65: 2605654_64.patch, failed testing.

dawehner’s picture

Just some quick feedback.

  1. +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -188,14 +188,8 @@ public function scan($type, $include_tests = NULL) {
    -    // at install time. Therefore Kernel service is not always available, but is
    -    // preferred.
    -    if (\Drupal::hasService('kernel')) {
    -      $searchdirs[static::ORIGIN_SITE] = \Drupal::service('site.path');
    -    }
    -    else {
    -      $searchdirs[static::ORIGIN_SITE] = $this->sitePath ?: DrupalKernel::findSitePath(Request::createFromGlobals());
    -    }
    +    // at install time.
    +    $searchdirs[static::ORIGIN_SITE] = $this->sitePath ?: DrupalKernel::findSitePath(Request::createFromGlobals());
    

    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.

  2. +++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionDiscoveryTest.php
    @@ -0,0 +1,187 @@
    +
    +    // We need to assert the root path of the extension, but it does not have
    +    // an accessor.
    +    $ref_root = new \ReflectionProperty($module, 'root');
    +    $ref_root->setAccessible(TRUE);
    +    $this->assertEquals(vfsStream::url('root'), $ref_root->getValue($module));
    ...
    +  /**
    +   * Helper method to get the cache array from an ExtensionDiscovery object.
    +   *
    +   * @param ExtensionDiscovery $extension_discovery
    +   *   An ExtensionDiscovery object.
    +   *
    +   * @return array[]
    +   *   The files array.
    +   */
    +  protected function getDiscoveredCache(ExtensionDiscovery $extension_discovery) {
    +    $ref_cache = new \ReflectionProperty(ExtensionDiscovery::class, 'files');
    +    $ref_cache->setAccessible(TRUE);
    +    return $ref_cache->getValue($extension_discovery);
    +  }
    

    Can't you use assertAttribute or so?

donquixote’s picture

Status: Needs work » Needs review
StatusFileSize
new8.58 KB
new13.43 KB

This illustrated a dependency between scan() and drupal_valid_test_ua(), which meant turning our humble unit test into a KernelTest.

Look 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.

Those failures lead to the discovery that ExtensionDiscovery ignores the $sites_path if 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.

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.

Status: Needs review » Needs work

The last submitted patch, 69: D8-2605654-69-ExtensionDiscoveryTest.patch, failed testing.

almaudoh’s picture

EDIT: "PHP 5.5 & MySQL 5.5 CI error" -> no idea what this means or what to do about it.

The last time this happened to me, the unit test didn't have a @group anotation. Looks like this could be the issue here.

donquixote’s picture

EDIT: Thanks @almaudoh!

donquixote’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 72: D8-2605654-72-ExtensionDiscoveryTest.patch, failed testing.

donquixote’s picture

donquixote’s picture

Status: Needs work » Needs review
mile23’s picture

Status: Needs review » Needs work

From the interdiff:

+++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
@@ -189,8 +189,14 @@ public function scan($type, $include_tests = NULL) {
-    // at install time.
-    $searchdirs[static::ORIGIN_SITE] = $this->sitePath ?: DrupalKernel::findSitePath(Request::createFromGlobals());
+    // at install time. Therefore Kernel service is not always available, but is
+    // preferred.
+    if (\Drupal::hasService('kernel')) {
+      $searchdirs[static::ORIGIN_SITE] = \Drupal::service('site.path');
+    }

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.

donquixote’s picture

If you don't have a Kernel present, then yay everything works.

Only 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.

dawehner’s picture

StatusFileSize
new7.49 KB
new4.42 KB

I 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.

mile23’s picture

Status: Needs work » Reviewed & tested by the community

If you move the same test to be subclass KernelTestBase, you end up with this:

$ SIMPLETEST_DB=mysql://root:root@localhost:8889/d8 ./vendor/bin/phpunit -c core/ --filter ExtensionDiscoveryTest
PHPUnit 4.8.11 by Sebastian Bergmann and contributors.

F

Time: 27.29 seconds, Memory: 203.50Mb

There was 1 failure:

1) Drupal\KernelTests\Core\Extension\ExtensionDiscoveryTest::testExtensionDiscoveryVfs
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
     'profile' => Array (
         'standard' => 'core/profiles/standard/standa...fo.yml'
-        'minimal' => 'sites/default/profiles/minimal/minimal.info.yml'
+        'minimal' => 'core/profiles/minimal/minimal.info.yml'
         'myprofile' => 'profiles/myprofile/myprofile.info.yml'
         'otherprofile' => 'profiles/otherprofile/otherpr...fo.yml'
     )
     'module' => Array (...)
     'theme' => Array (
-        'seven' => 'sites/default/themes/seven/seven.info.yml'
+        'seven' => 'core/themes/seven/seven.info.yml'
         'poorly_placed_theme' => 'modules/poorly_placed_theme/p...fo.yml'
     )
     'theme_engine' => Array (...)
 )

/Users/paul/pj2/drupal/core/tests/Drupal/KernelTests/KernelTestBase.php:1214
/Users/paul/pj2/drupal/core/tests/Drupal/KernelTests/Core/Extension/ExtensionDiscoveryTest.php:54

FAILURES!
Tests: 1, Assertions: 3, Failures: 1.

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:

  1. ExtensionDiscovery ignores the site path if the kernel is present.
  2. \Drupal is an anti-pattern. :-)

But since we've accomplished the goal of allowing $root as 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.

donquixote’s picture

Status: Reviewed & tested by the community » Needs work

I 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 :)

mile23’s picture

Assigned: mile23 » Unassigned

Assign yourself if you'd like.

donquixote’s picture

Assigned: Unassigned » donquixote

I 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:

@@ -478,7 +478,7 @@ protected function scanDirectory($dir, $include_tests) {
       else {
         $filename = $name . '.' . $type;
       }
-      if (!file_exists(dirname($pathname) . '/' . $filename)) {
+      if (!file_exists($this->root . '/' . dirname($pathname) . '/' . $filename)) {
         $filename = NULL;
       }

But also to existing code here:

  protected function scanDirectory($dir, $include_tests) {
    [..]
    $absolute_dir = ($dir == '' ? $this->root : $this->root . "/$dir");

    if (!is_dir($absolute_dir)) {
      return $files;
    }

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.

\Drupal is an anti-pattern.

Absolutely.

True even without the backslash? (running for cover)

mile23’s picture

vfsStream 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...

donquixote’s picture

In 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.

donquixote’s picture

Btw the correct way to fix the site path lookup would be like this:

    if ($this->sitePath) {
      $searchdirs[static::ORIGIN_SITE] = $this->sitePath;
    }
    elseif (\Drupal::hasService('kernel')) {
      $searchdirs[static::ORIGIN_SITE] = \Drupal::service('site.path');
    }
    else {
      $searchdirs[static::ORIGIN_SITE] = DrupalKernel::findSitePath(Request::createFromGlobals());
    }

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.

donquixote’s picture

Status: Needs work » Needs review
StatusFileSize
new24.75 KB
new29.21 KB
-    static::assertFileExists($root . '/core/modules/system/system.module');
-    static::assertFileExists($root . '/core/modules/system/system.info.yml');
+    $this->assertFileExists($root . '/core/modules/system/system.module');
+    $this->assertFileExists($root . '/core/modules/system/system.info.yml');

My 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?

-  private function populateFilesystemStructure(array &$filesystem_structure) {
-
+  protected function populateFilesystemStructure(array &$filesystem_structure) {

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.

+      $content_by_file[$file] = Yaml::dump($info);

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.

donquixote’s picture

I forgot to mention, I added this:

@@ -236,6 +236,9 @@ public function scan($type, $include_tests = NULL) {
    */
   public function setProfileDirectoriesFromSettings() {
     $this->profileDirectories = array();
+    if (!function_exists('drupal_get_profile')) {
+      throw new \RuntimeException('Function drupal_get_profile() does not exist.');
+    }

and the respective test:

+  /**
+   * Tests what happens if ->scan('module') is called without prior
+   * ->setProfileDirectories(), outside a functioning Drupal environment.
+   *
+   * If this behavior is to change in the future, this test needs to be updated.
+   *
+   * @covers ::scan
+   *
+   * @expectedException \RuntimeException
+   */
+  public function testMissingSetProfileDirectories() {
+
+    // Create an ExtensionDiscovery with a non-existing root directory.
+    $extension_discovery = new ExtensionDiscovery('FAKE_ROOT', FALSE, NULL, 'sites/default');
+
+    $extension_discovery->scan('module', FALSE);
+  }

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.

mile23’s picture

Issue summary: View changes

I think that if you make a change you should cover it with a test.

I'm also sure the goal here is to have ExtensionDiscovery do its work without any dependencies, including bootstrap.inc which is where drupal_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.

mile23’s picture

donquixote’s picture

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.

I 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 that if you make a change you should cover it with a test.

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.

Having the class explode if it's used without an include file is regression.

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.

I'm also sure the goal here is to have ExtensionDiscovery do its work without any dependencies, including bootstrap.inc which is where drupal_get_profile() lives.

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.

mile23’s picture

Status: Needs review » Reviewed & tested by the community
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.

I am fine with this.

OK, 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.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 87: D8-2605654-86-ExtensionDiscoveryUnitTest.patch, failed testing.

The last submitted patch, 87: D8-2605654-86-ExtensionDiscoveryUnitTest.patch, failed testing.

donquixote’s picture

Status: Needs work » Needs review
StatusFileSize
new7.49 KB

Re-posting #79 by @dawehner.

donquixote’s picture

Status: Needs review » Reviewed & tested by the community

Setting to RTBC via #92 (Mile23).

almaudoh’s picture

donquixote’s picture

Issue summary: View changes
alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed #79 97d1fae and pushed to 8.1.x and 8.2.x. Thanks!

  • alexpott committed 08824dc on 8.2.x
    Issue #2605654 by Mile23, donquixote, dawehner, alexpott, catch: Modify...

  • alexpott committed 97d1fae on 8.1.x
    Issue #2605654 by Mile23, donquixote, dawehner, alexpott, catch: Modify...
donquixote’s picture

Issue summary: View changes
donquixote’s picture

Status: Fixed » Closed (fixed)

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