Follow-up to #2605654: Modify Drupal\Core\Extension\ExtensionDiscovery to allow for scanning multiple file systems, enabling vfsStream testing

ExtensionDiscovery has a lot of behavior and possible states that is currently not covered by tests.

Some of this may be intended, some if it may be bugs.
I think the correct thing to do is to cover the current behavior with tests, and only then consider fixing things. This way we see exactly which behavior is changing.

This does not break anything or modify existing behavior, so it can go into 8.1.x.

Comments

donquixote created an issue. See original summary.

donquixote’s picture

Calling the new test ExtensionDiscoveryKernelTest, so we can distinguish it in conversations more easily.

Undecided between static::assertEquals() and $this->assertEquals().
In the current version of PhpUnit, it is clearly a static method. And my IDE will complain if I use $this->.
But in the previous issue, my static:: was changed to $this->.

Please don't just silently "fix" this, but explain why we use $this->assertEquals() in Drupal core.

Another note:
The two helper methods in ExtensionDiscoveryTest are protected instead of private or public. This suggests that they are meant for reuse, but the only way to reuse them is inheritance. I disagree with this. If we want reuse, they should rather be public static and live in a separate class. If we don't want reuse, they should be private.
But so it was done, and so shall it remain.

mile23’s picture

Status: Needs review » Needs work

Please don't just silently "fix" this, but explain why we use $this->assertEquals() in Drupal core.

Docs here: https://phpunit.de/manual/current/en/appendixes.assertions.html#appendix...

We use them as $this-> but they're static to support non-OOP use: https://github.com/sebastianbergmann/phpunit/blob/master/src/Framework/A...

Re: private and protected, the real problem in ExtensionDiscoveryTest is that there's a function that should be a data provider or at least just a helper method, but instead it builds *all* the dependencies for you, magically. That's why testVfs() is the wrong way to go: The test should be refactored so that dependencies and expectations are set up in the test method, so you can read them. Testing the test is a bad pattern.

Anyway, for the purposes of Drupal, there's no such thing as private because @dawehner doesn't like it. :-) So protected is what we have.

  1. +++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionDiscoveryKernelTest.php
    @@ -0,0 +1,73 @@
    +  public static function testScan() {
    

    Any reason this is static?

  2. +++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionDiscoveryKernelTest.php
    @@ -0,0 +1,73 @@
    +    // It is not possible to set up a virtual root directory with vfsStream,
    +    // because the test suite already uses vfsStream for its own purposes.
    +    // Instead, discover dummy modules in a test directory.
    +    $root = __DIR__ . '/root';
    

    Extreme frowny face. This is why Drupal can not have nice things. Of course, the real solution is to remove the dependency on the kernel from ExtensionDiscovery and continue to expand the vfs-based tests in unit-land.

  3. +++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionDiscoveryKernelTest.php
    @@ -0,0 +1,73 @@
    +        // The test theme, 'discovery_test_example_theme', will not be found,
    +        // because ExtensionDiscovery ignores the custom site directory.
    +        // When this behavior is fixed, the test needs to be updated.
    

    Let's assert that it's not found so that we'll come back and change this if we need to.

donquixote’s picture

Any reason this is static?

No. This is a bug.

Extreme frowny face.

Yeah. But we are testing existing behavior. So I think it is ok for this issue, and we can change it later.

Let's assert that it's not found so that we'll come back and change this if we need to.

This is already being asserted with the assertEquals() on arrays, which asserts that the array has exactly those values and keys that are expected, not more and not less. The test will fail if you change the sitePath handling in ExtensionDiscovery.
Imo this is fine as it is.

Re: private and protected, the real problem in ExtensionDiscoveryTest is that there's a function that should be a data provider or at least just a helper method, but instead it builds *all* the dependencies for you, magically. That's why testVfs() is the wrong way to go: The test should be refactored so that dependencies and expectations are set up in the test method, so you can read them. Testing the test is a bad pattern.

Data providers are useful if you want to run the same test more than once, with different parameter values.
Otherwise, I don't see an advantage compared to a helper method directly called from within the test.

The expectations in the different test methods are very similar, with a few changes only. Therefore, it makes sense to do the main part of the expectations setup in a shared helper method, instead of the test itself.

The helper could be changed to be more verbose and more explicit. Maybe this would improve readability, maybe not. I personally doubt it.
The price for the dynamic setup is that, if a test fails, we have to consider that our dynamic setup went wrong.
Therefore there are test methods to check whether the dynamic setup works as expected.
It is certainly unconventional, but I don't see any real problem with it.
The alternative, as said, is a more verbose and explicit setup. I personally don't see this as the preferable option.

Anyway, for the purposes of Drupal, there's no such thing as private because @dawehner doesn't like it. :-) So protected is what we have.

