Followup for #2946419-9: Entity: Convert system functional tests to phpunit

Problem/Motivation

In TestDiscovery:getTestClasses() we iterate through all the files that could potentially contain tests. But if the file does not contain a class, or contains a class without annotation, then we pass empty $doc_comment to getTestInfo():

$parser = new StaticReflectionParser($classname, $finder, TRUE);
...
$info = static::getTestInfo($classname, $parser->getDocComment());
...
public static function getTestInfo($classname, $doc_comment = NULL) {
    if (!$doc_comment) {
      $reflection = new \ReflectionClass($classname);
      $doc_comment = $reflection->getDocComment();
    }

These lead to execution of triggers inside of file, even if the file is never launched during the testing.

As result, we can not add deprecated trigger to traits, interfaces, *TestBase (without class annotation) etc.

Proposed resolution

getTestInfo() should not perform reflection if $doc_comment !== NULL. See #47.

Remaining tasks

User interface changes

API changes

Data model changes

CommentFileSizeAuthor
#51 interdiff-50-51.txt1.73 KBAnonymous (not verified)
#51 2949363-51.patch1.76 KBAnonymous (not verified)
#51 2949363-51-test-only.patch1.14 KBAnonymous (not verified)
#50 interdiff.txt1.37 KBMile23
#50 2949363_50.patch2 KBMile23
#50 2949363_50_test_only.patch1.37 KBMile23
#48 2949363-48.patch642 bytesAnonymous (not verified)
#44 2949363-44.patch786 bytesAnonymous (not verified)
#41 2949363-41.patch4.3 KBAnonymous (not verified)
#39 2949363-class-pass.patch1.97 KBAnonymous (not verified)
#35 2949363-class-pass.patch1.97 KBAnonymous (not verified)
#35 2949363-interface-fail.patch1.97 KBAnonymous (not verified)
#35 2949363-trait-fail.patch1.97 KBAnonymous (not verified)
#32 2949363-class-pass.patch1.97 KBAnonymous (not verified)
#32 2949363-interface-fail.patch1.98 KBAnonymous (not verified)
#32 2949363-trait-fail.patch1.97 KBAnonymous (not verified)
#25 2949363-25.patch3.17 KBAnonymous (not verified)
#25 2949363-25-test-only.patch1.45 KBAnonymous (not verified)
#21 interdiff-18-21.txt2.11 KBAnonymous (not verified)
#21 2949363-21.patch2.56 KBAnonymous (not verified)
#21 2949363-21-test-only.patch1.45 KBAnonymous (not verified)
#18 2949363-18.patch2.74 KBAnonymous (not verified)
#18 2949363-18-test-only.patch1.62 KBAnonymous (not verified)
#15 interdiff-14-15.txt1.16 KBAnonymous (not verified)
#15 2949363-15.patch1.4 KBAnonymous (not verified)
#15 2949363-15-test-only.patch634 bytesAnonymous (not verified)
#14 interdiff-6-14.txt804 bytesAnonymous (not verified)
#14 2949363-14.patch1.94 KBAnonymous (not verified)
#9 2949363-9.patch1.52 KBtimmillwood
#7 2949363-7.patch24.12 KBtimmillwood
#7 interdiff-2949363-7.txt545 bytestimmillwood
#6 2949363-6-test-only.patch1.15 KBAnonymous (not verified)
#4 2949363-4-test-only.patch1.14 KBAnonymous (not verified)
#2 2949363-2-test-only.patch1.19 KBAnonymous (not verified)
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Anonymous’s picture

vaplas created an issue. See original summary.

Anonymous’s picture

Status: Needs review » Needs work

The last submitted patch, 2: 2949363-2-test-only.patch, failed testing. View results

Anonymous’s picture

Status: Needs work » Needs review
FileSize
1.14 KB

Status: Needs review » Needs work

The last submitted patch, 4: 2949363-4-test-only.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Anonymous’s picture

FileSize
1.15 KB
timmillwood’s picture

Status: Needs work » Needs review
FileSize
545 bytes
24.12 KB

This worked for me locally, let's see if testbot agrees.

timmillwood’s picture

Sorry, the interdiff is correct, but the patch is messed up, let me try again.

timmillwood’s picture

FileSize
1.52 KB

The last submitted patch, 7: 2949363-7.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 9: 2949363-9.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

zymbian’s picture

Does the patch fail due to coding standard error ?

timmillwood’s picture

@zymbian no

Anonymous’s picture

@timmillwood, thanks for help here!

#9 - interesting setting, I did not know. But even if it softens the deprecated processing, should not they arise at all if the trait is not used?

Can we filter the tests list painlessly?

Anonymous’s picture

Lendude’s picture

Issue tags: +@deprecated

I realise this is a work in progress but some observations

  1. +++ b/core/modules/simpletest/src/TestDiscovery.php
    --- /dev/null
    +++ b/core/modules/system/src/Tests/Entity/DoNotUseMeDeprecatedTestTrait.php
    
    +++ b/core/modules/system/src/Tests/Entity/DoNotUseMeDeprecatedTestTrait.php
    @@ -0,0 +1,14 @@
    +namespace Drupal\system\Tests\Entity;
    

    This looks like a strange place to put this.

  2. +++ b/core/modules/system/src/Tests/Entity/DoNotUseMeDeprecatedTestTrait.php
    @@ -0,0 +1,14 @@
    + * @deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0.
    

    No we won't remove it, we still need to be able to deprecate traits in D9!

  3. +++ b/core/modules/system/src/Tests/Entity/DoNotUseMeDeprecatedTestTrait.php
    @@ -0,0 +1,14 @@
    +trait DoNotUseMeDeprecatedTestTrait {
    

    Lets come up with more descriptive name TestTraitDeprecationTrait or something?

And we need some way to indicate that this is testing something, so it won't get removed because it's not used anywhere.

Status: Needs review » Needs work

The last submitted patch, 15: 2949363-15.patch, failed testing. View results

Anonymous’s picture

So, the problem occurs when all tests are requested (for example, via get 'admin/config/development/testing' page). Let's just turn off error handling while getting this list.

#16: thanks for good points!

  • 16.1: ✅
  • 16.2: ✅ Sure, but I just could not resist the habit :)
  • 16.3: ✅ done even via Test class.
  • 16.4: ✅ via assert Name of deprecation test.

The last submitted patch, 18: 2949363-18-test-only.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 18: 2949363-18.patch, failed testing. View results

Anonymous’s picture

Status: Needs work » Needs review
FileSize
1.45 KB
2.56 KB
2.11 KB

Okay, back to Trait, because Test is also run, like all other tests.

The last submitted patch, 21: 2949363-21-test-only.patch, failed testing. View results

Mile23’s picture

So the problem is that when we add @trigger_error() to traits (and classes) we're actually deprecating the file and not the class. So whenever it's included or required, it will trigger the error.

For tests this is a non-issue because we don't deprecate tests, we just change them. For traits (and base classes) it's a problem because if the directory scan includes, requires, or reflects on the file, it will be loaded and the deprecation error is triggered.

If we disable the error handling during TestDiscovery, then there's two things:

1) The error will be 'handled' when the file loads, and we won't see the deprecation error during test time. This means we might as well not @trigger_error() in the first place. (Strictly speaking: We *might* see the error during test time, depending on which test runner, and whether we fork another process to run it.)

