Problem/Motivation

The form to install pending updates at /admin/reports/updates/update (and /admin/modules/update and /admin/theme/update) has no notion that some of the available updates are incompatible with your currently installed version of core, and will happily try to download and install them for you. I haven't actually tested what happens, but it seems seriously problematic to leave it as is.

Splitting this off from
#3102724: Improve usability of core compatibility ranges on available updates report
#3096078: Display core compatibility ranges for available module updates
...

For example, with the latest screenshots from #3102724, on a test site setup to believe it's 8.7.10 core, we'd see this:

Screenshot of available updates for both compatible and incompatible releases, initial state

Compare that with the "Update" page on the same site:

Screenshot of 'Update' form for a site with  both compatible and incompatible releases available for update -- no indication some of these aren't compatible

No indication at all that redis 8.x-1.4 (the "Recommended release") won't install on your current version of core (which you still have to manually update, unlike all the contrib updates this form would (try to) install for you...

Proposed resolution

Remaining tasks

  1. Decide what to do:
    1. Into what table should incompatible releases be moved? Either existing "Manual updates required" (#10) vs. new "Not compatible" (#14). - Decided: new table.
    2. If new, should it be above (#14) or below (#21) core's "Manual updates required"? Below
    3. If new, do we need a little intro sentence / description? If so, what exactly should it say? No description
    4. Any other tweaks / concerns / improvements? None yet
  2. Land #3102724: Improve usability of core compatibility ranges on available updates report so we have the new code we need to make this work.
  3. Implement proposed resolution. See #21
  4. Add test coverage of new scenarios.
  5. Reviews.
  6. Figure out how the markup in the new 'Not compatible' table should work so the required changes are both in scope and acceptable to @lauriii See #85 and #3121769: Make the table of incompatible releases in UpdateManagerUpdateForm more theme-friendly
  7. RTBC
  8. Commit

User interface changes

Prototype from patch #21:

Screenshot of 'Update' form with 'Not compatible' table below the 'Manual updates required' table.

API changes

Nothing public. Introduces a new protected helper method in the UpdateManagerUpdate form class.

Data model changes

None.

Release notes snippet

The Update Manager provides an 'Update' tab in three locations in the administrative interface (/admin/reports/updates/update, /admin/modules/update and /admin/theme/update). This form allows highly privileged site administrators to install updated versions of contributed modules and themes that have new releases available. Previously, the form had no knowledge of what versions of Drupal core a given update is compatible with, and would try to install incompatible updates. Now, if the recommended version of a given extension is not compatible with the currently installed version of Drupal core, the update will appear in a new "Not compatible" table that shows what versions of core are required for each update.

CommentFileSizeAuthor
#121 3113992.120_121.interdiff.txt1.13 KBdww
#121 3113992-121.patch19.09 KBdww
#120 3113992.116_120.interdiff.txt1.68 KBdww
#120 3113992-120.patch19.73 KBdww
#118 update-interdiff-118.txt2.4 KBxjm
#118 update-3113992-118.patch19.03 KBxjm
#116 3113992.111_116.interdiff.txt3.48 KBdww
#116 3113992-116.patch19.1 KBdww
#111 3113992-111.patch18.4 KBtedbow
#111 interdiff-110-111.txt4.29 KBtedbow
#110 3113992.105_110.interdiff.txt2.68 KBdww
#110 3113992-110.patch18.48 KBdww
#108 interdiff-100-106.txt1.5 KBtedbow
#108 3113992-106-demo-do-not-commit.patch19.61 KBtedbow
#105 3113992.94_105.interdiff.txt799 bytesdww
#105 3113992-105.patch17.51 KBdww
#102 3113992-94.patch17.51 KBdww
#101 3113992-101.broken-94.should-fail-will-fail.patch20.09 KBdww
#101 3113992-101.broken-100.should-fail-will-pass.patch19.67 KBdww
#101 3113992-101.break-it.do-not-test.patch1.63 KBdww
#100 3113992.99_100.interdiff.txt799 bytesdww
#100 3113992-100.patch17.09 KBdww
#99 3113992.91_99.interdiff.txt1.35 KBdww
#99 3113992-99.patch17.09 KBdww
#94 3113992.91_94.interdiff.txt3.06 KBdww
#94 3113992.87_94.interdiff.txt2.56 KBdww
#94 3113992-94.patch17.51 KBdww
#91 3113992-91.patch17.03 KBtedbow
#91 interdiff-87-91.txt3.88 KBtedbow
#87 3113992.84_87.interdiff.txt1.26 KBdww
#87 3113992-87.patch17.51 KBdww
#84 interdiff_82-84.txt533 bytesMeenakshiG
#84 3113992-84.patch17.12 KBMeenakshiG
#82 3113992.81_82.interdiff.txt1.35 KBdww
#82 3113992-82.patch17.2 KBdww
#81 3113992.77_81.interdiff.txt1.27 KBdww
#81 3113992-81.patch17.2 KBdww
#77 3113992.77_faux_vs_real.interdiff.txt2.52 KBdww
#77 3113992.75_77.real-provider.interdiff.txt2.03 KBdww
#77 3113992.73_77.faux-provider.interdiff.txt2.04 KBdww
#77 3113992-77.real-provider.patch17.21 KBdww
#77 3113992-77.faux-provider.patch17.68 KBdww
#75 3113992.73_75.interdiff.txt2.7 KBdww
#75 3113992-75.patch17.17 KBdww
#74 interdiff-71-73.txt5.17 KBtedbow
#73 3113992.71_73.rawdiff.txt2.03 KBdww
#73 3113992-73.patch17.63 KBdww
#71 3113992.63_71.interdiff.txt1.05 KBdww
#71 3113992-71.patch17.64 KBdww
#69 3113992.68_69.interdiff.txt787 bytesdww
#69 3113992-69.patch16.69 KBdww
#68 3113992.67_68.interdiff.txt2.56 KBdww
#68 3113992-68.patch16.53 KBdww
#67 3113992.63_67.interdiff.txt2.89 KBdww
#67 3113992-67.patch17.18 KBdww
#63 3113992.59_63.interdiff.txt15.37 KBdww
#63 3113992-63.patch17.83 KBdww
#60 3113992.43_59.interdiff.txt4.11 KBdww
#60 3113992-59.patch8.17 KBdww
#43 3113992.42_43.interdiff.txt1.14 KBdww
#43 3113992-43.patch7.84 KBdww
#42 3113992.39_42.interdiff.txt7.08 KBdww
#42 3113992-42.patch7.84 KBdww
#39 3113992.36_39.interdiff.txt2.42 KBdww
#39 3113992-39.patch7.18 KBdww
#36 3113992-36.patch8.1 KBtedbow
#36 interdiff-34-36.txt5.97 KBtedbow
#34 3113992.32_34.interdiff.txt2.06 KBdww
#34 3113992-34.patch8.84 KBdww
#33 3101299.38_40.interdiff.txt854 bytesdww
#33 3101299-40.patch7.66 KBdww
#32 3113992-32.patch10.18 KBtedbow
#32 interdiff-21-32.txt7.89 KBtedbow
#21 3113992-21.not-compatible-below-manual-updates.png218.84 KBdww
#21 3113992.14_21.interdiff.txt1.21 KBdww
#21 3113992-21.do-not-test.patch3.57 KBdww
#14 3113992-14.not-compatible-table.png218.97 KBdww
#14 3113992.10_14.interdiff.txt3.1 KBdww
#14 3113992-14.do-not-test.patch3.76 KBdww
#10 3113992-10.unstyled-prototype.png205.08 KBdww
#10 3113992-10.do-not-test.patch2.75 KBdww
#2 3113992-2.update-page-with-incompatible-updates.png190.91 KBdww
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

dww created an issue. See original summary.

dww’s picture

Adding screenshot of the problem, and some brainstorming on possible solutions.

dww credited AaronMcHale.

dww credited benjifisher.

dww credited ckrina.

dww credited shaal.

dww credited webchick.

dww’s picture

Issue summary: View changes
Priority: Major » Critical
Related issues: +#2917600: update_fix_compatibility() puts sites into unrecoverable state

We discussed this on the UX call right this morning. Updates:

  • Adding #2917600: update_fix_compatibility() puts sites into unrecoverable state as related
  • Bumping to Critical
  • Yes to most of the things.
  • No to disabled checkbox.
  • We discussed removing rows entirely from the form for incompatible releases. We were concerned that folks wouldn't have any indication they're out of date if they don't visit the "Available updates" report instead, etc.
  • I suggested moving incompatible updates down to the other "Manual updates required" table, everyone liked that.
  • Re: Adding details elements, we agreed we shouldn't add noise for "Compatible", and only inject details for "Not compatible".
  • No to trying to be fancy (here) and figuring out at least 8.x-1.3 could be installed in this case. #NeedsFollowup

Updating proposed resolution accordingly.
Crediting folks from the meeting.

dww’s picture

Title: The 'Update' page has no idea that some updates are incompatible » [PP-1] The 'Update' page has no idea that some updates are incompatible
Issue summary: View changes
Status: Active » Postponed

Actually, getting this working is going to require code from #3102724: Improve usability of core compatibility ranges on available updates report so I'm calling this postponed for now. We really need to land that one first so we can easily re-use the details element and all the supporting logic.

dww’s picture

Here's a very quick proof-of-concept. Requires latest patches from #3102724: Improve usability of core compatibility ranges on available updates report, but given that, this is surprisingly easy to get working. ;)

Screenshot of an unstyled prototype of the 'Update' form with incompatible releases moved to the 'Manual updates required' table.

dww’s picture

I'm re-thinking if we actually want <details> for this form at all. We're only going to print the message if an update is moved into the 'Manual updates required' table, we presumably always want that message to be seen, and it would save some hassle if we're not trying to use <details> for it again. Perhaps we should just always show a big warning in that table, "Not compatible. Requires Drupal core: 8.8.0 to 8.8.2" or whatever ?

dww’s picture

Also, note that core's 1st column in the table says: "Drupal core (security update)"

So maybe we should re-use the (important thing) pattern and have redis look like this:

Redis (not compatible) 8.x-1.2 8.x-1.4 (release notes)
Requires Drupal core: 8.8.0 to 8.8.2
dww’s picture

Not really up to me, but given the #ux team meeting discussion that we have a UI to badly break your site, seems we need to fix this before 9.0.0-beta1. Tagging for now.

dww’s picture

#3102724: Improve usability of core compatibility ranges on available updates report has moved on since I posted #10. Here's a new version.

Also, based on a Slack chat in #ux channel, we're now leaning towards a whole separate table for this, instead of lumping it in with "Manual Updates Required".

dww’s picture

Issue summary: View changes

Actually, we're not done yet with "decide what to do", so I uncrossed that off and fleshed it out in the summary with these sub-points:

  1. Into what table should incompatible releases be moved? Either existing "Needs manual update" vs. new "Not compatible".
  2. If new, should it be above or below core's "Manual updates required"? Screenshot #14 shows above, but maybe below is better?
  3. If new, do we need a little intro sentence / description? If so, what exactly should it say?
  4. Any other tweaks / concerns / improvements?
  5. ...

Re: 3: For now, I vote no. Less words is almost always better UI. But we could easily add it if folks think it's needed.

I'd like to have this patch as close to RTBC as possible when the parent issue lands, so the sooner we can sort out all the "Decide" tasks, the sooner we'll have this ready.

Thanks!
-Derek

klonos’s picture

You could re-label the button to something like "Download selected updates". Then also have it so that any incompatible updates either do not have a checkbox next to them, or their checkbox is unticked+locked, or replace the checkbox with a red x. This would allow to keep the updates in a single table, which would simplify the UI.

Other things to consider if this ^^ works:

  • also merging the manual updates in the same table - having a single table and a single submit button at the bottom of the page would improve UI consistency (no additional tables after the submit button)
  • denoting incompatible rows with something like different background color and/or text color
kualee’s picture

Re:2 in #15 if we do stick to having separate table, I reckon having it under core's "Manual updates required" is more logically, as in term of relevance, modules that can be manually updated are far more relevant than those are are incompatible. Taking the order of tables as order of "importance" or "action-ability", it makes a lot more sense for that new table to stick to the bottom. Just my 2cents

mandclu’s picture

For the cases where some extra explanation might be helpful, could it follow the model of the webform module's interface, where it commonly uses a question mark symbol you can hover over for additional help text? I like this model because experienced users get a nice clean interface, but less experienced users have the additional help text easily accessible.

dww’s picture

Re: #17:
- Having disabled checkboxes is almost always a UI WTF.
- Using color could be nice, but it can't be the only indication something is incompatible or it's an accessibility bug.
- I'm not sure that trying to lump everything into a single table would actually improve the UI. It'd make it more complicated in a lot of ways. Separate tables for separate projects in different states means that each table can be processed by the site admin independently. Sadly, the UI can't help you with anything that's not compatible or core itself, so those are basically just here for informational purposes.

Re: #18: Yeah, I agree that the 'Not compatible' table should probably go below the core "Manual updates" table. Sadly, that table is only ever used for core, and only ever has a single row in it. ;) That could potentially be cleaned up in a follow-up (but not here).

Re: #19: Hover / tooltips are usually very hard to make accessible. I'm a big -1 to trying to use that approach here. Honestly, this entire UI is for "less experienced" users. Experienced users are probably using composer to manage their sites, not this UI. So I'd rather not try to "simplify" this for "power users".

Thanks,
-Derek

dww’s picture

Patch + screenshot with 'Not compatible' table below 'Manual updates'.

I vote for this, but it's not just up to me. There's a UX team meeting tomorrow morning, and I hope they'll make some final decisions on all this. Stay tuned.

Thanks,
-Derek

dww’s picture

p.s. Re: separate tables vs. a single table, I will say that the screenshots demonstrate that either we should have everything in a single table (which I oppose as stated above), or we should consider some CSS to make the widths of the table columns more consistent across tables. As-is, it's pretty jarring and hard to scan. :(

benjifisher’s picture

Title: [PP-1] The 'Update' page has no idea that some updates are incompatible » The 'Update' page has no idea that some updates are incompatible
Issue summary: View changes
Status: Postponed » Needs work

Since #3102724: Improve usability of core compatibility ranges on available updates report was committed to 8.9.x and 9.0.x, we can un-postpone this issue. I am setting the status to NW and updating the title.

#8 mentions last week's meeting of the Usability team, and we discussed this issue again at today' meeting. Recordings (on YoiuTube):

We generally agree with the responses in #20 to some of the earlier suggestions. In particular, color and how to present the table description are up to the admin theme, so they are out of scope here.

We like the approach in #21:

  • Separate table
  • Below the "Manual updates" table
  • No additional description

I am updating the "Remaining tasks" to match.

In particular, leaving off the description gives us flexibility for future enhancements. For example, we may find that a particular update has a new module or composer dependency that has to be installed. Then we can add appropriate text to the row without further changing the title/description of the table.

I will create a follow-up issue to make the cell widths more consistent.

webchick’s picture

Just one additional bullet on benji's excellent summary...

The reason "why" for the order shown in #21 is because it's both a) what chronologically makes sense (have to update core in order to not have versions incompatible with core), and b) there might potentially be 5-6 things in that "not compatible" list, and this puts the necessary core update's visibility higher in the list so it doesn't get lost.

kualee’s picture

Title: The 'Update' page has no idea that some updates are incompatible » [PP-1] The 'Update' page has no idea that some updates are incompatible
Issue summary: View changes
Status: Needs work » Postponed

Thanks for your work @dww. I reckon we also need a quick summary/description under the table heading to add on to what actions could be taken by the site admin. As you mentioned, this screen is more for less experienced users, so some guidance/explanation here would help/don't send them into panic mode.
#20 I agreed, it definitely needs a follow-up issue to clean up, a table with one row is not very useful, and also taking up a fair bit of space too.

kualee’s picture

Title: [PP-1] The 'Update' page has no idea that some updates are incompatible » The 'Update' page has no idea that some updates are incompatible
Status: Postponed » Needs work

Didn't mean to change the title and status, my google chrome restarted and was carrying data from pre#23 (???)

dww’s picture

Issue summary: View changes
Issue tags: +Needs tests

Restoring the changes to the summary that were overwritten via #25.
Also doing some additional updates to the summary.
All that remains here is "Add test coverage of new scenarios.", so tagging for 'Needs tests'.

It looks like we have 0 test coverage of this form yet. :(

I wonder if it's desirable to extend the existing tests of the available updates report (that have all the fixtures we care about, etc) to also load this form and ensure the form looks like we want it to. Or perhaps it's better to add entirely new dedicated tests for this? Part of me doesn't want to further complicate the existing tests. The other side doesn't want to incur the cost of re-installing Drupal N more times for Functional tests of this form for N different update scenarios. We're already incurring all the cost of re-installing Drupal and setting up the fixtures -- so it'd be negligible test execution time to load another page in each scenario and run another set of assertions. I'll ping @tedbow about this and get their input before we proceed.

Thanks,
-Derek

dww’s picture

Issue summary: View changes

Also removing the screenshot of patch #14 from the summary, since we decided we want the order of tables from #21.

tedbow’s picture

re #27I think it would make sense to extend \Drupal\Tests\update\Functional\UpdateContribTest::testCoreCompatibilityMessage() to handle also checking admin/modules/update. Might be able to leave that method untouched actually and just update \Drupal\Tests\update\Functional\UpdateContribTest::assertCoreCompatibilityMessage() which it uses.

testCoreCompatibilityMessage() already has test cases for compatible and NOt compatible modules so we just need to load admin/modules/update and make sure they show in the correct table and with the correct message if not compatible.

dww’s picture

Re: #29 - great, yeah, that's pretty much exactly what I had in mind.

To manage scope, I spun off #3117229: Expand test coverage of Drupal\update\Form\UpdateManagerUpdate for the bigger problem that we have 0 test coverage of this form.

Thanks,
-Derek

tedbow’s picture

Assigned: Unassigned » tedbow

I chatted with @dww and I was going to add the test coverage as described in #29

tedbow’s picture

Assigned: tedbow » Unassigned
Status: Needs work » Needs review
FileSize
7.89 KB
10.18 KB
  1. +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
    @@ -196,10 +196,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    -      // Drupal core needs to be upgraded manually.
    -      $needs_manual = $project['project_type'] == 'core';
    +      // Drupal core always needs to be upgraded manually.
    +      $needs_manual = $project['project_type'] === 'core';
    

    Although this seem like good change it is out scope for this issue.

  2. +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
    @@ -196,10 +196,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +      $not_compatible = $recommended_release['core_compatible'] === FALSE;
    

    If will give a warning for the core project itself because the key is not set. Fixed

  3. Added the tests.
dww’s picture

Excellent, thanks for the fixes and the test coverage! Removing that tag and updating summary.

Minor nit with the tests... seems unfortunate to sprinkle calls to:

$this->drupalGet('admin/reports/updates');

before some (but not all) of the calls to assertCoreCompatibilityMessage().

I understand that refreshUpdateStatus() already loads that page, so it's a bit wasteful, but it seems better and less error-prone to maintain if assertCoreCompatibilityMessage() is doing targeted assertions for different pages that it's responsible to load the pages it's asserting about in all cases. So instead of it assuming you've loaded the right page first, it should just always do the drupalGet() it's assuming as context. Like so.

dww’s picture

Sorry, utterly wrong patch + interdiff. :( Try this.

Status: Needs review » Needs work

The last submitted patch, 34: 3113992-34.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review
FileSize
5.97 KB
8.1 KB
  1. re #32
    +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -807,30 +810,57 @@ public function testUnsupportedRelease() {
    +    if (in_array($expected_release_title, ['Recommended version:', 'Security update:'])) {
    

    When I added this if here I missed that this actually will remove the existing test coverage for all other types of updates, such as latest version and "also available".

    Adding this for just the new parts of the test as the form at /admin/module/updates doesn't show all types of updates.

    Also returning early now if it is not in this list instead.

  2. re #33 this change is better and more readable. I agree the extra page request is made up for by an easier to maintain and understandable test.
  3. I think the fail in #34 was an unrelated random fail. It passes locally for me. We will see with this patch
xjm’s picture

Issue tags: +beta target
dww’s picture

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

Great catch in #36 on not blasting the existing test coverage for those other cases. ;)
Glad you agree on #33.
Yeah, #34 was random fail, indeed.

Re: #36:
A few questions, a few nits, a few out-of-scope changes, and one unnecessary page load:

  1. +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
    @@ -210,31 +214,37 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +        else {
    

    To be really defensive in case we add something else to the outer if(), should this be elseif ($not_compatible) instead?

  2. +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
    @@ -297,6 +307,17 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +      $prefix = '<h2>' . $this->t('Not compatible') . '</h2>';
    ...
    +        '#prefix' => $prefix,
    

    This was copy/pasted from a code block with a conditional prefix. In this case, the prefix is always the same. Seems we should just insert it directly via #prefix and not have the $prefix variable.

  3. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -807,16 +810,21 @@ public function testUnsupportedRelease() {
    -      // it should have a download link and the details element should be closed
    -      // by default.
    +      // it should have a download link and the details element should be
    +      // closed by default.
    

    This seems both out of scope and unnecessary. 'closed' just barely fit at 80 chars in the original.

  4. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -826,12 +834,41 @@ protected function assertCoreCompatibilityMessage($version, $expected_range, $ex
    -      // core, it should not have a download link and the details element should
    -      // be open by default.
    +      // core, it should not have a download link and the details element
    +      // should be open by default.
    

    And here.

  5. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -826,12 +834,41 @@ protected function assertCoreCompatibilityMessage($version, $expected_range, $ex
    +      // 'admin/modules/update'.
    ...
    +    $this->drupalGet('admin/modules/update');
    

    Maybe it doesn't matter, but the exact same form is available at 3 different paths:
    admin/modules/update
    admin/theme/update
    admin/reports/updates/update

    That's a bit of a UI-WTF (and resolving it is of course completely out of scope). But I wonder if it'd be cleaner to have this test pointing at admin/reports/updates/update instead of doing something module-specific...

  6. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -826,12 +834,41 @@ protected function assertCoreCompatibilityMessage($version, $expected_range, $ex
    +      $this->drupalGet('admin/modules/update');
    

    This one is an unnecessary page load (inside the not_compatible else). We already loaded the right page after the early return.

To keep this moving forward, I'll quickly re-roll to address these. Stay tuned.

dww’s picture

Assigned: dww » Unassigned
Status: Needs work » Needs review
FileSize
7.18 KB
2.42 KB

Changes for #38:
1. Ignored for now. Probably should stay as-is from #36. TBD.
2. fixed
3. fixed
4. fixed
5. left it using admin/modules/update for now. TBD.
6. fixed

benjifisher’s picture

Status: Needs review » Needs work

This looks very close. I just have a few requests for making the code a little more readable.

  1. +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
    @@ -199,7 +199,11 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    ...
    +      $not_compatible = isset($recommended_release['core_compatible']) && $recommended_release['core_compatible'] === FALSE;
        

    In ProjectCoreCompatibility.php, I see

            $release['core_compatible'] = $this->isCoreCompatible($release['core_compatibility']);
        

    If this key is set, then the value is already boolean. I think it would be simpler to avoid double negatives and do it this way:

          $compatible = $recommended_release['core_compatible'] ?? TRUE;
        

    and then use !$compatible instead of !$not_compatible later. Or you could name the variable $is_compatible, as in the test.

    Sorry for the nit, but at least it shows that I am reviewing carefully! ;)

  2. +      if ($needs_manual || $not_compatible) {
    ...
    +        if ($needs_manual) {
    +          $projects['manual'][$name] = $entry;
    +        }
    +        else {
    ...
    +        }
           }
           else {
        

    The nested conditionals are hard to follow. It would be clearer to structure this as

    +      if ($needs_manual) {
    ...
    +      elseif (!$compatible) {
    ...
    +      }
           else {
        

    In order to be DRY, we could refactor the lines that restructure the render array for table instead of tableselect into a helper function. I consider that a down payment on refactoring the buildForm() method. With the current patch, that method runs to 252 lines, including 143 in a foreach loop.

    If we do this, then I think there is less need for the suggestion in #36.1.

  3. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -555,6 +555,9 @@ public function testHookUpdateStatusAlter() {
    ...
       public function testCoreCompatibilityMessage() {
    +    $update_admin_user = $this->drupalCreateUser(['administer site configuration', 'administer software updates']);
    +    $this->drupalLogin($update_admin_user);
        

    I guess we need this because this method calls assertCoreCompatibilityMessage(), and that method now loads a more restricted page.

  4. @@ -807,10 +810,15 @@ public function testUnsupportedRelease() {
        *   If the update is compatible with the installed version of Drupal.
        */
       protected function assertCoreCompatibilityMessage($version, $expected_range, $expected_release_title, $is_compatible = TRUE) {
    +    $this->drupalGet('admin/reports/updates');
    +    $assert_session = $this->assertSession();
    +    $expected_range_text = "Requires Drupal core: $expected_range";
    +    $compatible_table_locator = '[data-drupal-selector="edit-projects"]';
    +    $incompatible_table_locator = '[data-drupal-selector="edit-not-compatible"]';
        

    Most of these variables are not used until after the second drupalGet(). I suggest starting the function by defining $expected_range_text, since that is used on both pages. Then skip a line and do the tests on the first page. Then skip a line, define $assertSession and the locator strings, and test the second page. Consider replacing the several "Confirm ..." comments with a single, longer comment after the empty line. Related: do we often use "Confirm" in such comments? Maybe we can leave it out entirely ("There is no table ...") or replace it with "Assert".

  5. +1 for the suggestion in #36.5.
dww’s picture

Assigned: Unassigned » dww

Thanks, @benjifisher! I'll work on your excellent points...

dww’s picture

Assigned: dww » Unassigned
Status: Needs work » Needs review
FileSize
7.84 KB
7.08 KB

This should fix everything in #40, including #36.5. Took a little while since I ran tests locally to confirm UpdateContribTest is still passing after the refactorings and changes.

dww’s picture

FileSize
7.84 KB
1.14 KB

Bah, some newline funkiness crept in. This is better.

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

The feedback in #40 was excellent, and mirrored my own thinking as I reviewed the previous patch. The new if/elseif/else structure is much clearer.
Thanks!

benjifisher’s picture

Issue summary: View changes

(cross post. TL;DR +1 for RTBC)

@dww:

Thanks for those updates. I think this is ready to go.

In addition to reviewing the patch in #43, I did one last manual test: download and extract tarballs for redis-8.x-1.2 and token-8.x-1.5; hack Drupal.php to pretend the version is 8.7.10. My Update form looks a lot like the screenshot in the issue summary.

The refactoring is a step in the right direction. We removed 10 lines from the foreach loop that I mentioned in #40. The new helper function (not counting the doc block) is a little less than that. Coincidentally, the net effect compared to HEAD is no change to the number of lines in the foreach loop; we did add lines to buildForm() overall, because of the new block at the end of that function.

alexpott’s picture

+++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
@@ -199,42 +199,42 @@ public function buildForm(array $form, FormStateInterface $form_state) {
+      // If a contributed project is not compatible with the currently installed
+      // version of core, list that project in a separate table, too.
+      $compatible = $recommended_release['core_compatible'] ?? TRUE;

This raised an eyebrow. Since if $recommended_release['core_compatible'] = NULL the $compatible will equal TRUE. This is the opposite of the behaviour in template_preprocess_update_version() which does $core_compatible = !empty($version['core_compatible']); BUT it is the same as the behaviour in core/themes/stable/templates/admin/update-version.html.twig which does {% if version.core_compatible is not defined or version.core_compatible %}

Do we have test coverage when 'core_compatible' is missing?

benjifisher’s picture

@alexpott:

I think the code you refer to was all added in #3102724: Improve usability of core compatibility ranges on available updates report. I reviewed that issue, but I may have neglected the templates and preprocess functions a bit.

The code we add in this issue does what we want. If the key is not present, we want the default assumption to be that the project is compatible.


The comment I made in #40.1 of this issue also applies to the preprocess function. That is, we should simply have $core_compatible = $version['core_compatible']; because of the early exit in the preprocess function:

function template_preprocess_update_version(array &$variables) {
  $version = $variables['version'];
  if (empty($version['core_compatibility_message'])) {
    return;
  }
  $core_compatible = !empty($version['core_compatible']);
  // ...

so we never get to the line in question unless $version['core_compatibility_message'] is set. But in ProjectCoreCompatibility.php (one more line of context than I gave in #40.1) we have

        $release['core_compatible'] = $this->isCoreCompatible($release['core_compatibility']);
        $release['core_compatibility_message'] = $this->createMessageFromCoreCompatibility($release['core_compatibility']);

so those two keys are either both set or neither is.


Do we have test coverage when 'core_compatible' is missing?

I am not sure what you want to test.

Note the doc block of update-version.html.twig:

 * Available variables:
...
 * - version:  A list of data about the latest released version, containing:
...
 *   - core_compatible: A flag indicating whether the project is compatible
 *     with the currently installed version of Drupal core. This flag is not
 *     set for the Drupal core project itself.

I am pretty sure that we have test coverage of this template for project = Drupal core. But that is out of scope for this issue, so probably that is not what you are looking for.

dww’s picture

@alexpott re: #46: +1 to @benjifisher's explanation in #47. We could open a follow-up to change template_preprocess_update_version() to assume compatible if not otherwise specified, just to be more consistent with the other parts of core. But as explained, it doesn't actually matter there, since we only hit that code if we know we've got real values to work with and it'll never be NULL. /shrug

TL;DR: Still RTBC. ;)

xjm’s picture

Assigned: Unassigned » xjm

Reviewing this now.

xjm’s picture

Hiding some files since the IS formatting has made the last patch difficult to locate.

Meanwhile, crediting reviewers for helpful feedback, which turns out to be everyone but me. Were there any participants from the UX meeting in #23 who aren't included from the original credits added for #8?

xjm credited worldlinemine.

xjm’s picture

Adding credit for @worldlinemine based on @benjifisher's summary of who was at the UX meeting. (Thanks Benji!)

xjm credited Gábor Hojtsy.

xjm’s picture

Oh, and Gábor.

xjm’s picture

OK, that was a dense little 8 K patch. Note that I haven't read other reviews, #40, or any of the recent discussion yet -- I usually deliberately read the code first after the IS so that I don't bias my own review.

  1. --- a/core/modules/update/src/Form/UpdateManagerUpdate.php
    +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
    
    @@ -199,42 +199,42 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +      // If a contributed project is not compatible with the currently installed
    +      // version of core, list that project in a separate table, too.
    +      $compatible = $recommended_release['core_compatible'] ?? TRUE;
    +
    

    This is one of those "shorter is not always better" situations IMO. The comment also doesn't actually explain what's happening on the following line.

    What's happening on the following line, reading out the null coalescing operator in words, is: If a core compatibility flag is set for the recommended release, then set $compatible to that; otherwise, set it to TRUE.

    Which is... code smell. Null value leads to very un-null Boolean value. "Yeah, having nothing there means it's compatible". For an undocumented magic key in the release data. ("Magic key" is just how things work because all this code dates from ArrayPI days; "undocumented" is perhaps out of scope for this patch.) But, at the least, we need a better inline comment justifying this. And tests for all three of TRUE, FALSE, and NULL logics.

  2. +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
    @@ -199,42 +199,42 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +          $entry['data']['recommended_version']['data']['#template'] .= ' <div>{{ core_compatibility_message }}</div>';
    +          $entry['data']['recommended_version']['data']['#context']['core_compatibility_message'] = $recommended_release['core_compatibility_message'];
    

    Why're we sticking it in a classless child div? Some divitis up in here.

  3. +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
    @@ -199,42 +199,42 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    -      }
    -
    -      // Based on what kind of project this is, save the entry into the
    -      // appropriate subarray.
    -      switch ($project['project_type']) {
    -        case 'core':
    -          // Core needs manual updates at this time.
    -          $projects['manual'][$name] = $entry;
    -          break;
     
    -        case 'module':
    -        case 'theme':
    -          $projects['enabled'][$name] = $entry;
    -          break;
    -
    -        case 'module-disabled':
    -        case 'theme-disabled':
    -          $projects['disabled'][$name] = $entry;
    -          break;
    +        // Based on what kind of project this is, save the entry into the
    +        // appropriate subarray.
    +        switch ($project['project_type']) {
    +          case 'module':
    +          case 'theme':
    +            $projects['enabled'][$name] = $entry;
    +            break;
    +
    +          case 'module-disabled':
    +          case 'theme-disabled':
    +            $projects['disabled'][$name] = $entry;
    +            break;
    +        }
    

    Uhhh, what is going on with the diff here? As far as I can see, the only change is that the core case is being removed. Which... why? And unrelatedly, why is git detecting it as this massive change? Is there a different difference I missed?

  4. +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
    @@ -297,10 +297,40 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +    if (!empty($projects['not-compatible'])) {
    +      $form['not_compatible'] = [
    +        '#type' => 'table',
    +        '#header' => $headers,
    +        '#rows' => $projects['not-compatible'],
    +        '#prefix' => '<h2>' . $this->t('Not compatible') . '</h2>',
    +        '#weight' => 150,
    +      ];
    

    This is the core err, central change of the patch.

  5. +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
    @@ -297,10 +297,40 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +   * Prepares a row entry for use in a regular table, not a 'tableselect'.
    +   *
    +   * There are no checkboxes in the 'Manual updates' or 'Not compatible' tables
    +   * so they will be rendered by '#theme' => 'table', not 'tableselect'. Since
    +   * the data formats are incompatible, this method converts to the format
    +   * expected by '#theme' => 'table'.
    

    There's a missing comma after "tables" here, but this is just being factored into a method from stuff that used to be inline, so that's at best scope-adjacent.

    That said, the one-line summary of this method is kinda bizarre. Out of context, it's totally unclear why we would care about or expect tableselects. I think we need to add something that says something like, "The data in the $foo array is primarily used by Bar::method() for baz_purpose, so it includes a checkbox element to perform qux_operation."

    Without having read the whole form builder, I'm guessing baz and qux are "In the main update table, where a site owner can select which updates to install."

  6. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -555,6 +555,9 @@ public function testHookUpdateStatusAlter() {
        * Tests that core compatibility messages are displayed.
        */
       public function testCoreCompatibilityMessage() {
    ...
         $system_info = [
           '#all' => [
             'version' => '8.0.0',
    

    Hmm. No data provider. But is that maybe a faux-data-provider?

  7. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -555,6 +555,9 @@ public function testHookUpdateStatusAlter() {
    +    $update_admin_user = $this->drupalCreateUser(['administer site configuration', 'administer software updates']);
    +    $this->drupalLogin($update_admin_user);
    

    And why didn't we need to add permissions before? Surely the update status report is restricted?

  8. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -807,10 +810,13 @@ public function testUnsupportedRelease() {
    +    $expected_range_text = "Requires Drupal core: $expected_range";
    ...
    -    $this->assertContains("Requires Drupal core: $expected_range", $compatibility_details->getText());
    +    $this->assertContains($expected_range_text, $compatibility_details->getText());
    
    @@ -832,6 +838,34 @@ protected function assertCoreCompatibilityMessage($version, $expected_range, $ex
    +      $assert_session->elementTextContains('css', "$incompatible_table_locator tbody tr", $expected_range_text);
    

    Instead of defining $expected_range_text, I would assert Requires Drupal core: $expected_range in both cases. It's usually better for assertions to assert static things, except where they're varying with a data provider or parameter (which $expected_range itself does, but not the "Requires Drupal core:" part), or where they're verbose such that they they make the code harder to read (as with the CSS selectors). In this case, defining the additional variable is actually making the assertion harder to read.

    So, I would undo the change in the first half of the method, remove the variable declaration, and just repeat Requires Drupal core: in the new assertion.

  9. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -807,10 +810,13 @@ public function testUnsupportedRelease() {
    +    $this->drupalGet('admin/reports/updates');
    

    ...And why didn't it do this before?

  10. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -832,6 +838,34 @@ protected function assertCoreCompatibilityMessage($version, $expected_range, $ex
    +    $this->drupalGet('admin/reports/updates/update');
    

    Okay, this is... kind of weird. We're testing two different pages from within one assertion helper?

  11. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -832,6 +838,34 @@ protected function assertCoreCompatibilityMessage($version, $expected_range, $ex
    +    if ($is_compatible) {
    ...
    +    else {
    

    The conditional is also not what I typically expect; hoping this is getting called from something with a data provider.

  12. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -832,6 +838,34 @@ protected function assertCoreCompatibilityMessage($version, $expected_range, $ex
    +      $assert_session->elementsCount('css', "$compatible_table_locator tbody tr", 1);
    ...
    +      $assert_session->elementsCount('css', "$incompatible_table_locator tbody tr", 1);
    

    Hmm, hardcoding that there's only one table row makes me wonder about the extent of the test coverage here, and whether there should be more for more combinations.

  13. +++ b/core/modules/update/tests/src/Functional/UpdateContribTest.php
    @@ -832,6 +838,34 @@ protected function assertCoreCompatibilityMessage($version, $expected_range, $ex
    +      $assert_session->elementTextNotContains('css', $compatible_table_locator, 'Requires Drupal core');
    

    I like that this assertion is broader: If there were a bug or difference with the core version number and we asserted the whole thing, this could give a false positive.

Next I'm going to read the other reviews/comments. This is probably NW, but reading what others have said first.

xjm’s picture

Assigned: xjm » Unassigned
Status: Reviewed & tested by the community » Needs work

OK, read #38 on which seemed to be the most relevant to the current patch.

  1. For #40.1 / #46 / #55.1, two committers have WTFed at the line and asked for more test coverage. So let's fix the inline comment to reference the bit from ProjectCoreCompatibility that @benjifisher explained, and probably expand the test coverage.

    And actually based on #46 / #47 / #48 the situation is kinda worse than the patch alone indicates.

  2. Regarding this in #47:

    The code we add in this issue does what we want. If the key is not present, we want the default assumption to be that the project is compatible.

    But why do we want that? We need to justify it (in the inline comment, if not just by refactoring it to be clearer).

  3. #43.3 partly answers #55.7, but not entirely. While it makes sense for the page that lets you download code to the filesystem (or the next thing to it) to have the absolute highest permission restrictions, the admin status report should still have some permission requirement, so I'm still confused there. (It might make more sense once I read the whole test, which I'll do tomorrow.

Next I'm going to read both the test and the form builder with and without the patch applied to get more context for a few of my questions in #55, but that'll have to wait until tomorrow since it's late. Un-assigning for now, and NW for at least #55.1 (inline comment and test coverage, at a minimum, even if we scope refactoring things to followups), .5, and .8, as well as any other points for which someone has a fix.

dww’s picture

Assigned: Unassigned » dww

@xjm: Thanks for the careful review.

1. Sure, a better comment is in order.

2. Because we want it on a separate line from the recommended version + release notes link. This whole thing is weird with the inline twig stuff. Completely re-doing it seemed like scope creep. A <div> seemed like the simplest, easiest solution.

3. It’s now indented 2 chars since it’s now inside the else. Core’s removed since that’s now the if():

     if ($needs_manual) {
        $this->removeCheckboxFromRow($entry);
        $projects['manual'][$name] = $entry;
      }
      elseif (!$compatible) {
        ...
      }
      else {
        $form['project_downloads'][$name] = [
          '#type' => 'value',
          '#value' => $recommended_release['download_link'],
        ];

        // Based on what kind of project this is, save the entry into the
        // appropriate subarray.
        switch ($project['project_type']) {
          ...
      }

4. 👍

5. Sure, I can write a better comment for this, too.

6. Are you looking at the same patch? ;) None of that changed in this issue. No, that’s not a faux data provider. It’s how all the update module tests work. Here’s the full context:

    $system_info = [
      '#all' => [
        'version' => '8.0.0',
      ],
      'aaa_update_test' => [
        'project' => 'aaa_update_test',
        'version' => '8.x-1.0',
        'hidden' => FALSE,
      ],
    ];
    $this->config('update_test.settings')->set('system_info', $system_info)->save();

7. Available updates report is restricted, but the form we’re testing here, that lets you install new PHP code on your site, requires "administer software updates”.

8. /shrug. Ok, personal preference.

9. See #33 and others.

10. See #33 and others.

11. Yes, called from testCoreCompatibilityMessage(), which was added elsewhere. Seems like massive scope creep in your review here.

12. We’re explicitly testing with a single contrib. See the $system_info I posted above in answer to 6. We load both reports multiple times in multiple scenarios to make sure the single contrib is handled properly in each case. Sounds like you’re asking for all of #3117229: Expand test coverage of Drupal\update\Form\UpdateManagerUpdate to be done in here? Yes, this form has 0 test coverage right now. We’re adding test coverage to handle the scope of this issue. Adding extensive coverage of the whole form is exactly what 3117229 is for.

13. 👍

dww’s picture

Re: #56.3:

  protected function setUp() {
    parent::setUp();
    $admin_user = $this->drupalCreateUser(['administer site configuration']);
    $this->drupalLogin($admin_user);
  }
xjm’s picture

Thanks @dww.

I think we might need FEFM feedback on the markup for the markup/themeability choice in #55.2; I'll ask about it tomorrow.

Sorry about the confusion for #55.6; I read patches bottom-up so my comments are out of order because Dreditor orders them top-down, not by timestamp. So I first said I hoped there was a data provider because of the if/else, and then looked for it and didn't find it.

#55.8 isn't a personal preference BTW; it's a testing best practice for core. There's a reasonable chance other committers would have given the same feedback.

#33 didn't really clear anything up for me, but hopefully reading the whole test will.

Re: point 11, asking for more test coverage is not scope creep. It's asking for more test coverage. And for point 12, tests are a gate; while we don't have to test the whole form here (the other two tables), we should test the possible scenarios for the part that we are changing, including at least two values and a "one of each" case.

dww’s picture

Assigned: dww » Unassigned
Status: Needs work » Needs review
FileSize
8.17 KB
4.11 KB

TL;DR re: #55:
Fixed 1, 5 and 8.
Answered everything else in #57 and #58.

dww’s picture

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

#60 was a x-post with #59, so I didn't see any of that when I wrote the patch or posted the comment (and why the patch # doesn't match the comment).

#55.11 said:

The conditional is also not what I typically expect; hoping this is getting called from something with a data provider.

I answered in #57.11:

Yes, called from testCoreCompatibilityMessage(), which was added elsewhere. Seems like massive scope creep in your review here.

You seemed to be reviewing everything near the lines this issue is touching, not the patch itself. That was my concern. You seem to be upset about the test coverage and methodology introduced in #3102724: Improve usability of core compatibility ranges on available updates report, which is why I said it seemed like "massive scope creep in your review". You didn't actually ask for more test coverage for this issue.

#55.12 wasn't very clear about what additional test coverage you're expecting. #59 is clear:

we should test the possible scenarios for the part that we are changing, including at least two values and a "one of each" case.

So I'll work on that now.

dww’s picture

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

Wow, I started down this path, and it's a huge mess to add this additional test coverage, given the state of the rest of the update test code. All sorts of things assume there's only a single contrib being tested. I'm not going to re-write dozens (hundreds?) of lines of test plumbing for #59.

Opened #3121040: Add Update manager test coverage involving multiple contribs instead.

If you want to block this critical bug fix on that issue, it's your discretion as the release manager.

I think we have sufficient test coverage in here (not to mention all the manual testing I, @benjifisher and others have done) to prove this works as intended. Getting our test plumbing up to snuff is going to take significantly more effort, and I really don't want to see us keep shipping versions of core that have this glaring bug that we've already fixed here. We have #3121040 and #3117229: Expand test coverage of Drupal\update\Form\UpdateManagerUpdate for follow-ups to expand the test coverage.

dww’s picture

FileSize
17.83 KB
15.37 KB

After being away from this for a while, I saw it in a new light. At the risk of derailing this (but in the hopes this approach is more pleasing to @xjm and could be committed in time for 9.0.0-beta1), here's an attempt at better test coverage for this bug. Instead of trying to re-use much of anything, start over with a totally new test class for this form. This basically ignores and invalidates all the patches and most of the reviews since #29. But it should solve #59 and render obsolete or solve #55.6-13.

Pros

  1. Possible to test multiple contribs without doing a bunch of refactoring and changing existing tests at #3121040: Add Update manager test coverage involving multiple contribs
  2. Easier to document what the new tests are doing.
  3. Less confusion about test methods loading different pages and asserting different things.
  4. Clean slate.
  5. Lays ground-work for #3117229: Expand test coverage of Drupal\update\Form\UpdateManagerUpdate

Cons

  1. Duplicates some stuff from UpdateContribTest and sort of violates "Don't Repeat Yourself".
  2. More expensive to run since it's a whole new test class to setup, install Drupal, etc.
  3. Much larger patch to review.

p.s. I should add, although the inline comments don't spell this out, the new test coverage here means we're hitting all 3 cases for #55.1: TRUE (aaa_update_test 8.x-1.2), FALSE (bbb_update_test 8.x-1.2), and NULL treated as TRUE (bbb_update_test 8.x-1.1).

xjm’s picture

Thanks @dww! This looks really promising; will start a review once @tedbow has had a chance to look at it.

Not that I didn't commit #3102724: Improve usability of core compatibility ranges on available updates report, but when the test was added there, it covered the test scenarios that were important for that issue and that page.. However, this test was adding a conditional if/else in the assertion helper, variables checked in assertions, etc., which are warning flags because it can mean we're testing the test (in a way that might give false positives or hide regressions in the future). So either asserting static things, or assuring all the tested cases are covered by a data provider, is usually the best way to go. Plus that weird null coalesce we've talked a lot about; glad to see #63 should provide coverage for it.

tedbow’s picture

+++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
@@ -199,42 +199,49 @@ public function buildForm(array $form, FormStateInterface $form_state) {
+      // If the recommended release for a contributed project is not compatible
+      // with the currently installed version of core, list that project in a
+      // separate table. To determine if the releases is compatible, we inspect
+      // the 'core_compatible' key from the release info array. If it's not
+      // defined, we're dealing with a release from a project that doesn't have
+      // any version-specific core compatibility requirements, so we have to
+      // assume it is compatible. This ensures backwards compatibility for
+      // projects that don't yet define 'core_version_requirement' and are
+      // relying on 'core'.

This comment is slightly wrong but is because of some complications from a few different areas. I just happened to work on some of the related issues.

core_version_requirement doesn't have to be set in a projects info.yml file for $recommended_release['core_compatible'] to be set.

In drupal.org update feeds core_compatibility is set based core_version_requirement if it is set but falls back to core if it is not.

You can see this in the Webform XML feed. Search for 8.x-5.0-rc31 and see that it has 8.x. The info.yml file for that release does not have core_version_requirements set. Because in semver(at least composer's version) this means it compatible with all versions of 8.

So in \Drupal\update\ProjectCoreCompatibility::setReleaseMessage() where we

if (!empty($release['core_compatibility'])) {
        $release['core_compatible'] = $this->isCoreCompatible($release['core_compatibility']);
        $release['core_compatibility_message'] = $this->createMessageFromCoreCompatibility($release['core_compatibility']);
      }

This will almost always be set. The only case it would not be set is
1) If there was problem fetching the drupal core project's XML but not another project's XML because it we can't determine available versions of core we won't set this. This seems unlikely but of course could happen
2) If an alternative update server was being used. Not likely but possible. see #3120655: Determine if there are other update servers besides updates.drupal.org, if not deprecate the ability to have different servers.

I will review the rest of this after breakfast

tedbow’s picture

Status: Needs review » Needs work
+++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
@@ -199,42 +199,42 @@ public function buildForm(array $form, FormStateInterface $form_state) {
-        case 'core':
-          // Core needs manual updates at this time.
-          $projects['manual'][$name] = $entry;
-          break;

This is an unrelated change. Though I agree it may be an improvement we should try make as few changes as possible. I looked this patch locally also ignoring whitespace. I know we have this big section in the diff because this switch block is now in indented. So I am aware reverting this change will not make the actually patch file smaller it is a change we don't need to make.

This case is removed regardless of indentation. But nothing related to this has changed to make us have to make this change.

In the patch we are now setting this above via


if ($needs_manual) {
  $this->removeCheckboxFromRow($entry);
  $projects['manual'][$name] = $entry;
}

This if statement was already present just some of the code was refactor and put into removeCheckboxFromRow(). We don't need to also move $projects['manual'][$name] = $entry; here.

🤔Maybe the only reason this case is removed and the setting changed is that in an earlier version of the patch we did had

-      $needs_manual = $project['project_type'] == 'core';
+      // Drupal core always needs to be upgraded manually. If a contributed
+      // project is not compatible with the currently installed version of core,
+      // list that project as requiring a manual update, too.
+      $needs_manual = $project['project_type'] === 'core' || $recommended_release['core_compatible'] === FALSE;

This was because we were adding the incompatible projects into the manual table with core. So in that version of the patch removing the case and moving to the if block made sense. It does not now.

We could keep all the assigning to $projects[*][$name] = $entry; in the switch statement by doing

case 'module':
        case 'theme':
          if ($compatible) {
            $projects['enabled'][$name] = $entry;
          }
          else {
            $projects['not-compatible'][$name] = $entry;
          }

It seems like that might be better so it would be easier to all the projects be assigned to the various tables in the switch statement.

This is all the non-test comments I have. I do like use having a dedicated for this and will look at the test next.

dww’s picture

Status: Needs work » Needs review
FileSize
17.18 KB
2.89 KB

Re: #65: Fair enough. I admit I've lost track of a lot of these complications. I'm glad you remember. ;) You missed another reason: you're looking at core itself (which never defines compatibility with itself). That was part of why the other patches had to treat NULL as TRUE. Here's another stab at a better comment.

Re: #66: In an effort to make this patch easier to review, I think you making it harder to review. ;) That's not the only reason for the re-structured code. There's also the question of injecting the core_compatibility_message for the not compatible case. There's also DRY and not making the "smallest possible change" by bloating and complicating the resulting code. The original code was weirdly organized, and made it hard to extend. If we can't reorganize exactly the code we're dealing with in an issue to make the end result easier to read, I don't understand our scoping efforts. The structure we've arrived at after many hours of reviews and writing is in #63. The whole block is this:

     if ($needs_manual) {
        $this->removeCheckboxFromRow($entry);
        $projects['manual'][$name] = $entry;
      }
      elseif (!$compatible) {
        $this->removeCheckboxFromRow($entry);
        // If the release has a core_compatibility_message, inject it.
        if (!empty($recommended_release['core_compatibility_message'])) {
          $entry['data']['recommended_version']['data']['#template'] .= ' <div>{{ core_compatibility_message }}</div>';
          $entry['data']['recommended_version']['data']['#context']['core_compatibility_message'] = $recommended_release['core_compatibility_message'];
        }
        $projects['not-compatible'][$name] = $entry;
      }
      else {
        $form['project_downloads'][$name] = [
          '#type' => 'value',
          '#value' => $recommended_release['download_link'],
        ];

        // Based on what kind of project this is, save the entry into the
        // appropriate subarray.
        switch ($project['project_type']) {
          case 'module':
          case 'theme':
            $projects['enabled'][$name] = $entry;
            break;

          case 'module-disabled':
          case 'theme-disabled':
            $projects['disabled'][$name] = $entry;
            break;
        }
      }
    }

I believe you're proposing this:

  if ($needs_manual) {
        $this->removeCheckboxFromRow($entry);
      }
      elseif (!$compatible) {
        $this->removeCheckboxFromRow($entry);
        // If the release has a core_compatibility_message, inject it.
        if (!empty($recommended_release['core_compatibility_message'])) {
          $entry['data']['recommended_version']['data']['#template'] .= ' <div>{\
{ core_compatibility_message }}</div>';
          $entry['data']['recommended_version']['data']['#context']['core_compat\
ibility_message'] = $recommended_release['core_compatibility_message'];
        }
      }
      else {
        $form['project_downloads'][$name] = [
          '#type' => 'value',
          '#value' => $recommended_release['download_link'],
        ];
      }
      // Based on what kind of project this is, save the entry into the
      // appropriate subarray.
      switch ($project['project_type']) {
        case 'core':
          $projects['manual'][$name] = $entry;
          break;

        case 'module':
        case 'theme':
          if ($compatible) {
            $projects['enabled'][$name] = $entry;
          }
          else {
            $projects['not-compatible'][$name] = $entry;
          }
          break;

        case 'module-disabled':
        case 'theme-disabled':
          if ($compatible) {
            $projects['disabled'][$name] = $entry;
          }
          else {
            $projects['not-compatible'][$name] = $entry;
          }
          break;

      }

I really don't think that's an improvement, either for the patch size, nor the final result.

But here it is in patch form for consideration.

dww’s picture

And if we're going that far, we might as well remove removeCheckboxFromRow() entirely and do this...

dww’s picture

FileSize
16.69 KB
787 bytes

Although if we do #68, we need this, too...

dww’s picture

For visual comparison, here's the full code block from #69 to compare with the options in #67:

    if ($needs_manual || !$compatible) {
        // There are no checkboxes in the 'Manual updates' or 'Not compatible'
        // tables, so they will be rendered by '#theme' => 'table', not '#theme'
        // => 'tableselect'. Since the data formats are incompatible, we convert
        // now to the format expected by '#theme' => 'table'.
        unset($entry['#weight']);
        $attributes = $entry['#attributes'];
        unset($entry['#attributes']);
        $entry = [
          'data' => $entry,
        ] + $attributes;
      }
      if (!$compatible) {
        // If the release has a core_compatibility_message, inject it.
        if (!empty($recommended_release['core_compatibility_message'])) {
          $entry['data']['recommended_version']['data']['#template'] .= ' <div>{{ core_compatibility_message }}</div>';
          $entry['data']['recommended_version']['data']['#context']['core_compatibility_message'] = $recommended_release['core_compatibility_message'];
        }
      }
      // Manual updates don't want a 'project_downloads' value.
      elseif (!$needs_manual) {
        $form['project_downloads'][$name] = [
          '#type' => 'value',
          '#value' => $recommended_release['download_link'],
        ];
      }

      // Based on what kind of project this is, save the entry into the
      // appropriate subarray.
      switch ($project['project_type']) {
        case 'core':
          // Core needs manual updates at this time.
          $projects['manual'][$name] = $entry;
          break;

        case 'module':
        case 'theme':
          if ($compatible) {
            $projects['enabled'][$name] = $entry;
          }
          else {
            $projects['not-compatible'][$name] = $entry;
          }
          break;

        case 'module-disabled':
        case 'theme-disabled':
          if ($compatible) {
            $projects['disabled'][$name] = $entry;
          }
          else {
            $projects['not-compatible'][$name] = $entry;
          }
          break;

      }

progress? ;) /shrug I still think #63 is clearer than all this, but not up to me.

dww’s picture

FileSize
17.64 KB
1.05 KB

Should have done this originally, but here's #63 with only the improved comment from patch #67 to address #65.

tedbow’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/update/tests/src/Functional/UpdateUpdateTest.php
    @@ -0,0 +1,242 @@
    +namespace Drupal\Tests\update\Functional;
    +
    +
    +/**
    

    Extra blank line

  2. +++ b/core/modules/update/tests/src/Functional/UpdateUpdateTest.php
    @@ -0,0 +1,242 @@
    + * @todo In https://www.drupal.org/project/drupal/issues/3117229 expand this.
    

    Do we need a todo if the whole purpose of that issue is expand the test coverage?

  3. +++ b/core/modules/update/tests/src/Functional/UpdateUpdateTest.php
    @@ -0,0 +1,242 @@
    +class UpdateUpdateTest extends UpdateTestBase {
    

    I think this should be UpdateFormTest. UpdateUpdate is confusing

  4. +++ b/core/modules/update/tests/src/Functional/UpdateUpdateTest.php
    @@ -0,0 +1,242 @@
    +  public static $modules = [
    

    Should be protected

  5. +++ b/core/modules/update/tests/src/Functional/UpdateUpdateTest.php
    @@ -0,0 +1,242 @@
    +   * Instead of incurring the cost of a true dataProvider and re-installing
    +   * Drupal for every test scenario, setup the initial state of the site once,
    +   * then use a faux data provider to fetch different release history data and
    +   * assert that things behave as expected.
    ...
    +   * Provides data for test scenarios involving incompatible updates.
    ...
    +   * These test cases rely on the following fixtures containing the following
    +   * releases:
    ...
    +  private function getIncompatibleUpdatesTableTests() {
    ...
    +  /**
    +   * Tests the Update form for a single test scenario of incompatible updates.
    +   *
    +   * @param string $core_fixture
    +   *   The fixture file to use for Drupal core.
    +   * @param string $a_fixture
    +   *   The fixture file to use for aaa_update_test module.
    +   * @param string $b_fixture
    +   *   The fixture file to use for bbb_update_test module.
    +   * @param string[] $compatible
    +   *   Compatible recommended updates (if any). Keys module identifier ('AAA' or
    +   *   'BBB') and values are the expected recommended release.
    +   * @param array[] $incompatible
    +   *   Incompatible recommended updates (if any). Keys module identifier ('AAA'
    +   *   or 'BBB') and values are subarrays with the following keys:
    +   *   - 'recommended': The recommended version.
    +   *   - 'range': The versions of Drupal core required for that version.
    +   */
    +  private function doIncompatibleUpdatesTableTest($core_fixture, $a_fixture, $b_fixture, $compatible, $incompatible) {
    

    I think in general we should use an actual @dataProvider method when we have to go through this much complexity to avoid a dataProvider.

    I know we sometimes skip a dataprovider and just have an array of tests cases but when then have to make another doTest*() method as well as faux data provider method to make it understandable, I think we should just use @dataProvider and not worry that the test suite will take a little longer to run.

    That way we leverage more of PHPUnit and it is easier to understand and maintain for any developers who are new to this class. If we just use a standard dataProvider then it is just that much easier because it is pattern people have seen more.

  6. +++ b/core/modules/update/tests/src/Functional/UpdateUpdateTest.php
    @@ -0,0 +1,242 @@
    +   * @see self::getIncompatibleUpdatesTableTests()
    +   * @see self::doIncompatibleUpdatesTableTest()
    

    I searched core I more occurences of
    * @see ::methodOnSameClass()
    97
    than
    * @see self::methodOnSameClass()
    52

    I don't actually know which 1 is more correct. Maybe somebody else does?

  7. +++ b/core/modules/update/tests/src/Functional/UpdateUpdateTest.php
    @@ -0,0 +1,242 @@
    +      'only one compatible' => [
    ...
    +      'only one incompatible' => [
    ...
    +      'two incompatible, no compatible' => [
    

    I like the expanded number of test case 🎉

  8. +++ b/core/modules/update/tests/src/Functional/UpdateUpdateTest.php
    @@ -0,0 +1,242 @@
    +        'b_fixture' => '1_0', // Up to date.
    ...
    +      'only one incompatible' => [
    ...
    +        'b_fixture' => '1_0', // Up to date.
    

    Comments can't appear at the end of the line. Should be moved to the line before

  9. +++ b/core/modules/update/tests/src/Functional/UpdateUpdateTest.php
    @@ -0,0 +1,242 @@
    +    // Determine how many rows we're expecting in each table.
    +    $num_compatible = count($compatible);
    +    $num_incompatible = count($incompatible);
    

    I don't actually think we need to assign this and I don't think it makes it more clear.

    We use each of these 2x.

    Once in an if
    if ($num_compatible) {
    But since these are arrays I think this function the exact same way
    if (compatible) {

    The second time is
    $assert_session->elementsCount('css', "$compatible_table_locator tbody tr", $num_compatible);
    We could just do
    $assert_session->elementsCount('css', "$compatible_table_locator tbody tr", count($compatible));

    So aren't actually saving a call to count() by assigning the variable.

    I think it is more readable because $num_compatible is not as clear as count($compatible).

    Another reason is because if you are using an IDE invoking help on $compatible will give you the help next of @param vs nothing on $num_compatible because there is not doc text associated with it.

  10. +++ b/core/modules/update/tests/src/Functional/UpdateUpdateTest.php
    @@ -0,0 +1,242 @@
    +        // Both contribs always use 8.x-1.0 as the currently-installed version.
    ...
    +        // Both contribs always use 8.x-1.0 as the currently-installed version.
    

    I only see "contribs" 1 other place core. Should be "contrib modules"

dww’s picture

Status: Needs work » Needs review
FileSize
17.63 KB
2.03 KB

Thanks for reviewing the tests. Re #72:

1. Fixed.
2. Doesn't seem to hurt. The issue is already open. This class is missing a lot of coverage. Might as well say so.
3. I went with UpdateManagerUpdateTest to match UpdateManagerUpdateForm.
4. Fixed.
5. I've seen numerous core patches trying to use a dataProvider in a functional test get shot down. It's wickedly expensive and wasteful to re-install Drupal for every scenario, especially if you add lots of them. ;) It's all pretty thoroughly documented, so I trust other folks could add more test cases here without much trouble.
6. See #3112830: [policy, no patch] Allow static::methodName() and/or self::methodName() in @see comments when referring to the same class. There's no documented policy and core is inconsistent. I'm trying to fix that. /shrug
7. 👍
8. Fixed.
9. Fixed, good points.
10. Fixed.

Note: due to the rename, interdiff is wildly confused. So here's a raw diff of the 2 patch files. This is built on #71, my preference of the latest implementation patches).

tedbow’s picture

FileSize
5.17 KB

Here is an interdiff for 71-73.

@dww I putting

[diff]
  renames = copies

in your ~/.gitconfig
will allow it to find the renames. I am positive because I set it up awhile again

dww’s picture

FileSize
17.17 KB
2.7 KB

Here's a real data provider. As you can see from the patch and interdiff, it doesn't actually save us much in the test class. But it more than doubles the runtime of the test (at least locally):

Faux (#73)

% ./vendor/bin/phpunit --debug -v -c core/phpunit.xml core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
...
./vendor/bin/phpunit --debug -v -c core/phpunit.xml   10.70s user 2.16s system 14% cpu 1:28.06 total

Real (#75)

% ./vendor/bin/phpunit --debug -v -c core/phpunit.xml core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
...
./vendor/bin/phpunit --debug -v -c core/phpunit.xml   52.84s user 8.38s system 32% cpu 3:10.16 total

I vote #73, and work (elsewhere) to make it less expensive to have dataProviders for Functional tests.

tedbow’s picture

@dww thanks for updates
#73

  1. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,239 @@
    +        // Both contribs always use 8.x-1.0 as the currently-installed version.
    ...
    +        // Both contrib modules use 8.x-1.0 as the currently-installed version.
    

    Missed 1 "contribs" to "contrib modules"

  2. 5) I chatted more with @dww and @xjm about.

    I still think it makes more sense to use real @dataProvider. @dww you did document it well adn I don't think it will be hard to add ongoing test cases. I just think if we use @dataProvider we don't need the documentation because we are using standard feature of our testing framework.

    @xjm mention that FunctionalJavascript test are much slower so not using a dataProvider might make sense in that case.

    All that being said I won't block this issue on it.

couple things I missed before

  1. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,239 @@
    +  protected function setUp() {
    

    Needs {@inheritdoc}

  2. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,227 @@
    +  protected function setUp() {
    

    Needs {@inheritdoc}

  3. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,227 @@
    +   * @param array[] $incompatible
    

    I think this should be @param string[][]

  4. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,227 @@
    +  public function testIncompatibleUpdatesTable($core_fixture, $a_fixture, $b_fixture, $compatible, $incompatible) {
    

    Needs array type hint for $compatible, $incompatible. This problem was there on the non-dataprovider version. I just missed it

Just saw #75.
@dww thanks for making this version.

  1. As you can see from the patch and interdiff, it doesn't actually save us much in the test class.

    To me this much clearer. I just see @dataProvider I don't have to know anything else to know the relationship between the 2 methods.

  2. As the command output. I am not sure why I don't see the exact same out as you.

    In the older version I see "Time: 10.77 seconds"
    and the new version I see "Time: 28.87 seconds"
    so yes 20 seconds more but seems worth it to me.

dww’s picture

Re: #76 thanks @tedbow for continued careful reviews...

1st 1. Fixed.
1st 2. TBD. I'm providing both real and faux versions for continued consideration.

2nd 1. Fixed.
2nd 2. Duplicate
3. Fixed.
4. Fixed.

3rd 1. /shrug Providing an interdiff between both patches here for continued comparison.
3rd 2. /shrug I know every time we add 20 seconds to the test runs, that gets multiplied *a lot* for all of core development, and it costs the DA real cash money. Not my decision. I still vote faux, until that doesn't cost us anything.

p.s. Officially crediting @xjm on this, who's spent far more effort and valuable time on this than many of the folks already credited. ;)

p.p.s. We need another RTBC, so putting that back on the remaining tasks list.

dww’s picture

Asked for more input on this topic in Slack, to which @alexpott replied:

I think using a real data provider is fine. In general unless the test is super expensive it should be preferred because it isolates each test dataset - potentially revealing bugs we might not spot (of course the opposite is true too) but I think the isolated datasets might be more realistic to how a real site would work.

So yay, a real dataProvider it is! ;) Hiding faux from the summary. Posting this here so I have a lasting place to point people to who push back against Functional dataProvider patches in the future.

Thanks!
-Derek

benjifisher’s picture

Thanks @dww and @tedbow for all the effort you have put in here. I only have time to make a few suggestions.

First, the change discussed in #66, #67, and following comments. I agree with @dww on this point. In #44 and #45, @tim.plunkett and I reviewed and approved that bit of moving code around. In other words, we both decided that the change was in scope given the changes needed for this issue. I think that is the right standard to apply, not "as few changes as possible".

Second, we can add some comments as part of this issue, but a proper fix for the "code smell" (the term used in #55.1 for the line $compatible = $recommended_release['core_compatible'] ?? TRUE;) is outside the scope of this issue. I started to create a follow-up issue to do something about that, but then I realized how hard it will be to fix. Currently, ProjectCoreCompatibility::setReleaseMessage() is responsible for adding messages to projects that need a compatibility warning. Asking it to add a flag to projects that do not need such a warning seems like the wrong solution.

I think the right answer is to have a class that "owns" the update information for all projects and can answer questions like "is version foo of core compatible with version bar of the quux module?" What do you think?

Third and last point: I think the problem we are trying to solve with the removeCheckboxFromRow() method (#55.5) is that we have already prepared an array for '#type' => 'tableselect', and now we are trying to modify it for '#type' => 'table',. I would rather pass around a simpler data structure until we know whether it will be used for table or tableselect. Is that in scope for this issue? I think not, and if you agree then I am happy to create a follow-up issue for that.

dww’s picture

@benjifisher Thanks for #79:

1) Does that mean you want to RTBC this? ;)

2) Yeah, it is a bit hard to solve that cleanly, given nearly the entire codebase dates from the ArrayPI days of Drupal (thanks @xjm, that's a great term!). Regardless, I think cleaning up that smell properly requires a follow-up, not something we can solve here. Feel free to open it, and we can redesign / rescope it as things evolve. I don't know that a global class that knows everything is necessarily the right answer. A step in the right direction would be #3100110: Convert update_calculate_project_update_status() into a class, but that's not the whole picture. I don't think it's unreasonable for ProjectCoreCompatibility to be responsible for setting an is_compatible flag on releases. That's basically what the class is all about, as the name implies. We'd just need to think a little more clearly and carefully about how we want that to work, and if it's worth continuing to make the ArrayPI more beautiful with a yummy aroma, or if it'd be more worthwhile to start converting more of Update Manager into classes. /shrug

3) Yes, that'd probably be cleaner. Yes, that also seems like good follow-up material. Given how much angst has been spilled in here over scope creep already, trying to change anything else about this code in this critical bug fix makes me nervous.

Thanks again,
-Derek

dww’s picture

Until #3113798: Remove unused (and generally wrong) <tag> markup from Update module test XML fixtures is done, we might as well not make that problem worse. Fixing a copy/paste propagation of more of the totally bogus <tag> values in our fixtures.

dww’s picture

Fixing a bit more bogus (but irrelevant) data in the new fixtures I had missed.

Short of the "divitis" from #55.2, I think this is RTBC. Everything else has been either fixed in the patch or answered in previous comments.

It seems like yucky scope creep to add a whole new twig template for the new table here. I'm happy to add a class to the <div> if that's better, but I suspect it's not. I fear if I go changing anything else, it's going to be labeled "nice but out of scope" again. But I know this particular bit of markup isn't easily modified or themed, so that's not great, either.

Since this is now in a separate table, we could add a whole new column for this. But that'll make #3116542: Make column widths consistent on the Update form more difficult or impossible.

Really not clear on how this should be done so it's acceptable to everyone.

Tagging for @lauriii's opinion and adding this point to remaining tasks.

Thanks,
-Derek

Both of the fixes in #81 and #82 are needed because the original bbb_update_test.1_0.xml file has both errors. I'd fix those, but then I'm *sure* someone would complain it's out of scope.

tedbow’s picture

Status: Needs review » Needs work

@benjifisher thanks for the further review.

@dww thanks for the fixes.

  1. All looks good to me. Just one other thing I missed that I just noticed.
    +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,230 @@
    +      'administer modules',
    

    We don't need the "administer modules" permission. I just ran the test locally without it and they all pass.

  2. I would RTBC after the permission is fixed except for <div>/template question.
MeenakshiG’s picture

Status: Needs work » Needs review
FileSize
17.12 KB
533 bytes
lauriii’s picture

Generally we wouldn't want to add <div> elements without at least having an API that would allow adding attributes to it. Given that this issue is critical, I'd say adding the <div> here is fine. We could consider using a <p> element, which would feel slightly better solution in the sense that it usually has some default styling. However, this changes the design. I recommend that if we prefer the design as it is, we should move forward with the current solution and ensure we have opened a follow-up for refactoring the render array to use templates.

dww’s picture

Assigned: Unassigned » dww
Issue summary: View changes
Status: Needs review » Needs work
Issue tags: +Needs followup

@tedbow re: #83: Good catch, thanks for reviewing.
@Meenakshi.g re: #84: Thanks for fixing that point.
@lauriii re: #85: Thanks for weighing in, and for validating the pragmatic path I picked and explained in #57.2. Follow-up it is. ;)

I'll open the follow-up now, and add @todo to the patch to point to it. Stay tuned.

dww’s picture

Assigned: dww » Unassigned
Issue summary: View changes
Status: Needs work » Needs review
Issue tags: -Needs followup
FileSize
17.51 KB
1.26 KB
dww’s picture

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

@dww thanks for sticking with it! RTBC 🎉

xjm’s picture

Assigned: Unassigned » xjm

Reviewing now.

tedbow’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
3.88 KB
17.03 KB

I chatted with @xjm and @tim.plunkett about this.

Just couple test changes and some nits

  1. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,229 @@
    +      $i = 1;
    ...
    +        $compatible_row = "$compatible_table_locator tbody tr:nth-of-type($i)";
    ...
    +        $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(2)", "$module Update test");
    ...
    +        $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(3)", '8.x-1.0');
    +        $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(4)", $version);
    

    We were trying to figure out a way to make the test depend less on 1) the other of the modules in the table and 2) the particular markup.

    I have made some changes that do this but don't change the overall structure of the test.

  2. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,229 @@
    +   * - aaa_update_test.cre_compatibility.8.x-1.2_8.x-2.2.xml
    

    Small typo here; I think it's core_compatibility and not cre_compatibility.

  3. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,229 @@
    +      // We never want to see a compatibly range in the compatible table.
    

    compatibility range

  4. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,229 @@
    +        // Both contrib modules use 8.x-1.0 as the currently-installed version.
    ...
    +        // Both contrib modules use 8.x-1.0 as the currently-installed version.
    

    Nitpick: "currently-installed" does not need to be hyphenated; "currently" is an adverb.

dww’s picture

@tedbow: Thanks for posting your review in the form of a new patch. ;) Still hope this lands in time for 8.8.5! I obviously can't RTBC it, and you can't either. Not sure what happens next.

Re: #91:

  1. Not sure why we'd want that. I thought the point of our tests was to make sure things behave exactly as we expect, including the order (that's what you meant by "other", right?) we're expecting things to be in, and the specific markup generated. Other tests are asserting for exact contents of twig templates and checking md5 hashes. I thought we shot down test assertions that were too lax and broad, and generally preferred more targeted and specific assertions. I've seen the argument made that having to update test assertions when we change markup is a Good Thing(tm) because it's helping us feel the pain of site builders if we don't deal with markup in BC ways. Etc, etc. /shrug
  2. 👍
  3. 👍
  4. 👍 For the interested reader: https://english.stackexchange.com/questions/231516/is-currently-installe... ;)

Status: Needs review » Needs work

The last submitted patch, 91: 3113992-91.patch, failed testing. View results

dww’s picture

Status: Needs work » Needs review
FileSize
17.51 KB
2.56 KB
3.06 KB

Restoring the working tests from #87 and ignoring #91.1.
Fixing everything else from #91.
Interdiffs relative to both #87 and #91.

🤞

Thanks,
-Derek

dww’s picture

Note on why #91 failed, looking at the interdiff:

  1. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -187,18 +187,14 @@ public function testIncompatibleUpdatesTable($core_fixture, $a_fixture, $b_fixtu
    -      $assert_session->elementsCount('css', "$compatible_table_locator tbody tr", count($compatible));
    ...
    +      $assert_session->elementsCount('css', "$compatible_table_locator tr", count($compatible));
    

    This is what broke the tests... these tables have a row for the header in <thead>, and then rows for the payload in <tbody>. The previous, more precise locator is needed unless you want to count() + 1 to deal with the header row...

  2. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -187,18 +187,14 @@ public function testIncompatibleUpdatesTable($core_fixture, $a_fixture, $b_fixtu
    +        $compatible_row = "$compatible_table_locator tr:contains('$module Update test')";
    
    @@ -208,16 +204,13 @@ public function testIncompatibleUpdatesTable($core_fixture, $a_fixture, $b_fixtu
    -      $assert_session->elementsCount('css', "$incompatible_table_locator tbody tr", count($incompatible));
    ...
    +      $assert_session->elementsCount('css', "$incompatible_table_locator tr", count($incompatible));
    

    Same here.

If we insist on going with something like #91, those are the parts that would need to be reverted for the tests to pass.

dww’s picture

Issue summary: View changes

Initial stab at a release note snippet. Probably too verbose (as usual) but it can be edited down as desired. Not sure we need one at all, but that's up to @xjm...

Cheers,
-Derek

dww’s picture

Issue summary: View changes

Other summary updates to make it accurate and complete.

xjm’s picture

Assigned: xjm » tedbow
Status: Needs review » Needs work

The test changes in #91.1 were based on the fact that the test was too tightly coupled to the specific markup and order of the modules; while there's a bug, it was on the right track. It is not desirable for tests to be coupled to markup any more than is necessary to ensure that we're testing the right thing. So marking NW and assigning to Ted to fix tomorrow. :)

dww’s picture

Assigned: tedbow » Unassigned
Status: Needs work » Needs review
FileSize
17.09 KB
1.35 KB

Confirmed this passes locally.

dww’s picture

FileSize
17.09 KB
799 bytes

Comment typo.

dww’s picture

Here's why I think #94 is better than #91/#100. Let's say #3121775: Refactor UpdateManagerUpdateForm::buildForm() to make ::removeCheckboxFromRow() unnecessary introduces a bug where we get mixed up with which column a given thing should go into. See 3113992-101.break-it.do-not-test.patch -- something of a stretch, but it's possible. Seems like exactly the kind of regression we would hope these tests would prevent.

If you apply that to #100 (and tell the bot not to waste cycles running the entire test suite), you get 3113992-101.broken-100.should-fail-will-pass.patch -- which passes all 5 cases in the new test. :( We're no longer testing that the version string we expect is in the column where we expect it.

If you apply that to #94, you get 3113992-101.broken-94.should-fail-will-fail.patch which now fails, since the expected installed version string isn't just present anywhere, it's missing from the column where it should be.

Meanwhile, at #3117229: Expand test coverage of Drupal\update\Form\UpdateManagerUpdate we're going to need to start testing the order of the things in these tables. There's code that puts the security updates at the top:

      switch ($project['status']) {
        case UpdateManagerInterface::NOT_SECURE:
        case UpdateManagerInterface::REVOKED:
          $entry['title'] .= ' ' . $this->t('(Security update)');
          $entry['#weight'] = -2;
          $type = 'security';
          break;

        case UpdateManagerInterface::NOT_SUPPORTED:
          $type = 'unsupported';
          $entry['title'] .= ' ' . $this->t('(Unsupported)');
          $entry['#weight'] = -1;
          break;

How do we test this if not by having tests that care about the order of the rows in the tables?

In short, why are we undoing test coverage? Isn't all this "testing the right thing"? I don't understand #98, and it doesn't answer #92.1.

dww’s picture

Since the bot's going to be confused about re-testing the #101 patches, also re-uploading #94 as the latest patch...

The last submitted patch, 101: 3113992-101.broken-94.should-fail-will-fail.patch, failed testing. View results

dww’s picture

p.p.s. I'd happily have classes in the table columns (which would make #3116542: Make column widths consistent on the Update form easier) instead of nth-of-type()'ing these, but that'll surely be called out of scope. So I'm doing the best I can given the constraints. ;)

dww’s picture

FileSize
17.51 KB
799 bytes

I forgot #94 doesn't have the comment typo fix from #100. This is better.

3113992.94_105.interdiff.txt is identical to 3113992.99_100.interdiff.txt

tedbow’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
    @@ -199,42 +199,49 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +      $compatible = $recommended_release['core_compatible'] ?? TRUE;
    

    In #46 @alexpott asked if we have test coverage for $recommended_release['core_compatible'] not being set.

    In #55.1 @xjm also asked if we had test coverage for the TRUE, FALSE and NULL cases.

    I have checked and we do.
    aaa_update_test.8.x-1.2.xml has core_compatibility set will evaluate to $recommended_release['core_compatible'] === TRUE
    bbb_update_test.1_2.xml has core_compatibility set will evaluate to $recommended_release['core_compatible'] === FALSE
    bbb_update_test.1_1.xml does not have core_compatibility set and will mean $recommended_release['core_compatible'] will not be set.

  2. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,229 @@
    +   * - bbb_update_test.1_1.xml
    +   *   - 8.x-1.1 is available and compatible with everything.
    ...
    +          'BBB' => '8.x-1.1',
    

    While this fixture does not have core_compatibility set and will therefore cover the test case of $recommended_release['core_compatible'] not being set this comment doesn't actually explain. Being "compatible with everything" could mean core_compatibility is not set but it also could mean
    <core_compatibility>^8</core_compatibility>
    Not everything, but all core version we would be testing. an all version of 8.
    or
    <core_compatibility>*</core_compatibility>
    Truely everything. Not a value hopefully people are using but it is value we do use other test modules info files.

    So I think unless this is well documented we could easily accidently add either 1 of the values above to bbb_update_test.1_1.xml especially in an issue like #3110917: [meta] Fix update XML fixtures bad data

    in that case all the tests would continue to pass but we would lose test coverage the case where $recommended_release['core_compatible']

    I think we should add a comment in bbb_update_test.1_1.xml explicitly saying core_compatibility can not be added to the release there and why.

    we are now relying on not being set where we weren't before this issue.

tedbow’s picture

  1. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,229 @@
    +        $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(2)", "$module Update test");
    +        // Both contrib modules use 8.x-1.0 as the currently installed version.
    +        $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(3)", '8.x-1.0');
    +        $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(4)", $version);
    ...
    +        // Both contrib modules use 8.x-1.0 as the currently installed version.
    +        $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(2)", '8.x-1.0');
    +        $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(3)", $data['recommended']);
    +        $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(3)", 'Requires Drupal core: ' . $data['range']);
    

    I think @dww's comment in #101 shows there is some possible value to having the assertion that in asserting the particular column order. I am fine with this. This very unlikely to change.

  2. +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
    @@ -0,0 +1,229 @@
    +      $i = 1;
    ...
    +        $compatible_row = "$compatible_table_locator tbody tr:nth-of-type($i)";
    

    While the order of the modules unlikely change I am less convinced that we need to assert the order all modules in this table.

    yes #3117229: Expand test coverage of Drupal\update\Form\UpdateManagerUpdate we will need to assert that security updates are before other updates it does not follow that we to test the exact order of other modules. For all we know we will have separate test method for that assertion anyways.

    Nowhere in the update functions involved does it document or guarantee they are returned in specific order.

    The order of the projects ultimately comes from our extension list classes which also do not document the order. and their unit/kernel tests don’t explicitly test the order.

    We could add back
    $incompatible_row = "$incompatible_table_locator tr:contains('$module Update test')";
    and get rid of $i++ logic and leave everything else basically the same.

tedbow’s picture

Here is just a demo of what I was trying to explain in #106.2

If we update the fixtures in this way(not at all unlikely without strict comments) we lose test coverage.

dww’s picture

Assigned: Unassigned » tedbow

Re: #106:
1. Yeah, exactly like I said at the end of #63 in answer to both #46 and #55:

I should add, although the inline comments don't spell this out, the new test coverage here means we're hitting all 3 cases for #55.1: TRUE (aaa_update_test 8.x-1.2), FALSE (bbb_update_test 8.x-1.2), and NULL treated as TRUE (bbb_update_test 8.x-1.1).

2. I'm not sure what you're talking about. bbb_update_test.1_1.xml is a new fixture for this issue/test. There's no "we are now relying on not being set where we weren't before this issue" since the fixture never existed until now. I'm of course in favor of more comments (almost always!), but I think this is somewhat grasping at straws to find any possible reason to not RTBC this. :/ We could block this (and every other Update manager issue) on fixing #3115435: Make clear why each XML update.module fixture is created the way it is, but that would violate your position in #3100386: Create contrib update module test cases that use semantic versioning that fixing critical bugs is more important than documenting our test fixtures properly. I give up.

Re: #107:

1. 👍

2. 🤦‍♂️ Maybe I should start yet another issue tag "Perfect is the enemy of good". Lord knows I'd set that on my own reviews in many other issues. 😉But still, really? We're going to keep delaying this issue for stuff like this? Assigning to you. Have fun. I'm very burned out on trying to fix this bug.

I think #105 is RTBC and should be in core before any more releases. But if you want to keep polishing the test coverage, knock yourself out.

dww’s picture

Adding comments for #106.

If #107.2 blocks this issue, someone else can do that.

tedbow’s picture

@dww thanks for addressing #106.

  1. +++ b/core/modules/update/tests/modules/update_test/bbb_update_test.1_1.xml
    @@ -0,0 +1,48 @@
    +To ensure we've got test coverage for the case where the '<core_compatibility>'
    +tag is not defined at all, this fixture does not include that value.
    +-->
    +<?xml version="1.0" encoding="utf-8"?>
    

    Comment looks but will throw an error because the
    <?xml version="1.0" encoding="utf-8"?> need to the first element in an xml file. Just moving that to the top.

  2. addressing #107.2

Assign to @tim.plunkett for review

xjm’s picture

Nice work and thanks for checking #106.1. Can we add a brief column to the data provider documenting that coverage?

@dww, thanks for your reviews and feedback. It's okay to step back on an issue if it's burning you out. Please do let reviewers provide feedback and remember they also care both about landing this critical and ensuring we're not adding other technical debt when we do. And remember that this issue is an allowed change during beta, and actually since there's a regression we'll make an exception here to backport it to a patch release, so this change is not at risk of not getting in. I understand that it can be tough to balance the QA process and covering edgecases under time pressure, but it's okay also for this bug to be fixed and tested in a way that everyone who contributed is confident in it.

Since we do want to backport this to 8.8.x, I queued an 8.8.x test to check that the change does apply there also, and am setting the branch accordingly.

xjm’s picture

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

Actually setting the branch.

dww’s picture

Thanks for the encouragement, @xjm. Yes, I understand we're all balancing a lot. I'm finding the differences between what folks are saying and doing in here vs. the arguments being made in #3100386: Create contrib update module test cases that use semantic versioning are creating a lot of cognitive dissonance for me, so I'm struggling to keep my cool. Glad to know this critical bug can be backported to 8.8.x. I'm feeling time pressure because this has been fixed for a month, but it got bogged down in getting the test coverage perfect. I know we want solid test coverage, but this is starting to feel rather absurd.

Re:

Can we add a brief column to the data provider documenting that coverage?

Do you mean something more than these comments I already added?

+++ b/core/modules/update/tests/modules/update_test/bbb_update_test.1_2.xml
@@ -1,3 +1,13 @@
diff -u b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php

diff -u b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
--- b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php

--- b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
+++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php

+++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
+++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
@@ -72,9 +72,11 @@

@@ -72,9 +72,11 @@
    * - bbb_update_test.1_0.xml
    *   - 8.x-1.0 is the only available release.
    * - bbb_update_test.1_1.xml
-   *   - 8.x-1.1 is available and compatible with everything.
+   *   - 8.x-1.1 is available and compatible with everything (does not define
+   *     <core_compatibility> at all).
    * - bbb_update_test.1_2.xml
-   *   - 8.x-1.1 is available and compatible with everything.
+   *   - 8.x-1.1 is available and compatible with everything (does not define
+   *     <core_compatibility> at all).
    *   - 8.x-1.2 is available and requires Drupal 8.1.0 and above.

These are comments for incompatibleUpdatesTableProvider() (not that the patch file gives you that context on its own). If that's not sufficient, can you clarify what you want?

Thanks,
-Derek

tim.plunkett’s picture

I think those docs are helpful, especially as they reference which fixtures are used.
The last missing piece is something inline at the top of incompatibleUpdatesTableProvider() that explains what the order of the keys means, essentially repeating the @param docs of testIncompatibleUpdatesTable().

For example, see \Drupal\Tests\layout_builder\Unit\OverridesSectionStorageTest, specifically how testExtractEntityFromRoute() providerTestExtractEntityFromRoute() have their docs done.

dww’s picture

Re: #115: I thought that's part of why we wanted a real dataProvider, not the faux provider, since everyone is supposed to understand the relationship between these functions automatically, and will know where to look if they need to better understand the structure of the returned data. In other issues, when I asked for comments about something in multiple places to make each spot easier to understand, core committers like @xjm have said, in effect: "But then there are multiple places we'd have to update if we change anything, and that's not good."

But since I love excessively commented code, here goes nothing. ;) Also fixed every other conceivable nit I could possibly find in the comments, and moved the @dataProvider annotation above the @param annotations to match the linked example of perfect dataProvider documentation.

RTBC? Pretty please with 31 patches on top?
🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned
Status: Needs review » Reviewed & tested by the community

It's beautiful 😭😭😭

@tedbow and @dww I really appreciate both of your work on this. I think this is ready to go!

xjm’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
19.03 KB
2.4 KB

Sorry for being unclear; I am literally asking for the words from #106.1 to be put in inline comments on the specific array items in the data provider that test each of the three logics. That will make it easier to identify the test coverage for that one line, given the brittleness of the behavior that's out of scope to fix for this issue. Something like this...

I am just going off what @tedbow wrote in #106 here and have not validated this myself, so please check that the comments are true and in the right place. If others +1 that this small comment improvement is adding true information then I can still commit this. 🤞Or if it's wrong and someone else fixes it. Sorry for the confusion!

dww’s picture

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

#118 clobbers the fixes in #116 to address #115. Re-rolling and putting the comments in the right places.

dww’s picture

Assigned: dww » Unassigned
Status: Needs work » Needs review
FileSize
19.73 KB
1.68 KB
dww’s picture

In Slack, @xjm confirmed:

@dww I meant to undo the part that was a duplicate of the docblock because you were right about the data provider docblock being sufficient. Meant to keep the wording change you'd added. Tim was trying to explain what I meant but our brains didn't actually line up about what we were saying. :smile:

@tim.plunkett: You owe me a bottle of wine. ;) And a follow-up to remove the duplicate stuff from your linked example in layout_builder. ;)

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

Just checke 120 and it documents nicely what I mentioned in #106.1

I prefer the non-duplication of comments like #121 though I know not everybody does.

Looks RTBC to me.

@dww, @tim.plunkett and @xjm thanks for all the work!

xjm’s picture

Status: Reviewed & tested by the community » Fixed

Thanks all!

Just wanted to point out that Tim's recommendation wasn't wrong at all, and what LB does is completely correct. I was just trying to explain why my interdiff hadn't included the hunk when I was trying to illustrate what I meant. There's precedent for both #120 and #121 in core and I'm happy to commit either one. Since there seems to be a slight majority for #121 and the most critical parts of the docs are covered, I'll commit that one.

Thanks everyone for your perseverance on this. I know it's been a pain.

Committed to 9.1.x down through 8.8.x. Thanks!

  • xjm committed 5d6cba4 on 9.0.x
    Issue #3113992 by dww, tedbow, xjm, Meenakshi.g, benjifisher, kualee,...

  • xjm committed 949038a on 9.1.x
    Issue #3113992 by dww, tedbow, xjm, Meenakshi.g, benjifisher, kualee,...

  • xjm committed 0680aa0 on 8.9.x
    Issue #3113992 by dww, tedbow, xjm, Meenakshi.g, benjifisher, kualee,...

  • xjm committed 189d7cc on 8.8.x
    Issue #3113992 by dww, tedbow, xjm, Meenakshi.g, benjifisher, kualee,...

Status: Fixed » Closed (fixed)

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