I am not happy, as I explain here: #2727011: [policy, no patch] Private vs protected, and the role of inheritance
But I am ok to move forward with protected, until significant people change their minds.

donquixote’s picture

We use them as $this-> but they're static to support non-OOP use:

Ok.
It is just sad for my IDE. I like my files to appear with a green checkbox.
Now I either have to live with these IDE notices, or I have to disable this inspection. Not really happy with either.
But if this is how we do things, then ok let's keep it this way.

donquixote’s picture

Waiting for more opinions / review(s). Then I will post another patch.

dawehner’s picture

  1. +++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionDiscoveryKernelTest.php
    @@ -0,0 +1,73 @@
    +
    +    // It is not possible to set up a virtual root directory with vfsStream,
    +    // because the test suite already uses vfsStream for its own purposes.
    +    // Instead, discover dummy modules in a test directory.
    +    $root = __DIR__ . '/root';
    

    Can't you setup a second vfs root?

  2. +++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionDiscoveryKernelTest.php
    @@ -0,0 +1,73 @@
    +    // Verify a few individual extensions in more detail.
    +    $extension_expected = new Extension($root, 'module', 'core/modules/discovery_test_example/discovery_test_example.info.yml', NULL);
    +    $extension_expected->subpath = 'modules/discovery_test_example';
    +    $extension_expected->origin = 'core';
    

    Would it be possible to move this bit to a second test function?

donquixote’s picture

Can't you setup a second vfs root?

This would make things easier indeed.
I had some difficulties wrapping my head around the API of vfsStream.
If you can post a simple example how to achieve this, I would be grateful.
Also, if you know a way to register slash-separated paths instead of nested array structures.

Would it be possible to move this bit to a second test function?

Why would we? These assertions are all on data from the same operation being tested.
Putting this into a separate method would mean the same operation, with same input data, would be run twice.

dawehner’s picture

Well, I think something like this should work:

\org\bovigo\vfs\vfsStream::setup('example1', [], ['dir1' => ['file1' => '123]]);
\org\bovigo\vfs\vfsStream::setup('example2', [], ['dir2' => ['file2' => '123]]);
donquixote’s picture

There we have it.

    vfsStream::setup('example1', null, ['dir1' => ['file1' => '123']]);
    scandir('vfs://example1');  // Works.
    vfsStream::setup('example2', null, ['dir2' => ['file2' => '123']]);
    scandir('vfs://example1');  // Fails.

The first scandir() works, the second one fails.
Fail at the second scandir():

scandir(vfs://example1): failed to open dir: "org\bovigo\vfs\vfsStreamWrapper::dir_opendir" call failed

It seems that vfsStream can only handle one root directory at a time, globally.

Btw, if I use [] instead of null as the second argument, even the first one fails, I suppose for permission reasons.

But I find now that the following code works, by reusing the same root directory:


    vfsStream::setup('example1', null, ['dir1' => ['file1' => '123']]);

    static::assertEquals(
      [
        '.',
        '..',
        'dir1',
      ],
      scandir('vfs://example1'));

    vfsStream::create(['dir2' => ['file2' => '123']], vfsStreamWrapper::getRoot());

    static::assertEquals(
      [
        '.',
        '..',
        'dir1',
        'dir2',
      ],
      scandir('vfs://example1'));

I see what I can do with this :)

donquixote’s picture

Status: Needs work » Needs review
StatusFileSize
new30.88 KB
new30.55 KB

Now everything with vfsStream :)
Who said it cannot be done?

EDIT: Oh no I did it again. Unintended modifications in ExtensionDiscovery. Ignore this patch.

donquixote’s picture

Trying again.

donquixote’s picture

Is there anything else we need to test, with or without kernel?

The last submitted patch, 11: D8-2729711-11-ExtensionDiscoveryTest.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 12: D8-2729711-12-ExtensionDiscoveryTest.patch, failed testing.

donquixote’s picture

donquixote’s picture

Status: Needs work » Needs review
mile23’s picture

Status: Needs review » Needs work

@donquixote asked me to review this in IRC, so here we go. :-)

  1. +++ b/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryTest.php
    @@ -17,21 +17,159 @@
    +  private $filesByTypeAndNameExpected;
    ...
    +  private $vfsRootUrl;
    ...
    +  private $vfsDrupalRoot;
    ...
    +  public function testScan() {
    +
    +    // Build a modified version of the expectations array.
    +    $files_by_type_and_name_expected = $this->filesByTypeAndNameExpected;
    

    Class properties are a bad place to store fixtures and figure out expectations. That's because when it comes time to patch this test, it will have to be refactored, and then we lose the whole test class' predictive value.

    Enforce isolation by re-building the fixture for each test locally, or figure out how to turn it into a dataProvider.

    Magic and side-effects are bad.

  2. +++ b/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryTest.php
    @@ -17,21 +17,159 @@
    +  /**
    +   * Sets up a virtual subdirectory at 'vfs://root/subdir' in addition to the
    +   * virtual root at 'vfs://root' already set up by the base class.
    +   *
    +   * Unfortunately, vfsStream only supports one root directory active at a time.
    +   *
    +   * Since the virtual directories persist between test methods, it is better to
    +   * do this all at once in $this->setUp(), and not in each test method.
    +   */
    +  function setUp() {
    +    parent::setUp();
    +
    +    $drupal_directory_structure = [];
    +
    +    // The expected extensions array for a default run of test discovery, with
    +    // 'sites/default' as site path.
    +    $this->filesByTypeAndNameExpected = $this->populateFilesystemStructure($drupal_directory_structure);
    +
    +    $this->validateDrupalDirectoryStructure($drupal_directory_structure);
    +
    +    $root_directory_structure = [];
    +
    +    // Virtual Drupal directory structure at 'vfs://root/www'.
    +    $root_directory_structure['www'] = $drupal_directory_structure;
    +
    +    // Another, much simpler, Drupal directory structure at 'vfs://root/www_other'.
    +    $root_directory_structure['www_other']['core']['profiles']['minimal']['minimal.info.yml'] = <<<EOT
    +type: profile
    +name: 'Distinct root paths profile'
    +core: 8.x
    +
    +EOT;
    +
    +    // Set up the 'vfs://root/subdir' to simulate a filesystem.
    +    $this->vfsRootUrl = vfsStream::setup('root', null, $root_directory_structure)->url();
    +    $this->vfsDrupalRoot = $this->vfsRootUrl . '/www';
    +  }
    

    This code will be run for every test, and will set up expectations and cause assertions before the test even starts.

    That means all tests which rely on this class are risky.

    It also means that you are setting expectations for all future tests which will be added to this class. That will likely be a problem later on.

  3. +++ b/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryTest.php
    @@ -17,21 +17,159 @@
    +  private function validateDrupalDirectoryStructure(array $filesystem) {
    ...
    +    $this->assertTrue(isset($filesystem['profiles']['myprofile']['modules']['myprofile_nested_module']['myprofile_nested_module.info.yml']));
    ...
    +    $this->assertSame($expected_file_contents, $filesystem['profiles']['myprofile']['modules']['myprofile_nested_module']['myprofile_nested_module.info.yml']);
    ...
    +  /**
    +   * Tests and documents the discovery expectations for the tests below.
    +   */
    +  public function testExpectations() {
    ...
    +    $this->assertEquals('modules/dir_vs_subdir/subdir/dir_vs_subdir.info.yml', $files_by_type_and_name_expected['module']['dir_vs_subdir']);
    ...
    +    $this->assertArrayHasKey('typeshift', $files_by_type_and_name_expected['module']);
    +    $this->assertArrayHasKey('typeshift', $files_by_type_and_name_expected['theme']);
    +    $this->assertArrayHasKey('typeshift', $files_by_type_and_name_expected['theme_engine']);
    +    $this->assertArrayHasKey('typeshift', $files_by_type_and_name_expected['profile']);
    ...
    +    $this->assertArrayNotHasKey('block_test', $files_by_type_and_name_expected['module']);
    +    $this->assertArrayNotHasKey('core_test', $files_by_type_and_name_expected['module']);
    ...
    +  /**
    +   * Tests and documents the behavior of vfsStream.
    +   */
    +  public function testVfs() {
    

    Tests should not assert the validity of their fixtures. Declaring the fixture itself is defining the expectation. That's why it should be easy to read. If the fixture code is so complex that you have to test it, you're headed down the wrong path.

    It's just an array, man. :-)

Basically:

Turn populateFilesystemStructure() into a helper method that does not take a parameter and just returns an array. Rename it to something like getDefaultFilesystemStructure(). Use that method in each test to build out the filesystem you need in a local variable. Remove the filesystem validation code.

If you find that different tests modify the filesystem in the same way, you can then add another helper which does the modification and then returns the array.

The benefit of figuring out how to turn this into a dataProvider would be that you could then use the array key to send a message through the fail notification for that data set, which might be a big help since there are going to be more and more edge cases added later. But just having helper functions instead of class properties is a good start.

Strive to make setUp() empty.

donquixote’s picture

@Mile23 Thanks for the review.

Tests should not assert the validity of their fixtures. Declaring the fixture itself is defining the expectation. That's why it should be easy to read. If the fixture code is so complex that you have to test it, you're headed down the wrong path.

Okok you are right.

I was shying away from the big verbose explicit array definition. But ok, it is the right thing to do.

This code will be run for every test, and will set up expectations and cause assertions before the test even starts.

That means all tests which rely on this class are risky.

It also means that you are setting expectations for all future tests which will be added to this class. That will likely be a problem later on.

The vfsStream setup is shared between test methods. If I do this setup in one method, then the next method will see the same directories in vfsStream, unless it overwrites them. By doing it in the setUp() method, at least we are honest.

This leaves the following options:
- Let each test method clean up after itself.
- Tell PhpUnit to run each test method in its own separate process (which I think is possible with a doc tag or something).
- Let each test method that uses vfsStream override any existing setup.
- Let each test method use a different virtual subdirectory.
- Do a shared filesystem setup in the setUp() method. This is what I chose to do in the patch.

If we make the expectations setup explicit, and decouple it from the filesystem setup, then only the filesystem setup is left in the setUp() method.

I don't think leaving this there is as big a problem as you make it out to be.
Future test methods that do not wish to use this specific directory structure can register additional directories elsewhere.
E.g. vfs://root vs vfs://root2

Next patch will follow when we make a decision about the above options.

Tests should not assert the validity of their fixtures. Declaring the fixture itself is defining the expectation. That's why it should be easy to read. If the fixture code is so complex that you have to test it, you're headed down the wrong path.

If we have a good reason why the fixtures code should be complex, or if the fixtures depend on a fragile or intransparent tool, then I see nothing wrong with testing the fixtures first.

This premise needs to be questioned, of course. Is the complexity really necessary or helpful?
In our case, the dynamic setup of expectations was not necessary. So here it is better to use an explicit array definition and get rid of the assertions.

For an outsider, vfsStream has to appear like a fragile and intransparent tool. Is some global PHP configuration is messing with vfsStream? Maybe we are not using vfsStream correctly?
Seeing a few assertions that verify that vfsStream works correctly can be reassuring for a reader.

And seeing such an assertion fail is certainly preferable to a failure further down, where we are not sure if vfsStream is wrong, or the component we are testing is wrong.

donquixote’s picture

This patch gets rid of the ridiculously dynamic building of the expectations array.
Also the root path is now hardcoded, so we get rid of all object variables.

The vfs setup is still shared between all test methods.
I think this is kind of ok.

donquixote’s picture

Status: Needs work » Needs review

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

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

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

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

andypost’s picture

Version: 8.6.x-dev » 8.8.x-dev

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative, +Needs issue summary update

This issue is being reviewed by the kind folks in Slack, #needs-review-queue-initiative. We are working to keep the size of Needs Review queue [2700+ issues] to around 400 (1 month or less), following Review a patch or merge request as a guide.

Tagging for IS update as a lot has changed in 7 years so this issue may need to be rescoped.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.