2) We've added complexity to TestDiscovery as a workaround that then will lead to false positives in testing, because we want things to explode (a little) when we use a deprecated class.

So basically -1 on #21.

This problem would go away as a side effect of #2296635: Evaluate performance impact of limiting test discovery to *Test.php filename suffix so we should work on that instead.

Anonymous’s picture

Status: Needs review » Closed (duplicate)
Related issues: +#2296635: Evaluate performance impact of limiting test discovery to *Test.php filename suffix
Anonymous’s picture

Status: Closed (duplicate) » Needs review
FileSize
1.45 KB
3.17 KB

Due to BC problem we need to go through several minor releases before #2296635: Evaluate performance impact of limiting test discovery to *Test.php filename suffix.

Perhaps we should look for some workaround? How about a more specialized version of the #21?

The last submitted patch, 25: 2949363-25-test-only.patch, failed testing. View results

alexpott’s picture

Let's adapt our test discovery to not scan TestBase.php files. Then we can deprecate TestBase classes. We can then proceed with #2296635: Evaluate performance impact of limiting test discovery to *Test.php filename suffix

alexpott’s picture

And we can do the same for *Trait.php

Mile23’s picture

Title: Impossible to make deprecated trait without fails » [meta] Impossible to make deprecated trait without fails
Component: phpunit » simpletest.module

