Follow-up to #2571375: Remove TranslationManager dependency from LanguageManager
Problem/Motivation
Since that issue was committed there are some Exceptions in some modules like monitoring and simplenews, the exception says that the service added in this patch "locale.plural.formula" doesn't exist. These come up when you are running tests that call \Drupal::translation()->formatPlural
from within a site that has the Language/locale module enabled.
The problem is in PluralTranslatableString::getPluralIndex(). That checks the existence of a function in locale.module and then assumes that if that function is defined, the service should exist. The function_exists() check returns TRUE because the function exists in the parent site, and it can even get called from within the test. But the function doesn't work, because it calls into the service, which doesn't exist within the test environment because locale.module is not enabled.
Kind of a mess!
Proposed resolution
1. Remove the static cached function_exists() return value in PluralTranslatableMarkup.
2. Check if the service exists before trying to use it in locale_get_plural(). This handles the case when the function was already there but the service is not.
Remaining tasks
None.
User interface changes
None.
API changes
None.
Data model changes
None.
Comment | File | Size | Author |
---|---|---|---|
#81 | interdiff.txt | 1.17 KB | juampynr |
#81 | function_exists_check-2573975-80.patch | 1.61 KB | juampynr |
#74 | 2573975-3-74.patch | 1.62 KB | alexpott |
#74 | 55-74-interdiff.txt | 1.89 KB | alexpott |
#60 | funcexists.txt | 1.87 KB | jhodgdon |
Comments
Comment #2
alexpottComment #3
BerdirWe have an existing issue to write a test to run tests with language/locale modules are enabled.
Comment #4
alexpottComment #6
alexpottFixing the patch
Comment #10
edurenye CreditAttribution: edurenye at MD Systems GmbH commentedAdded related issues.
Comment #11
alexpottA different approach where only the locale module needs to be aware of the locale module.
Comment #12
BerdirCan we not keep using $this->getOption('langcode') for this, like the removed code?
Nitpicks:
* missing types
* the return description is a bit strange as it just documents the special case now.
* (optional)
Interesting. Wondering what happens if someone tries to replace that from a site specific services.yml. AFAIK, later yml files just autoamtically replace earlier definitions (core is always the first I think), not sure if it's a good idea to rely on that or not.
.
Did you just copy the full class or is the plan to simplify the core implementation do not actually do any translation?
Comment #14
alexpottComment #18
alexpottInstallUninstallTest really should not use t() or format_plural() at all - this stuff is changing underneath it.
Comment #19
juanse254 CreditAttribution: juanse254 at MD Systems GmbH commentedLooking good to me.
Comment #20
BerdirI think we're still missing explicit test coverage for this, e.g. what #2097185: Add tests to test SimpleTest with language module enabled on the parent site would provide and possibly merge that patch in here.
We already decided once before to postpone test coverage for that when that issue was opened, that it never happened shows that we should probably not get this in without a regression test.
Comment #24
alexpottAdded test from #2097185: Add tests to test SimpleTest with language module enabled on the parent site and made it work - but it passes on HEAD and with the PATCH :(
Comment #27
alexpottSo the test in #2097185: Add tests to test SimpleTest with language module enabled on the parent site / #24 didn't prove that this changes is a fix. I think that this might be a critical bug for multilingual sites but it would be great to have test that shows how HEAD is broken.
Comment #28
alexpottComment #29
edurenye CreditAttribution: edurenye at MD Systems GmbH commentedNeeds rebase.
Actually this test #2097185: Add tests to test SimpleTest with language module enabled on the parent site shows that this issue change the errors that gets, I think this fixes part of the errors that we get in the test.
Comment #30
edurenye CreditAttribution: edurenye at MD Systems GmbH commentedI'll do the rebase and merge with the tests.
Comment #31
edurenye CreditAttribution: edurenye at MD Systems GmbH commentedRebased. There is still some failing tests that I don't know how to fix.
Comment #33
edurenye CreditAttribution: edurenye at MD Systems GmbH commentedI don't know why, but if I change the test to one that doesn't use WebTestBase like LanguageFallbackTest, then the test doesn't fail, but the problem is without the patch it also don't fail, so then it doesn't really help us.
Comment #34
BerdirI think to trigger this scenario, we need a test that does *not* enable locale in a parent site that does. So we need two tests, with different modules.
We also can't just run any test because a child site test apparently needs that special .htkey file to be able to do requests against the child-child site. So added a new test that does just that. Still doesn't fail as expected, though :(
Uploading the current progress and including test-only patch that doesn't seem to fail for me.
Comment #35
aspilicious CreditAttribution: aspilicious commentedI have this error today and for some reason I thought Berdir could help me ;).
After patching I had a fatal but a readable one.
The actual test line was the following:
That route does a redirect to the URL above.
I know this is probably not related but maybe it gives an idea of the cause of the issue.
Comment #36
jhodgdonUpdating issue summary because it wasn't clear it was talking about running tests.
Tempted to bump to critical as suggested in #27. You basically can't run simpletests from multilingual sites and expect them to work.
Comment #37
jhodgdonBy the way, that latest patch seems way way over complicated.
See #2583697-3: Fatal error running some tests in UI with locale.module enabled for a suggestion of a simpler way to fix this -- just check that the service exists before calling into it, rather than assuming it does exist. This seems way simpler than making a whole new class etc.
As far as making a test for this... could we not take a different tactic and make a new testing *environment* for it? I think we are already testing vs. PostgreSQL and various versions of PHP for Drupal 8. Could we also make a different test script that would enable all Drupal modules in the outer environment, and run all the tests from that environment? That might catch some other similar problems. I don't know enough about the testing infrastructure I guess, but an approach like that might be easier than trying to coerce simulation of running a test within another test.
Comment #38
alexpott@jhodgdon whilst the approach might seem over complicated it fixes the dependencies - core's TranslationManager shouldn't have code that checks if a module or service exists... this is what dependency inversion is all about. Also the fact that this breaks when certain modules are installed in the parent testing environment shows that the way we test is wrong. The should never be any chance that the test runner can change what is under test.
Comment #39
jhodgdonOK, that makes sense.
I still don't know if we can make an automated test for this though, since it's a case of "If you run this test from a site that has module A installed, the way the test runner works makes the test break", not "If you do this on a site, it is broken and the test fails to illustrate the bug" like most tests. The attempt at making a test so far has failed.
Which is why I was wondering if we could make a test environment that would run tests from a site that has more modules enabled (like maybe all of them), to see if any other problems come up. It is possible that locale having this particular problem is not the only one in Core like this, where the assumption is that if a function exists, a certain module is enabled (and thus its services exist.
Comment #40
jhodgdonAlso regarding the latest patch... I thought that if Core provides a service, and a module wants to override the service (when it is enabled), then the right way was ... see
https://api.drupal.org/api/drupal/core!core.api.php/group/container/8#se...
(the section on overriding services). That doesn't seem to be what the locale module is doing in this patch?
Hm. We don't seem to have any documentation on service "decorating". What is that and where is it documented? Guess that is a separate issue...
Comment #41
alexpott@jhodgdon - yeah the decoration pattern is advantageous because you can still access the old service from the replacement service but other can't. We don't have docs on it - it is a feature of the symfony container and in general a good design pattern. Because if everything decorates services then it is possible for multiple modules to work together whilst decorating the same core service.
Comment #42
juampynr CreditAttribution: juampynr at Lullabot commentedRe-rolling.
I can reproduce this issue when running more than one test of GitHub's port of Pathauto. Without this patch, the following error happens when running more than one test (and Locale module not being installed):
After applying the patch, I don't see this error when I run Pathauto tests.
Comment #44
jgrubb CreditAttribution: jgrubb as a volunteer commentedHi, I'm getting this on a completely clean install with the minimal profile --
I do not have any multi-lingual modules enabled and am trying to run everything in the "system" group from the UI.
Comment #45
Gábor HojtsyNeeds issue summary update with proposed resolution, API changes, etc. Also given the API changes (new methods required on an interface), I don't think its allowed in 8.0.x, but it would need to be 8.1.x, no?
Comment #46
xjmYep, the current patch on this issue is 8.1.x material. There might be a way that we could make an 8.0.x-safe patch, but moving to 8.1.x for now.
Comment #47
EclipseGc CreditAttribution: EclipseGc commentedAs I understand semver, we'll have to make an 8.0.x safe patch any way we cut this. No?
Eclipse
Comment #48
juampynr CreditAttribution: juampynr at Lullabot commentedOops, I forgot to add a file into my re-roll patch at #42 (TranslationManager.php). Here is the full patch. There is still work to do to meet @Gábor Hojtsy's and @xjm's feedback.
Comment #49
Gábor HojtsyDiscussed this further with @xjm. The matter of fact is Drupal 8.1.x does not allow backwards incompatible changes to interfaces either, such as requiring new methods to be implemented, which would mean a WSOD with existing class implementations. See https://www.drupal.org/core/d8-allowed-changes#minor My understanding then is the proposed change as-is is only possible in Drupal 9 and a new interface needs to be added extending the existing one to fix this issue.
Comment #50
juampynr CreditAttribution: juampynr at Lullabot commentedRemoved a debug statement and fixed a t() statement in the tests.
I am working now on a patch that complies with semantic versioning for 8.0.x.
Comment #51
xjmOkay I misunderstood @Gábor Hojtsy's question on the issue. Looking at the current patch, it cannot go into 8.1.x either because there are changes to an interface. We need to find a way to fix this bug that does not break BC, including not changing existing interfaces. See https://www.drupal.org/core/d8-allowed-changes#minor and https://www.drupal.org/core/d8-bc-policy for details on what can go into a minor release.
@EclipseGc and @juampynr, not all fixes go into 8.0.x at all. I'd suggest only focusing on an 8.1.x-compatible fix for now.
Comment #52
juampynr CreditAttribution: juampynr at Lullabot commentedI must be missing something, but this alternative patch works for me locally. I could run Pathauto tests without experiencing the reported error. Let's see what core tests say.
Comment #53
Gábor HojtsyLook simple and even 8.0.x compatible to me :) Let's add a test in core then that this fixes the problem.
Comment #54
BerdirThe static doesn't make sense to me. We can't statically cache a non-static state that might change during the request. If this works, then only because nothing calls it before calling it within the test and nothing calls it after that we'd expect to work again.
But yes, something like that is the quickfix that we have to apply here then. But I'd actually do the hasService() within locale_get_plural() then, because then it's statically cached with drupal_static() which is invalidated before/after running tests.
Note that we have tried to write tests for this before and failed to produce a test that fails/passes as expectedly.
This is a pretty serious issue, that everyone with a non-english site faces when running tests and it's very hard to understand. Not sure if we should push a fix in without test coverage. We did that before (#2097185: Add tests to test SimpleTest with language module enabled on the parent site) and it is now broken again, but fixing it might be more important here than preventing regressions.
Comment #55
juampynr CreditAttribution: juampynr at Lullabot commentedHere is another patch where I have removed the static variable
static::$localeEnabled
(as far as I understand, it is not needed) and I have moved thehasService()
check intolocale_get_plural()
.I don't know how to replicate this issue with tests because part of the required context resides in the environment that triggers the tests (having locale module installed).
Here is the simplest way to replicate it:
1. Install locale module.
2. Run Drupal\aggregator\Tests\AggregatorAdminTest
Expected: tests pass.
Actual: The following errors are triggered because the test calls
\Drupal::translation()->formatPlural
:Here is the textual version of the error:
Comment #56
alexpottThe problem with #55 is that locale_get_plural() will then statically cache results when the service is not available. And we need a failing test...
Comment #57
alexpottFrom the issue summary...
So the problem is with the way in which we run tests... ie the existence of a parent site :(. Every single function exists check for a Drupal function is wrong :( because any module that has one of these functions in the parent site but in the child site could cause problems.
Comment #58
jhodgdonYES, #57 spotlights the problem exactly. I think that checking function_exists() could also cause weirdness where a function could get called (in the parent site PHP) that shouldn't exist in the test environment because the module isn't enabled. If we're using function_exists() as a proxy for "is this module installed that defines the function", we should instead check that the module is installed.
However, the problem is probably not that bad. For instance, we do function_exists() to invoke hooks, but we already only check for invoking them on installed modules, or in other cases like while we're installing a module. We also do function_exists() to check for PHP extensions, and those don't differ between the test running environment and the in-test site.
Anyway... maybe we should do a survey of all the function_exists() calls in Core and see if they're OK [wrapped inside some module exists check or checking PHP extension type things] or need attention.... I'll take a look.
Comment #59
alexpottFWIW #2312191: Change Simpletest UI from a test runner to a CLI snippet generator would fix this.
Comment #60
jhodgdonOK... So I grepped through core for function_exists() calls, and looked at all of them. I decided it was probably OK if a test was looking for function_exists() on a testing module, because no one would have that enabled anyway. And it was probably OK if a call was made to test for a PHP library like Curl or GD or multibyte or stuff like that. And I think the calls in ModuleHandler and ThemeHandler* classes are OK because they're wrapped inside checks for which modules/themes are enabled.
So. Here's the remaining list of possibly questionable function_exists() calls, for your perusal. Some of them may not be problematic... some probably are, for cases of "Running test A from an environment where test B is enabled".
Comment #61
alexpottThese two might cause issues if the rdf module is enabled in the parent site
Comment #62
juampynr CreditAttribution: juampynr at Lullabot commentedI am not clear on how should I adjust the patch. @alexpott's feedback at #56 says:
I think that I don't understand the above statement. By debugging I could see that locale_get_language() caches the plural formula for all languages. Should I change anything there?
On an additional note, this was my initial statement for debugging the workflow until locale_get_language() is called:
echo \Drupal::translation()->formatPlural(2, '1 comment', '@count comments');
Comment #63
jhodgdonRight, and there's one in there that would do the same for the History module:
And several that I think are calling into File module functions, or they could be the base file.inc functions...
Comment #64
juampynr CreditAttribution: juampynr at Lullabot commentedComment #65
jgrubb CreditAttribution: jgrubb as a volunteer commentedHi, just wanted to say that the patches from #52 and #55 both fix the problem that I was having - not being able to run the "system" test group from the UI or the "path" test group from the UI. It was bombing out with the same error that @juampy posted at the top of #55 - the supposed ajax error that was actually caused by the subject of this thread.
Thanks all.
Comment #66
juampynr CreditAttribution: juampynr at Lullabot commentedThanks @jhodgdon, I inspected the following for each of your findings a) what does the code that has the function_exists() statement do with it; and b) what does the function being checked by function_exists() do:
Here are my findings:
Seems safe to me. The theme layer is always present.
This is the one that got us into this issue. It is problematic because locale_get_plural() calls
\Drupal::service('locale.plural.formula')->getFormula($langcode);
, which may not be available in a test.Same as the above one.
Safe. It just returns $controller.
Safe. It is mocking the function.
Safe. It is mocking the function.
Safe. It does not load any service that may not be available in a test.
Safe, it's just calling the function.
Safe. It is mocking the function.
Safe. It is mocking the function.
Safe. It is mocking the function.
Safe. It is mocking the function.
Safe. It does not load any service that may not be available in a test.
Safe. It does not load any service that may not be available in a test.
Safe. It is mocking the function.
Safe. It is mocking the function.
Safe. It is mocking the function.
Safe. It is mocking the function.
Safe. It is mocking the function.
As @alexpott said, once #2312191: Change Simpletest UI from a test runner to a CLI snippet generator gets fixed, this issue won't be a problem again as there will be full separation between the environment running tests and the test environment.
Should I work on anything else on the latest patch?
Comment #67
jhodgdonThanks for the analysis in #66, but... The problem is not just that a service wouldn't exist. The problem is also this...
For example, let's think about function_exists('rdf_get_namespaces').
Let's say you are running tests from a site that has the RDF module enabled, and running a test that doesn't include the RDF module. This function exists check, when running inside a test, will return TRUE. However, inside the test site, the RDF module is not enabled, so the TRUE return value is wrong. The test can call the function, but it will probably not function properly (no database for RDF in the test site, etc.).
Comment #68
jhodgdonOK, so. We still have this very commonly encountered problem that tests have trouble running from the Simpletest UI if either the locale/language modules are being used in the test and not in the site the tests are running from, or they are being used on the site the tests are running from, but not in the test (I have seen problems both ways).
The above patch fixes this problem, apparently (see #65), but it may need some adjustment (see #56) and it also doesn't have a test because it's really hard to make a test that catches this problem in the simpletest UI.
Also, there are probably other problems in core (see #60).
So. Questions:
a) Does this issue have to fix all possible problems like this in Core? Or can we fix the problem many people are currently having, and maybe do all the rest in a follow-up issue?
b) Can we commit this patch that would fix the immediate problem, without a test and even though it may have other problems?
This is really getting in the way of development, at least for me.
Comment #69
Gábor HojtsyI don't think all the function_exists should be fixed in this issue. Also given that we don't see a way to test this, I think this should get in and let people move forward. However the analysis done on the other cases of function_exists is good and issues should be opened to fix those too. The solution may be entirely different for those.
Comment #70
Gábor HojtsyComment #73
alexpottThe problem I have with the current solution is that translation code can get called super super early and when it does it could end up statically caching the incorrect information in
locale_get_plural()
.I don't think we should be statically caching anything if the service does not exist.
Comment #74
alexpottWe could do something like this...
I've also filed a follow-up to explore a proper solution - #2660338: locale_get_plural call in PluralTranslatableString is wrong
Comment #75
juampynr CreditAttribution: juampynr at Lullabot commentedPatch at #74 works for me.
Comment #76
swentel CreditAttribution: swentel commentedLooks good to me too
nitpick - over 80 chars
Comment #77
juampynr CreditAttribution: juampynr at Lullabot commentedPatch at #75 still applies.
Comment #78
lokapujyaMore than a nitpick. certain is doubled.
Comment #79
Gábor Hojtsy@lokapujya: can you help with the nitpick and more than nitpick so we can get this in? :) that would be amazing :) Thanks!
Comment #80
edurenye CreditAttribution: edurenye at MD Systems GmbH commentedComment #81
juampynr CreditAttribution: juampynr at Lullabot commentedNitpicked.
Comment #82
Gábor HojtsyYay, thanks!
Comment #85
catchCommitted/pushed to all three 8.x branches, thanks!
Comment #87
Gábor HojtsyYay, thanks all!