Let's adapt our test discovery to not scan TestBase.php files. Then we can deprecate TestBase classes. And we can do the same for *Trait.php

Assuming you mean excluding *testbase.php files (we still want to scan for simpletest tests).

So basically instead of only allowing *Test.php we should exclude *TestBase.php and *Trait.php

Turning this into a meta, so we can track the later work.

Here's a specific issue: #2973127: Exclude *TestBase.php, *Trait.php and *Interface.php files from test discovery reflection

alexpott’s picture

Anonymous’s picture

Title: [meta] Impossible to make deprecated trait without fails » [meta] Impossible to make deprecated no-class without fails
Issue summary: View changes
FileSize
1.97 KB
1.98 KB
1.97 KB

Classes have not this problem, so we not need add special rules for *TestBase.

This is what happens:

# TestDiscovery:getTestClasses():
 $parser = new StaticReflectionParser($classname, $finder, TRUE);
...
 $info = static::getTestInfo($classname, $parser->getDocComment());

But StaticReflectionParser::parse() designed for parsing classes. So, it is not found doc annotaion for non-class file:

# StaticReflectionParser::parse()
if (preg_match("/\A.*^\s*((abstract|final)\s+)?class\s+ ....

So, when we request $parser->getDocComment() we get default empty value.

But for empty $doc_comment we do additional reflection:

public static function getTestInfo($classname, $doc_comment = NULL) {
    if (!$doc_comment) {
      $reflection = new \ReflectionClass($classname);
      $doc_comment = $reflection->getDocComment();
    }

As result - deprecated fails.

Interdiff between attached patches:

trait TestTraitDeprecationTrait { #fail
class TestTraitDeprecationTrait { #pass
interface TestTraitDeprecationTrait { #fail

The last submitted patch, 32: 2949363-trait-fail.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 32: 2949363-class-pass.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Anonymous’s picture

The last submitted patch, 35: 2949363-interface-fail.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

The last submitted patch, 35: 2949363-trait-fail.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 35: 2949363-class-pass.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Anonymous’s picture

Status: Needs work » Needs review
FileSize
1.97 KB

My bad in run-tests.sh for class again.

alexpott’s picture

Ah nice detective work @vaplas. So the real question is why do we bother with the real reflection? If we can't do static reflection let's assume it's not a test? Does that work...

Anonymous’s picture

Unlikely we can remove

if (!$doc_comment) {
      $reflection = new \ReflectionClass($classname);
      $doc_comment = $reflection->getDocComment();
    }

because getTestInfo() can be used in other places without pre-parsing. But what if we just pass not empty comment:

- $info = static::getTestInfo($classname, $parser->getDocComment());
+ $info = static::getTestInfo($classname, $parser->getDocComment() ?: 'incorrect class annotation');

Attached patch should pass with KernelTestDiscoveryTest, and fail with TestDeprecationClassTestBadName.

Status: Needs review » Needs work

The last submitted patch, 41: 2949363-41.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

alexpott’s picture

So #41 looks good. The fail is in a test which is found and running it triggers the deprecation - no?

Anonymous’s picture

Title: [meta] Impossible to make deprecated no-class without fails » [meta] Impossible to make trigger_error in some files without test fails
Issue summary: View changes
Status: Needs work » Needs review
FileSize
786 bytes

Yes + still my bad like #39:

-  $test_list = ['Drupal\Tests\simpletest\KernelTestDiscoveryTest', ...
+  $test_list = ['Drupal\Tests\simpletest\Kernel\TestDiscoveryTest', ....

IS updated per #40/ #41.

Mile23’s picture

Status: Needs review » Needs work

The combined Voltron powers of not performing reflection if it's not a test plus filtering out based on *TestBase.php and *Trait.php will yield one full solution. Should we close #2973127: Exclude *TestBase.php, *Trait.php and *Interface.php files from test discovery reflection and do it here? I guess I misunderstood all the stuff about phasing in this change.

+++ b/core/modules/simpletest/src/TestDiscovery.php
@@ -174,7 +174,7 @@ public function getTestClasses($extension = NULL, array $types = []) {
+        $info = static::getTestInfo($classname, $parser->getDocComment() ?: 'incorrect class annotation');
       }
       catch (MissingGroupException $e) {

This really should either return an empty data structure or it should throw MissingGroupException. I vote for MissingGroupException to be consistent with current behavior, and so we avoid false positives.

Anonymous’s picture

@Mile23, can you explain in a little more detail what we need to do?

#44 already throw @MissingGroupException, because 'incorrect class annotation' value does not contain '@group'. Or did I not get the #45 idea?

Mile23’s picture

OK.... Helps to read the thread a little better. :-)

The problem is that if we're calling from getTestClasses() and there's no existing annotation, getTestInfo() will perform reflection. We don't want that.

The solution is to have getTestInfo() check for NULL, because it says this:

public static function getTestInfo($classname, $doc_comment = NULL) {
    if (!$doc_comment) {

So we change !$doc_comment to $doc_comment === NULL.

Then getTestInfo() will not perform reflection because it already has a docblock that happens to be an empty string. It will throw MissingGroupException for the empty docblock, including those it finds within *TestBase and so forth.

Back in getTestClasses(), we then have to figure out whether that's from a real actual test that is only missing a @group annotation, or whether it's from an abstract type, such as *TestBase. getTestClasses() does that by looking for class names that end in *Test.

#2973127: Exclude *TestBase.php, *Trait.php and *Interface.php files from test discovery reflection will help with that, too, by giving us fewer files to begin with. In fact, that single change to getTestInfo() will solve the problem for run-tests.sh and we don't actually need to filter out *TestBase etc. However, we still need to change scanDirectory() for the phpunit test suite classes: #2973127-2: Exclude *TestBase.php, *Trait.php and *Interface.php files from test discovery reflection

Anonymous’s picture

Status: Needs work » Needs review
FileSize
642 bytes

Mile23, great thanks!

So we change !$doc_comment to $doc_comment === NULL.

I did not have enough brains to find it! Done.

Mile23’s picture

Thanks for working on it. :-)

In an ideal world, we'd have a good way to test whether reflection ended up happening. But we don't, other than feeding in an empty docblock and then comparing whether the class loaded or not.

Mile23’s picture

Added a test. I was wrong about how to test it... We can just compare which type of exception it throws, if we have both an empty annotation and a nonexistent class.

Anonymous’s picture

Nice idea! 💎

+++ b/core/modules/simpletest/tests/src/Unit/TestDiscoveryTest.php
@@ -385,6 +385,27 @@ public function providerTestGetPhpunitTestSuite() {
+      $this->assertInstanceOf(MissingGroupException::class, $e);
...
+      $this->assertNotInstanceOf(\ReflectionException::class, $e);

Nit: If the first assert fails, the test will end. Therefore, we can shorten the code.

RTBC++

Mile23’s picture

Status: Needs review » Reviewed & tested by the community

And since it's my idea but not actually my code I can still maybe get away with RTBC. :-)

I'm doing this prior to the tests finishing, but this only adds a a test and we had success in #48.

The last submitted patch, 50: 2949363_50_test_only.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

The last submitted patch, 51: 2949363-51-test-only.patch, failed testing. View results

alexpott’s picture

Title: [meta] Impossible to make trigger_error in some files without test fails » Impossible to make trigger_error in some files without test fails

No longer very meta :)

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed f22c3b0952 to 8.6.x and 7257e86610 to 8.5.x. Thanks!

Backported to 8.5.x because this is the test system and making everything work the same is less headachey.

  • alexpott committed f22c3b0 on 8.6.x
    Issue #2949363 by vaplas, Mile23, timmillwood, alexpott, Lendude:...

  • alexpott committed 7257e86 on 8.5.x
    Issue #2949363 by vaplas, Mile23, timmillwood, alexpott, Lendude:...
alexpott’s picture

To test this patch I used the --list option on the the run-test.sh command. Before and after applying the patch had no differences even with quite a few popular contrib modules hanging about.

Anonymous’s picture

Issue summary: View changes

#55: 😆

Perfect news! Thanks! 🚀🎉

Status: Fixed » Closed (fixed)

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