Problem/Motivation

We have hit problems with the security release of 8.6.7 and the bug fix release of 8.6.8. See:

The technical reasons for each one are different but our tests that extends UpdatePathTestBase have not caught these problems. We need a way in which we can address the security issues caused by phar and the PHP bug behind #2701829: Extension objects should not implement \Serializable without causing these update woes.

The update programme - #3029353: First launch of the beta testing program - is one tack but this issue proposes to add a new test type.

General specifications for the new testing type:

  • It will not use the branch being tested as a starting codebase
  • It will not use the drupal environment to run - it will be based on Drupal's unit test case or PHP's.
  • It will install the minimum version of Drupal supported determined by our policy and the branch that is being tested \Drupal::VERSION
  • It will install Drupal codebase using composer
  • It will install Drupal somehow
  • It will log into the site as user 1
  • It will then be able to do upgrades via composer / drush / tarball?
  • It will then test the resulting upgrade.

Proposed resolution

  • Create a new test suite. We’ll call it ‘build.’
  • HTTP requests must be able to run in isolation from the system running the tests.
  • Tests must be able to run in parallel under run-tests.sh.
  • Each build test case will encapsulate a workspace in the file system, and potentially an HTTP server to allow for making requests against the assembled site.
  • Tests will be able to execute external commands on the command line, such as git or composer, in order to manipulate the workspace.
  • Tests will be able to copy the patched version of Drupal to the workspace.
  • Using the provided HTTP server, tests will be able to make a request, and assert against the results.
  • We’ll use Mink as a framework for assertions against content.

Remaining tasks

Remove the proof-of-concept tests from the patch before committing. Create followups as necessary to re-add them.

Determine which sorts of tests are out of scope for this framework.

Ensure we’re not re-running a large number of tests. See #44.

RM review of symfony/lock as a new dev only dependency. https://github.com/symfony/lock/blob/master/CHANGELOG.md might be a good place to start.

User interface changes

None

API changes

New test type to use - \Drupal\BuildTests\Framework\BuildTestBase

Data model changes

None

Release notes snippet

A new test type has been added that can build a Drupal site. This will allow more realistic update testing. See https://www.drupal.org/node/3082383 for more information.

CommentFileSizeAuthor
#242 interdiff_241-242.txt1.04 KBheddn
#242 3031379-242.patch49.97 KBheddn
#233 3031379-226-to-233-interdiff.txt1.5 KBgreg.1.anderson
#233 3031379-226.patch50.32 KBgreg.1.anderson
#226 3031379-225-to-226-interdiff.txt2.03 KBgreg.1.anderson
#226 3031379-226.patch50.32 KBgreg.1.anderson
#225 3031379-221-to-225-interdiff.txt1.71 KBgreg.1.anderson
#225 3031379-225.patch50.47 KBgreg.1.anderson
#221 3031379-221.patch50.29 KBgreg.1.anderson
#221 3031379-219-to-221-interdiff.txt1.85 KBgreg.1.anderson
#219 3031379-219.patch50.45 KBgreg.1.anderson
#219 3031379-216-to-219-interdiff.txt1.33 KBgreg.1.anderson
#216 interdiff.txt1.56 KBMile23
#216 3031379_215.patch51.62 KBMile23
#215 interdiff.txt4.31 KBMile23
#215 3031379_215.patch52.02 KBMile23
#211 interdiff.txt22.09 KBMile23
#211 3031379_211.patch56.32 KBMile23
#209 interdiff.txt14.82 KBMile23
#209 3031379_209.patch60.56 KBMile23
#207 interdiff.txt3.9 KBMile23
#207 3031379_207.patch55.24 KBMile23
#205 interdiff.txt11.35 KBMile23
#204 3031379_204.patch53.68 KBMile23
#203 interdiff.txt15.67 KBMile23
#203 3031379_203.patch49.98 KBMile23
#195 interdiff_193-194.patch620 bytesGhost of Drupal Past
#195 3031379-194.patch55.49 KBGhost of Drupal Past
#193 interdiff.txt11.61 KBMile23
#193 3031379_193.patch55.52 KBMile23
#186 3031379-183.patch67.11 KBGhost of Drupal Past
#186 interdiff_176-183.txt4.46 KBGhost of Drupal Past
#181 3031379_181.patch67.41 KBGhost of Drupal Past
#181 interdiff_176-181.txt1.22 KBGhost of Drupal Past
#176 3031379-176.patch67.57 KBheddn
#176 interdiff_173-176.txt1.2 KBheddn
#173 interdiff_170-173.txt7.21 KBGhost of Drupal Past
#173 3031379-173.patch67.56 KBGhost of Drupal Past
#170 3031379-170.patch66.65 KBheddn
#170 interdiff_166-170.txt6.29 KBheddn
#168 3031379-168.patch63.9 KBheddn
#168 interdiff_164-168.txt3.29 KBheddn
#164 3031379-162.patch64.11 KBheddn
#164 interdiff_157-162.txt21.1 KBheddn
#157 3031379-157.patch61.23 KBheddn
#157 interdiff_155-157.txt1.84 KBheddn
#155 3031379-154.patch61.21 KBheddn
#154 interdiff_151-154.txt4.18 KBheddn
#154 interdiff_151-154.txt4.18 KBheddn
#151 3031379-151.patch60.69 KBheddn
#151 interdiff_150-151.txt1.08 KBheddn
#150 3031379-150.patch60.67 KBheddn
#150 interdiff_149-150.txt4.1 KBheddn
#149 3031379-149.patch60.68 KBheddn
#146 interdiff_135-146.txt2.27 KBheddn
#146 3031379-146.patch60.67 KBheddn
#145 3031379-144.patch61.49 KBheddn
#143 3031379-143.patch61.59 KBheddn
#142 3031379-142.patch61.69 KBheddn
#140 3031379-140.patch61.64 KBheddn
#140 interdiff_138-140.txt647 bytesheddn
#138 interdiff_135-138.txt975 bytesheddn
#138 3031379-138.patch61.63 KBheddn
#135 interdiff.txt829 bytesMile23
#135 3031379_135.patch61.63 KBMile23
#133 interdiff.txt2.63 KBMile23
#133 3031379_133.patch61.63 KBMile23
#131 3031379-131.patch60.64 KBheddn
#131 interdiff_128-131.txt586 bytesheddn
#128 interdiff_126-128.txt833 bytesheddn
#128 3031379-128.patch60.63 KBheddn
#126 3031379-126.patch60.65 KBheddn
#126 interdiff_123-126.txt856 bytesheddn
#123 interdiff.txt600 bytesMile23
#123 3031379_123_no_chmod.patch60.38 KBMile23
#123 3031379_123.patch60.42 KBMile23
#120 interdiff.txt5.6 KBMile23
#120 3031379_119.patch60.42 KBMile23
#116 interdiff_115-116.txt1.38 KBheddn
#116 3031379-116.patch59.74 KBheddn
#115 interdiff_111-114.txt2.56 KBheddn
#114 3031379-114.patch59.73 KBheddn
#114 3031379-111.patch58.31 KBheddn
#111 interdiff_107-111.txt3.87 KBheddn
#111 3031379-111.patch58.31 KBheddn
#107 3031379-107.patch58.41 KBheddn
#107 interdiff_106-107.txt4.42 KBheddn
#106 3031379-106.patch57.59 KBheddn
#106 interdiff_104-106.txt13.28 KBheddn
#104 interdiff.txt10.44 KBMile23
#104 3031379_104.patch61.08 KBMile23
#101 interdiff.txt30.5 KBMile23
#101 3031379_101.patch71.51 KBMile23
#99 3031379-98-to-99-interdiff.txt2.36 KBgreg.1.anderson
#99 3031379-99.patch44.94 KBgreg.1.anderson
#98 interdiff_97-98.txt510 bytesheddn
#98 3031379-98.patch45.2 KBheddn
#97 3031379-97.patch46.02 KBheddn
#97 interdiff_95-97.txt2.13 KBheddn
#95 3031379-94.patch45.69 KBheddn
#95 interdiff_92-94.txt2.31 KBheddn
#92 3031379-92.patch45.59 KBheddn
#92 interdiff_90-92.txt930 bytesheddn
#90 3031379-90.patch45.59 KBheddn
#90 interdiff_88-90.txt816 bytesheddn
#88 interdiff_86-88.txt8.14 KBheddn
#88 3031379-88.patch45.59 KBheddn
#86 3031379-86.patch44.84 KBheddn
#86 interdiff_83-86.txt1.86 KBheddn
#83 interdiff_80-83.txt1.32 KBheddn
#83 3031379-83.patch43.88 KBheddn
#80 3031379-77-to-80-interdiff.txt2.6 KBgreg.1.anderson
#80 3031379-80.patch43.6 KBgreg.1.anderson
#77 3031379-75-to-77-interdiff.txt771 bytesgreg.1.anderson
#77 3031379-77.patch43.6 KBgreg.1.anderson
#75 3031379-72-to-75-interdiff.txt1.51 KBgreg.1.anderson
#75 3031379-75.patch43.6 KBgreg.1.anderson
#72 3031379-71-to-72-interdiff.txt1.02 KBgreg.1.anderson
#72 3031379-72.patch43.59 KBgreg.1.anderson
#71 3031379-71.patch43.6 KBheddn
#70 3031379-69.patch41.6 KBheddn
#70 interdiff_67-69.txt2.78 KBheddn
#67 3031379-66.patch40.76 KBheddn
#67 interdiff_62-66.txt1.88 KBheddn
#62 interdiff_60-62.txt2.12 KBheddn
#62 3031379-62.patch40.77 KBheddn
#60 interdiff_59-60.txt1.68 KBheddn
#60 3031379-60.patch40.15 KBheddn
#58 3031379-58.patch40.23 KBheddn
#58 interdiff_57-58.txt1.94 KBheddn
#57 3031379-57.patch39.75 KBheddn
#49 interdiff.txt14.19 KBMile23
#49 3031379_49.patch40.15 KBMile23
#46 interdiff.txt18.97 KBMile23
#46 3031379_46_POC_tests.patch77.93 KBMile23
#46 3031379_46_framework_REVIEW_THIS.patch43.38 KBMile23
#42 3031379_42_framework.patch39.81 KBMile23
#42 3031379_42.patch73.38 KBMile23
#30 3031379_30_framework_only.patch40.13 KBMile23
#30 interdiff.txt12.53 KBMile23
#30 3031379_30.patch74.76 KBMile23
#28 3031379_28_framework_only.patch51.16 KBMile23
#28 3031379_28.patch68.7 KBMile23
#26 interdiff_framework_only.txt18.17 KBMile23
#26 3031379_26_framework_only.patch25.17 KBMile23
#26 interdiff.txt42.37 KBMile23
#26 3031379_26.patch69.11 KBMile23
#20 3031379_20.patch44.69 KBMile23
#15 interdiff.txt17.24 KBMile23
#15 3031379_15.patch22.6 KBMile23
#13 3031379_13.patch17.79 KBMile23
#12 3031379_12-do-not-test.patch107.76 KBMile23
#10 interdiff.txt1.05 KBMile23
#10 3031379_10.patch106.3 KBMile23
#8 interdiff.txt95.4 KBMile23
#7 3031379_7.patch106.27 KBMile23
#6 3031379_6.patch62.2 KBMile23
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

alexpott’s picture

alexpott created an issue. See original summary.

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

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

alexpott’s picture

Mile23’s picture

Mile23’s picture

Assigned: Unassigned » Mile23

At DrupalCon 2019 sprints we brainstormed a little about this issue and similar issue #2984031: Create Build Tests For Composer and Drupal

I think the relatively narrow scope of both these issues helps us reach a conclusion.

Let's think about this issue first, since it addresses an existing gap in our test infrastructure. We'll keep in mind that the solution should be flexible enough to address #2984031: Create Build Tests For Composer and Drupal and similar build-based issues.

We can also have follow-ups to move existing unit-test-scoped builders to use the framework we make here. This would include the demo umami installer and so forth.

Mile23’s picture

Assigned: Mile23 » Unassigned
Status: Active » Needs review
FileSize
62.2 KB

Tests should fail here. This is a proof of concept and is offered for discussion.

This patch adds a test type called BuildTestBase. The testsuite is 'build'.

The main concepts here are:

  • A way to locate a workspace directory.
  • A way to execute command line commands.
  • A way to assert that comands worked.
  • A way to use only the kernel to send 'requests.'

The use case for this is as outlined in the IS: Performing an installation and then performing and update, and then performing requests.

There's some infrastructure and one test: Drupal\BuildTests\BuildInstallTest. It looks like this:

  public function testInstall() {
    // Perform clone of drupalci's core repo into fixture dir.
    $this->assertCommand('git clone --branch 8.8.x --depth 1 https://git.drupalcode.org/project/drupal.git .');
    // composer install
    $this->assertCommand('composer install');
    // demo_install
    $this->assertCommand('php ./core/scripts/drupal install standard');
    // visit paths
    $this->kernelRequest($this->getWorkspaceDirectory(), new Request());
  }

This test fails locally like this:

$ ./vendor/bin/phpunit -c core/ --testsuite build
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.

Testing 

THE ERROR HANDLER HAS CHANGED!

This means the kernel request began to work, but the kernel happily changed the error handler without concern for phpunit-bridge's feelings. (Actually this doesn't mean that, because we never get beyond trying to load the autoloader.) You can see that I marked it @group legacy, and I also tried to disable the bridge, but no luck.

Edit: I neglected to add this testsuite to drupalci.yml so it won't run. Sigh.

Mile23’s picture

FileSize
106.27 KB

Composer lock file suitable for PHP 7.0, run-tests.sh --browser now works, truncated drupalci.yml.

Mile23’s picture

FileSize
95.4 KB

Forgot interdiff...

Status: Needs review » Needs work

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

Mile23’s picture

Status: Needs work » Needs review
FileSize
106.3 KB
1.05 KB

An artifact from the last test:

PHP Fatal error:  Cannot redeclare composerRequireDrupal8() (previously declared in /var/www/html/vendor/composer/autoload_real.php:67) in /tmp/build_workspace_be4e9c40d642212cb9b50c7bb19ee8e9/vendor/composer/autoload_real.php on line 74

So we use require_once instead.

Status: Needs review » Needs work

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

Mile23’s picture

FileSize
107.76 KB

Rethinking so we're using PHP's test server as the HTTP server.

Currently does not work. Sharing here for review.

Mile23’s picture

Status: Needs work » Needs review
FileSize
17.79 KB

Here's a working test framework. You'll see there are plenty of @todos.

The main problem that exists now is that PHP's HTTP server isn't shut down by the time the next test is run. This would be ameliorated by using semirandom port numbers (so they can overlap for concurrency), and/or by adding a wait() in the tearDown() method.

Status: Needs review » Needs work

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

Mile23’s picture

Status: Needs work » Needs review
FileSize
22.6 KB
17.24 KB

Converted to Mink so we can do nifty assertions. We don't use UiHelperTrait because it needs a bunch of core stuff that is inappropriate for this test type.

Tests created with this framework can add all sorts of dependencies, like being able to run git. So we might consider being careful about that as we move forward.

The main TODOs here:

  • Figuring out the timing on letting the PHP HTTP server spin up. I'm pretty sure that's why it's failing here.
  • The tests rely on having different port numbers for concurrency. Since they'll be running *from* different processes, we're almost guaranteed that there will be port collisions under run-tests.sh. So we need a way to ensure that ports aren't being recycled before their time.
  • The file workspaces are a little tricky to clean up due to permissions errors. Currently the workspace directories will just fill up /tmp.
  • Since the workspaces are under /tmp, we might need to make a mode where it figures out that they should be under sites/simpletest/. I'm not sure whether this is purely necessary.
  • It's going to be hard to turn any of the generated files into artifacts for DrupalCI. It might be that we don't want to, and only want outcomes.
  • This framework has zero dependencies on Drupal. It could be a component.

Status: Needs review » Needs work

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

alexpott’s picture

https://github.com/twigphp/Twig/pull/2993 does some really interesting stuff that we might be able to leverage.

Mile23’s picture

That does look interesting.

We'd still need to set up the PHP HTTP server and use assertCommand() and friends to create the environment, but functional tests could be blackfire player scripts.

https://github.com/blackfireio/player

It has a --json output option, but no idea how that would map to run-tests.sh or junit, or other sundry concerns.

Mile23’s picture

Discovered this while making some tests for this issue.

Mile23’s picture

Status: Needs work » Needs review
FileSize
44.69 KB

Now that I'm back from vacation...

Adds some prototype tests for existing needs. This helps us understand whether the framework meets our needs.

Still non-concurrent. We'll need to figure out a solution using a lock file to protect concurrent HTTP ports.

There's a little bit of trouble in that in order to do real black box testing of update.php, our mink client has to respond to meta refresh tags. UiHelperTrait does this with checkForMetaRefresh(), but we can't inherit the dependencies of UiHelperTrait. That means we can either split up the trait or re-roll a similar solution here. The latter would involve expanding GoutteDriver::visit() to perform refreshes, which might be a better solution that could replace that part of UiHelperTrait. Or not. Dunno.

It's also true that as of 4.2.0, Goutte (via BrowserKit) will follow meta refresh tags for us: https://github.com/symfony/symfony/pull/27118

Anyway, the meat of the framework is in core/tests/Drupal/Tests/BuildTests/Framework. The rest are essentially POC tests.

alexpott’s picture

@Mile23 amazing work.

Some thoughts:

  • We can copy the meta refresh code - and add an @todo to remove it when we require Symfony 4. DRY is fine but there are times to ignore it.
  • I'm not convinced that doing min max testing inside these tests as the latest patch adds for components is correct. If we look at travis and appveyor and probably many other CIs the build matrix is defined outside of test itself - as in what is being suggested in #2976407: Use drupalci.yml for composer dependency min/max testing - DO NOT COMMIT - just because we can do min max testing for components using this doesn't mean we should.
  • Re getting a port - we could create something similar to \Drupal\Core\Test\TestDatabase to get a port number between 8080 and 65,535 - I think we'll also need to check whether the port is in use as well.
+++ b/core/tests/Drupal/BuildTests/Dependencies/CoreDependencyRegressionTest.php
@@ -0,0 +1,41 @@
+    $this->markTestIncomplete('Core cant pass this test yet. See https://www.drupal.org/project/drupal/issues/2976407');

Why doesn't core pass this test? I look on the linked issue but it is far from immediately obvious to me. But any as above I'm not sure we should be using this test type for this test anyways.

Mile23’s picture

The core dependency regression test looks like this (once you comment out the line that marks it as incomplete):

$ ./vendor/bin/phpunit -c core/ --testsuite build --filter CoreDependencyRegressionTest

[...]

...........................................................  2773 / 17890 ( 15%)
...........................................................  2832 / 17890 ( 15%)
......................................
THE ERROR HANDLER HAS CHANGED!

Legacy deprecation notices (9)

ERROR: 

Failed asserting that 255 matches expected 0.

I was able to repro this on core like this:

$ git checkout 8.8.x
$ composer install
$ composer run-script drupal-phpunit-upgrade
$ ./vendor/bin/phpunit -c core/ --testsuite unit

THE ERROR HANDLER HAS CHANGED! is basically the least useful error message ever. :-)

Some hacking later and we discover that this error handler error goes away when we fix DrupalKernelTest: #3054649: DrupalKernelTest results in ERROR HANDLER CHANGED!

There are still a bunch of other fail/error tests, so we need to address those. But CoreDependencyRegressionTest is more of a proof of concept in this instance anyway.

I'm not convinced that doing min max testing inside these tests as the latest patch adds for components is correct.

Same deal... This patch only proves that the framework allows for that sort of thing, and all the tests outside of Drupal\BuildTests\Framework are POC, not for inclusion in the final patch. We can argue about them in other issues. :-)

Sorry if I didn't make that clear.

Mile23’s picture

Berdir’s picture

Not quite sure how the current work here ties back to issue summary, and whether that is still accurate, but, two thoughts about the current issue summary:

* The reason we didn't saw those extension issues in our update tests is that our dumps have empty caches. That would be relatively easy to address by creating a dump that includes all cache data, would just be quite a bit of data. Maybe we can keep it out of git or use that git LFS stuff that gitlab supports? Or somehow fetch it on demand from somewhere?
* You can't install old versions of Drupal 8 on modern PHP versions, 8.4 and older doesn't install on PHP 7.2 for example.

Mile23’s picture

The patch to review is in #20, and the stuff with fixing tests is a little off topic, yah.

I talked with @alexpott about this at DrupalCon and basically it's a black box test, where you do a build and then check expectations. This way we can perform an install -> upgrade -> see if we got what we want process.

There's a little scope creep in that I did some prototypes of other test types this build system would likely be used with, such as #2984031: Create Build Tests For Composer and Drupal and #2984068: Create a test that upgrades from one version of core to the next. and could be used for #2962157: TestSiteApplicationTest requires a database despite being a unit test

Mile23’s picture

Another new child: #3055495: Search page migration tests are misplaced kernel tests

This patch adds a subclass of Behat\Mink\Driver\Goutte\Client that can follow meta redirects, with some tests.

Improves port seeking for the HTTP server.

The big patch includes all the child issue patches which allow the composer min/max test to pass.

The framework_only patch does not include any of these changes, or the prototype tests.

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

Mile23’s picture

Did I crack the code of concurrent ports? Let's ask the testbot.

Edit: Woops, the framework_only patch isn't only the framework.

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

Mile23’s picture

The previous fail shows us this error which fails the tests which copy core:

RecursiveDirectoryIterator::__construct(/var/www/html/sites/simpletest/91070516/files/dir): failed to open dir: Permission denied

This is a fail because we don't have permission to do anything with sites/simpletest/dir/. And why is that? Because Drupal\Tests\migrate\Kernel\process\FileCopyTest::testNonWritableDestination() does not clean up after itself. #2793121-15: FileTestBase doesn't confirm its filesystem exists, does not clean up

So that leaves us with some options: Wait for FileCopyTest to get fixed or just never copy sites/simpletest/ to the system under test. The latter seems completely reasonable regardless of FileCopyTest, so there we are.

We add symfony/finder so it can make the file iterator for us, and then we copy core without sites/files/, sites/simpletest/, or vendor/.

This patch also adds the ability to say @requires externalCommand git in the test annotation, so it can skip instead of doing a bunch of build operations only to fail for obvious reasons.

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

jibran’s picture

I don't understand why this has to be part of core. Why can't we setup deadicated project/jobs to run this?

Mile23’s picture

Core maintainers seem reticent to trust a test that doesn't run under drupalci. For instance, this languishes, and I can only assume it's because Travis CI is inadmissable in court: #2876669-60: Fix dependency version requirement declarations in components

There was some small degree of work put into having different build targets within drupalci.yml. That way you could specify a different build process for RTBC or weekly regression test or whatever. Also languishes, not updated in a year, because its really only me and @mixologic: #2951375: Allow for build targets

So that leaves us with baking it into core, unless there's a better idea.

jibran’s picture

Core maintainers seem reticent to trust a test that doesn't run under drupalci.

Why can't we setup a new project on drupal.org?

Mile23’s picture

What sort of project?

alexpott’s picture

I don't understand why this has to be part of core. Why can't we setup deadicated project/jobs to run this?

Because if a change to core breaks this we shouldn't be committing it.

Note the vital part of this is not min max testing of our components. It's the ability to do proper update testing as a user would do updates.

Mile23’s picture

Note the vital part of this is not min max testing of our components. It's the ability to do proper update testing as a user would do updates.

That's stubbed out in Drupal\BuildTests\Update\UpdateTest, which demos updating from arbitrary releases to a newer release using git, and also updating from a release tag to the current codebase (including everything patched in DrupalCI).

It uses the Quickstart feature, which might not be ideal. Do you have any specs for how this update should occur?

jibran’s picture

It's the ability to do proper update testing as a user would do updates.

if a change to core breaks this we shouldn't be committing it.

I totally understand both points but it doesn't mean it has to be in Drupal core codebase. We can still set up a drupal CI job(along with the test runs) for each patch in Drupal core issue queue to do the following things:

It will install the minimum version of Drupal supported determined by our policy and the branch that is being tested \Drupal::VERSION
It will install Drupal codebase using composer
It will install Drupal somehow
It will log into the site as user 1
It will then be able to do upgrades via composer / drush / tarball?
It will then test the resulting upgrade.

+++ b/core/composer.json
@@ -63,6 +63,8 @@
+        "symfony/filesystem": "^3.4",
+        "symfony/finder": "~3.4",

We have 7 years old issue to add these components #1451320: Evaluate Symfony2's Finder Component to simplify file handling and we also have #3029088: Replace file system component with a 3rd party library. I think adding these components here just for this patch is not right. We need a wider discussion and other things to consider like #1451320-25: Evaluate Symfony2's Finder Component to simplify file handling here.

heddn’s picture

#3043235: [Policy, No patch] How to provide automated, in place security updates of Drupal core is an issue currently in a sandbox project. But parts will start land into drupal core over the next few releases. And in that case, we want to be able to download certain, fixed versions of Drupal and provide a mechanism to update to a newer release. We could _possibly_ work around that, but it would be ideal to run actual, in-place updates of the version of files in a project.

alexpott’s picture

@jibran

I think adding these components here just for this patch is not right.

We're not adding these components to core. We're adding them as dev requirements. This means that any project built on top of Drupal does not inherit them (well it would lovely if that was 100% true but things a very inside out in Drupal land). But the point is that we're not bringing them into core in the same way as a regular dependency which is what those issues are doing and can still do regardless of this issue.

With respect to

We can still set up a drupal CI job(along with the test runs) for each patch in Drupal core issue queue to do the following things:

I don't think what being suggested is realistic. Adding matrix builds to DrupalCI is not an inconsiderable amount of work. The association has many other priorities. We have a desperate need to prove updates and do them in as real a way as possible. And as @heddn points out we other things coming in the pipeline that really really need this functionality.

If someone is willing to put the work into DrupalCI to make it work that way or has a realistic architecture that can point to the needed changes and inspire others to work on it then great but otherwise I think we should proceed here. But before someone invests the time and energy to do that I might point out that the main contributor to this issue @Mile23. @Mile23 is one of the two main current contributors to drupalci and I trust that if this was simple to implement there they might be doing just that.

Mile23’s picture

Currently DrupalCI has command and container_command plugins. It's entirely possible to do all of this in either a script that is called by container_command, or as a series of container_command directives in a drupalci.yml.

So we could, conceivably, without changing DrupalCI at all, add a build phase to the current drupalci.yml which does all this stuff. It would run with every patch. It would be cumbersome but it's do-able.

The down-sides of doing that would be that if you're normalizing on using PHPUnit to run tests locally, you wouldn't be able to run these important update (or build regression) tests unless you learned how to run DrupalCI locally. We'd pretty much still have to wait for a patch run to see whether the changes broke updating.

The up-side is that someone would have to write it, and then maintain it. And I'm not sure either of those is really up-sides. Because when you write that script, you're basically re-implementing some of the build test suite I just wrote, or you're stretching the boundaries of container_command and DrupalCI scripting.

As far as adding a build matrix or other features to DrupalCI, it has a build matrix already for environments. Just start another test for whichever PHP version or DB. It's not scriptable, but it can be set up to be automatic.

As far as whether or not to use symfony/finder or symfony/filesystem, like @alexpott says these aren't in core, per se, and guess what? If we have a build system, it can build a Drupal site that is build as composer install --no-dev and find out whether someone did a no-no and used symfony/finder in production code or whatever. Without it we can't, full stop.

Also, we shouldn't use the library we're testing to build the SUT. That leaves bare-metal PHP and that makes the test system less maintainable.

And also also: symfony/finder, for instance, just speeds up development. That's another thing we don't have to test in this new infrastructure. However, if it's absolutely necessary that we not have rapid development on this thing, then review the patch and say, "Don't use symfony/finder." Then it will take much longer and result in twice as much code to maintain.

As far as development on the testbot, it seems to be mostly me and @mixologic, but really mostly @mixologic. So if you can clone @mixologic and get the clone on the task of telling me what's his priority, then maybe there's enough time in the day. Also I'm currently looking for work and so therefore the testbot gets low priority.

Mile23’s picture

Reroll and updated the core min/max test after #3008870: Drop support for PHPUnit 4.8 once PHP 5 is no longer supported (8.8.x)

Dunno how I missed @heddn's earlier comment in #39. #3043235-21: [Policy, No patch] How to provide automated, in place security updates of Drupal core looks like a pretty solid endorsement.

Combined with #2984031: Create Build Tests For Composer and Drupal this means that in addition to the existing update needs, two important initiatives also need a way to do this. Both should be scoped for running in a patch evaluation context. It's much easier to snap together some Symfony components with Mink to accomplish this than to add more infrastructure to DrupalCI, and the tests which use this framework would have a better DX than a private build or a shell script.

This would give us velocity on both Automatic Updates and Composer initiatives so maybe we'll accomplish one or both before 8.8.0 is released.

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

Mixologic’s picture

I'd chime in here that there is also an issue of boundaries and responsibilities.

DrupalCI has four primary responsibilities:
1. Construct an environment to test in
2. Build a system to be tested
3. Execute provided tests, and capture the results
4. Provide a UI to examine the results

This may seem like we're moving some of the "build a system to be tested" into core, but it really isn't. This gives core the capability to define a category of tests to prove installing/updating/building under a variety of scenarios works as expected. The boundary here is that drupalci cannot have the burden of defining the scenarios. Those must happen in core, and be defined by core developers. I have no way of knowing what is important to test.

And as far as I understand, this test type is intended to be a specific test, like "build the site on 8.6.6, and then upgrade it to 8.6.7 using composer and check a couple things to prove that succeeded" Vs "build the site on 8.6.6, upgrade it in place to 8.6.7, and then *run the whole test suite against the newly upgraded site*". That could be a very thorough, comprehensive exercise, but Im not sure thats the scope here.

If we're talking about the latter (running the existing test suite against a variety of scenarios), then we'll need to be *very* sure about how/what/when we do those kinds of tests, because that sounds like a lot more testing, and perhaps a lot of it that is not useful except in very specific circumstances.

Mile23’s picture

If we're talking about the latter (running the existing test suite against a variety of scenarios), then we'll need to be *very* sure about how/what/when we do those kinds of tests, because that sounds like a lot more testing, and perhaps a lot of it that is not useful except in very specific circumstances.

Yes, exactly.

The reason some of these demo tests run tests subsequently is to prove that they worked. They're also demos to prove the concept of a building a thing and then proving it works.

Again: The thing to review here is the _framework patches, not the full one.

If anyone has any suggestions about how to organize this better or specific use cases that are not addressed, please offer it so we can have some forward motion.

Mile23’s picture

This patch changes the way copyCodebase() works so that you can inject whatever iterator you please. By default it excludes site-specific files.

Because of this change, I'm including both the framework patch and the full proof-of-concept patch. In the future it'll just be the framework.

The patch also adds some tests to the framework.

Anyone please chime in with some suggestions or next steps. Thanks.

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

Status: Needs review » Needs work

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

Mile23’s picture

Status: Needs work » Needs review
FileSize
40.15 KB
14.19 KB

Some experimentation tells me that we only need to tell the PHP HTTP server to sleep for 5 seconds by default when it starts up.

This patch:

  • Fixes CS issues
  • Improves the BuildTestInterface docblock docs.
  • Adds getMink() to the interface since assertRequest() tells you to look at the Mink object.
Mixologic’s picture

Question: when we discovered that using symfony filesystem /finder could be replaced with newer versions because of a newer composer, we opted to use composer's api's for interacting with the filesystem. Is there any similar risks here?

Mile23’s picture

I think you're referring to #2982684-177: Add a composer scaffolding plugin to core which, yah, bad. That's referring to the version baked into the composer.phar file you run at the command line.

These tests don't operate under the context of a Composer plugin, so we're good.

The only caveat I can think of is if someone runs their tests with a command like composer run-script tests.

Mile23’s picture

Assigned: Unassigned » Mile23
Issue tags: +Needs reroll
Mile23’s picture

Assigned: Mile23 » Unassigned
Issue tags: -Needs reroll

I must have been trying to apply the patch to 8.7.x or something when I added that tag, because it applies now.

greg.1.anderson’s picture

Status: Needs review » Needs work

LGTM, just a few minor issues.

  1. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,365 @@
    +    // @todo Glean working directory from env vars, etc.
    

    @todo requires a follow-up issue (or just implement something reasonable).

  2. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,365 @@
    +    //   to fully start up.
    

    I used to use netstat -an for this. See wait-for-webserver script. Is netstat available on drupal-ci?

  3. +++ b/core/tests/Drupal/BuildTests/Framework/DrupalMinkClient.php
    @@ -0,0 +1,65 @@
    + * @todo Update this client when Drupal starts using Symfony 4.2.0+.
    

    Needs follow-up issue

  4. +++ b/core/tests/Drupal/BuildTests/Framework/ExternalCommandRequirementsTrait.php
    @@ -0,0 +1,98 @@
    +    // @todo Figure out the Windows version of this.
    

    On Windows, use `where`. See ExecTrait in Drush.

Mixologic’s picture

Netcat is what we use on the db containers:
while ! netcat -vz localhost 3306; do sleep 1; done

I can make sure netcat is installed on the php containers as well if we need that. (assuming this is running *inside* a container)

heddn’s picture

Issue tags: +Needs reroll

Let's first reroll this thing.

heddn’s picture

This picks up #54.3 and 54.4.

I'd argue for 54.1 we should remove the TODO as sys_get_temp_dir seems like a fine default location.

#54.2 could be a follow-up me thinks. Is netstat available on windows?

heddn’s picture

Looking into 54.2 right now. Found https://symfony.com/doc/current/components/process.html#running-processe.... Might be able to look for the results:

 php -S localhost:9090 -t .
PHP 7.2.21-1+0~20190807.25+debian9~1.gbp935ebf Development Server started at Wed Aug 28 20:37:50 2019
Listening on http://localhost:9090
Document root is /var/www/html
Press Ctrl-C to quit.

Let's test it out.

heddn’s picture

Seems we have an older symfony component. But there seems to be a comparable means. Here's a test.

Status: Needs review » Needs work

The last submitted patch, 60: 3031379-60.patch, failed testing. View results

heddn’s picture

Status: Needs work » Needs review
FileSize
40.77 KB
2.12 KB
Mixologic’s picture

Re #58 these will run in parallel, so we need to make sure whatever working dirs are out there share nothing with any other simultaneously running test.

Mile23’s picture

+++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
@@ -0,0 +1,364 @@
+    // @todo Glean working directory from env vars, etc.
+    $this->workspaceDir = sys_get_temp_dir() . '/build_workspace_' . md5($this->getName() . microtime());

Workspace directories per process have a hashed dir name. The idea with the todo was that if drupalci tells us an environmental variable for the temp directory, we should use that. Leaving it alone right now is probably OK, though, because I think this issue needs more consensus before we fill in too many details.

Status: Needs review » Needs work

The last submitted patch, 62: 3031379-62.patch, failed testing. View results

greg.1.anderson’s picture

If drupalci would just make one temp directory for every test, and configure sys_get_temp_dir() to return it, it would make it much easier to avoid contention between different tests.

heddn’s picture

Status: Needs work » Needs review
FileSize
1.88 KB
40.76 KB

The php process seems to be special and know when it is attached to a tty terminal or not. But I think we have all the building blocks already. Let's try this on for size. It passes on local.

Mixologic’s picture

Re #66 -> that would work, but we should do that in run-tests.sh, not drupalci.

greg.1.anderson’s picture

#68: Good idea. Here's an implementation that might suffice. Feel free to continue from #67 if this is out of scope for this issue.

Do we have a better alternative than adding simpletest_rmdir() here? I wasn't sure if it was safe / prudent to use Symfony components in run_tests.sh.

heddn’s picture

Here's some additional test coverage and demonstrates a more typical test scenario (along my thinking) of how to use this new build type.

Oops, cross post w/ #69. I'll merge them next.

heddn’s picture

greg.1.anderson’s picture

The last submitted patch, 70: 3031379-69.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 72: 3031379-72.patch, failed testing. View results

greg.1.anderson’s picture

Status: Needs work » Needs review
FileSize
43.6 KB
1.51 KB

microtime() has a space in it, so we'll use microtime(true) instead.

Status: Needs review » Needs work

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

greg.1.anderson’s picture

greg.1.anderson’s picture

Status: Needs work » Needs review

... and setting the status.

Status: Needs review » Needs work

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

greg.1.anderson’s picture

Status: Needs work » Needs review
FileSize
43.6 KB
2.6 KB

Code style and minor cleanup.

Status: Needs review » Needs work

The last submitted patch, 80: 3031379-80.patch, failed testing. View results

greg.1.anderson’s picture

Build failing with:

15:02:43 Build step 'Publish JUnit test result report' changed build result to UNSTABLE
15:02:43 [Deprecation] [-ERROR-] No files found for pattern 'jenkins-drupal_patches-6699/artifacts/phpstan/*.xml'. Configuration error?

Did not search to find out where that's coming from.

Status: Needs review » Needs work

The last submitted patch, 83: 3031379-83.patch, failed testing. View results

greg.1.anderson’s picture

@heddn If you want to back out my addition so that the tests pass, that would be fine. I'm not sure why making a dedicated temporary directory interfered with asset collection. Maybe I deleted the temporary directory too soon. Perhaps someone else can help with this.

heddn’s picture

Status: Needs work » Needs review
FileSize
1.86 KB
44.84 KB

The lates failures seem to be from my test additions. I'm limiting the testing scope in drupalci.yml since otherwise takes a really long time when we can be pretty sure its just this new test type we want to turn green.

Status: Needs review » Needs work

The last submitted patch, 86: 3031379-86.patch, failed testing. View results

heddn’s picture

Status: Needs work » Needs review
FileSize
45.59 KB
8.14 KB

Fewer failures. And some minor refactoring for DX.

Status: Needs review » Needs work

The last submitted patch, 88: 3031379-88.patch, failed testing. View results

heddn’s picture

Status: Needs work » Needs review
FileSize
816 bytes
45.59 KB

Status: Needs review » Needs work

The last submitted patch, 90: 3031379-90.patch, failed testing. View results

heddn’s picture

Status: Needs work » Needs review
FileSize
930 bytes
45.59 KB

Status: Needs review » Needs work

The last submitted patch, 92: 3031379-92.patch, failed testing. View results

greg.1.anderson’s picture

Set sendmail_path = "true" in php.ini to avoid errors sending email when installing a site. Unfortunately, this setting cannot be set via ini_set(); it has to be done in php.ini or via -d on the commandline.

heddn’s picture

Status: Needs work » Needs review
FileSize
2.31 KB
45.69 KB

I had the wonderful help of a colleague assist here. drush uli has an extra enter added to it that was messing up the http request and resulting in a 404. This should fix it now.

It was requesting:
http://localhost:8000/user/reset/1/1567199950/bwW9hwnekXsqOTBRWlbdrILBhT9uzBxg5DC-0Uf4Kz8/login\n

Notice the newline character "\n" in the URL at the end? It isn't easily visible in xdebug.

Status: Needs review » Needs work

The last submitted patch, 95: 3031379-94.patch, failed testing. View results

heddn’s picture

re #94: switching to executeCommand instead. It will fix the sendmail thing and let us install on php 7.2 even w/ warnings about the installer not supporting PHP 7.2.

re 96: this is about the drush uli command in the tests. I was hitting my head against a specific bug on my local for a really long time as described in that comment.

And hopefully this passes green.

greg.1.anderson’s picture

Use Symfony Filesystem to remove our temp directory, since run_tests.sh is already loading the autoload file, and it is therefore available.

Clean up the 'uli' output a bit. It turns out that uli does not support output formatters, so `--format=json` is a non-starter.

jibran’s picture

Have we considered postponing this on #2982680: Add composer-ready project templates to Drupal core? Once that issue is committed then we can install multiple version of Drupal via composer to whatever folder we want using .drupalci.yml and simple upgrade script in the core can update the core the latest dev, run the upgrades and tests.

Mile23’s picture

+++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
@@ -0,0 +1,191 @@
+ * @requires externalCommand drush
...
+  public function testSiteUpdate() {
...
+    $this->executeCommand('drush site-install --db-url=sqlite://db.sqlite -y');
...
+    $this->assertCommand('drush updb -y');

I'm not sure where this was introduced, but a big ol' -1 on this for a couple of reasons:

1) The whole point of this test framework is that we can test Drupal core's install and update systems on a brand new codebase. If we let drush do that, then it's not the test we actually want. In fact, the reason we have an HTTP server in there is so we can test update.php.

2) You've added drush as a dependency of a unit test of BuildTestBase, which compounds the issue in #1 above.

Back in #46 there were tests of installing and updating core. I took them out so potential reviewers could concentrate on the framework instead of the test cases.

It seems like folks want the framework, so I've added those PoC tests back so we can talk about what sorts of test cases this framework should and shouldn't support.

Also moves the drush install/update test to its own test class.

greg.1.anderson’s picture

Yes, regarding Drush, wouldn't it be better to use Drupal\Core\Command\InstallCommand instead of drush si? Similarly, it seems it would be worthwhile to also have an updatedb command in core.

As for min/max testing, I am wondering if perhaps we should hold off on this until we have a way to run the min tests in parallel. The existing test suite already takes over an hour to run; that duration could increase substantially if we reran a substantial portion of the suite in series.

Mile23’s picture

Status: Needs review » Needs work
Issue tags: +Needs followup

That's the thing: The PoC tests are PoC and not the actual tests. :-)

The one that's in the patch does in fact use the install command. Check QuickStartTestBase. And again: While it would be nice to have an updatedb command, the raison d'etre for this test framework is to test existing update systems in core. While it's possible that making (and testing) the command would give some tertiary benefits for people who use update.php, the point of this issue is testing existing stuff.

So I'll add needs followup here, so we can go on to write actual tests based on what we land on in this framework-based issue.

The min/max testing part is somewhat contentious here and in other min/max issues, like this one: #2976407: Use drupalci.yml for composer dependency min/max testing - DO NOT COMMIT So I'm setting to needs work to remove the component and dependency regression tests. According to the log, all of the build tests took 4 minutes total, which includes re-running some unit tests with different dependencies. But adding it back can be a follow-up.

17:35:23 All tests will run.
17:35:23 
17:35:23 Test run started:
17:35:23   Saturday, August 31, 2019 - 17:35
17:35:23 
17:35:23 Test summary
17:35:23 ------------
17:35:23 
17:35:24 Drupal\BuildTests\Dependencies\CoreDependencyRegressionTest    1 passes                                      
17:35:24 Drupal\BuildTests\TestSiteApplication\InstallTest              1 passes                                      
17:35:24 Drupal\BuildTests\Framework\Tests\DrupalMinkClientTest        14 passes                                      
17:35:24 Drupal\BuildTests\Framework\Tests\ExternalCommandRequirement   2 passes                                      
17:35:24 Drupal\BuildTests\Framework\Tests\BuildTestTest                5 passes                                      
17:35:38 Drupal\BuildTests\Dependencies\ComponentDependencyBoundsTest   5 passes                                      
17:38:02 Drupal\BuildTests\Update\UpdateSiteDrushTest                   1 passes                                      
17:38:21 Drupal\BuildTests\Update\UpdateTest                            4 passes                                      
17:39:04 Drupal\BuildTests\QuickStart\InstallTest                       3 passes                                      
17:39:20 
17:39:20 Test run duration: 3 min 57 sec
17:39:20 
Mile23’s picture

Status: Needs work » Needs review
FileSize
61.08 KB
10.44 KB

Removes the dependency tests and their fixtures.

Next question: Should this framework know about the database under test? That is: We probably want to install a test site using MySQL instead of the SQLite that is used by default in the quickstart site. How should that work?

heddn’s picture

I'm fine w/ removing the additional tests. I probably just got too excited. I'm going to do one final tweak to the drush build test here, then strip them out in the next comment so we can do what @Mile23 suggests in #101. I'd really rather have the framework then have us block ourselves on bikeshedding the contents of futher tests.

heddn’s picture

Actually, the PoC tests are in really, really nice shape. It seems a shame to strip them. How about some minor tweaks? I say minor, because as always, the tests written by Paul are already in very good shape.

heddn’s picture

FileSize
4.42 KB
58.41 KB

Some minor comment improvements. Who else needs to review this at this point to get us to RTBC?

heddn’s picture

diff --git a/core/tests/Drupal/BuildTests/Update/UpdateTest.php b/core/tests/Drupal/BuildTests/Update/UpdateTest.php
index 46235aaa41..e4bc50d76b 100644
--- a/core/tests/Drupal/BuildTests/Update/UpdateTest.php
+++ b/core/tests/Drupal/BuildTests/Update/UpdateTest.php
@@ -6,10 +6,7 @@
 use Symfony\Component\Filesystem\Filesystem;
 
 /**
- * Test updates to core.
- *
- * This is a proof-of-concept test for the build test framework. Any resemblance
- * to actual tests, living or dead, is purely coincidental.
+ * Test updates to core using update.php

This update should happen if we re-roll this again. Or if not, on commit.

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

Status: Needs review » Needs work

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

heddn’s picture

Status: Needs work » Needs review
FileSize
58.31 KB
3.87 KB

The first failure is an easy fix.

The failure for the site application I'm able to track down to the following error. I saw this a couple times earlier in my local testing but don't know what made it go away or if I did things differently and then it went away. For now I've marked that test as a WIP. Maybe someone else has some suggestions?

$ php ./core/scripts/test-site.php install --base-url=http://localhost:8001 --db-url=sqlite://localhost/foo.sqlite --install-profile=minimal --json
PHP Warning:  Class 'PHPUnit\Framework\AssertionFailedError' not found in /private/var/folders/2h/rkh_prld5gl2tftzbst02cc40000gn/T/build_workspace_5e91481e47f92248ad5178315cc98471/core/tests/bootstrap.php on line 190
PHP Stack trace:
PHP   1. {main}() /private/var/folders/2h/rkh_prld5gl2tftzbst02cc40000gn/T/build_workspace_5e91481e47f92248ad5178315cc98471/core/scripts/test-site.php:0
PHP   2. require_once() /private/var/folders/2h/rkh_prld5gl2tftzbst02cc40000gn/T/build_workspace_5e91481e47f92248ad5178315cc98471/core/scripts/test-site.php:19
PHP   3. class_alias() /private/var/folders/2h/rkh_prld5gl2tftzbst02cc40000gn/T/build_workspace_5e91481e47f92248ad5178315cc98471/core/tests/bootstrap.php:190

Warning: Class 'PHPUnit\Framework\AssertionFailedError' not found in /private/var/folders/2h/rkh_prld5gl2tftzbst02cc40000gn/T/build_workspace_5e91481e47f92248ad5178315cc98471/core/tests/bootstrap.php on line 190

Call Stack:
    0.0004     391792   1. {main}() /private/var/folders/2h/rkh_prld5gl2tftzbst02cc40000gn/T/build_workspace_5e91481e47f92248ad5178315cc98471/core/scripts/test-site.php:0
    0.0014     429504   2. require_once('/private/var/folders/2h/rkh_prld5gl2tftzbst02cc40000gn/T/build_workspace_5e91481e47f92248ad5178315cc98471/core/tests/bootstrap.php') /private/var/folders/2h/rkh_prld5gl2tftzbst02cc40000gn/T/build_workspace_5e91481e47f92248ad5178315cc98471/core/scripts/test-site.php:19
    0.7112    1311272   3. class_alias() /private/var/folders/2h/rkh_prld5gl2tftzbst02cc40000gn/T/build_workspace_5e91481e47f92248ad5178315cc98471/core/tests/bootstrap.php:190
<snip/>
    5.7568   44862752  17. Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy->setId() /private/var/folders/2h/rkh_prld5gl2tftzbst02cc40000gn/T/build_workspace_5e91481e47f92248ad5178315cc98471/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php:167
    5.7568   44862752  18. session_id() /private/var/folders/2h/rkh_prld5gl2tftzbst02cc40000gn/T/build_workspace_5e91481e47f92248ad5178315cc98471/vendor/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php:94

{"db_prefix":"test37124925","user_agent":"simpletest37124925:1567450397:5d6d651de85848.28066836:iG2f4psQ0MOufHSECa_06xUus3tjpje_svP2sT1oIr8","site_path":"sites\/simpletest\/37124925"}
Mile23’s picture

Re: #106 Thanks. :-) We'll have these patches as starting points when we get to actual test implementations, assuming they're needed at all.

Status: Needs review » Needs work

The last submitted patch, 111: 3031379-111.patch, failed testing. View results

heddn’s picture

Well, rubber ducking this... I think the fix for the errors in #111 are pretty trivial. Patch attached.

But it still fails because we set base-url in TestSiteInstallCommand. Then don't really use it for what we need it, which is finding the site back again in a subsequent visit. So, fixes for that are also attached.

heddn’s picture

Status: Needs work » Needs review
FileSize
2.56 KB

Here's the proper interdiff.

Lendude’s picture

Really nice that this is green!

Most important first step I think is an update to the IS, lots of good discussions in the comments, we should see some of the conclusion in the IS. It's a little hard to determine the scope of what is being done here, with people getting excited and adding all sorts of tests (this is great btw!). But should we be aiming for something a little smaller in scope and adding more test cases later? Without the IS being up to date, its a little hard to pin down if the tests are all needed or are just cool applications of the new test framework.

Just some things I saw:

  1. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,383 @@
    + * Allows tests which need to build things in the file system.
    

    This really doesn't help me understand what this does :)

  2. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,383 @@
    +  private $workspaceDir;
    ...
    +  protected $hostPort;
    

    The use of protected vs private seems a bit messy. I don't see any specific logic to this. In general we make everything protected (I'm not saying this is a good thing, I'm just saying..). But here some have getters and are protected and some are private and don't have a getter and vice versa.

  3. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,383 @@
    +        // Don't fail build if chmod doesn't work.
    

    If it's not important that chmod works, why are we doing it in the first place?

  4. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,383 @@
    +  protected function initMink() {
    

    needs a doc block

  5. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,383 @@
    +    // Synchronously run the process.
    +    $process->run();
    

    Synchronously with what? We can probably just leave the comment out.

  6. +    $this->assertStringStartsNotWith('http://', $request_uri, $message);
    +    $this->assertStringStartsNotWith('https://', $request_uri, $message);
    

    should we test for a starting '/' too?

  7. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,383 @@
    +    $this->assertEquals((int) $expected_status_code, (int) $actual_code, $message);
    

    casting to int seems redundant, is it?

  8. +++ b/core/tests/Drupal/BuildTests/QuickStart/InstallTest.php
    --- /dev/null
    +++ b/core/tests/Drupal/BuildTests/QuickStart/QuickStartTestBase.php
    
    +++ b/core/tests/Drupal/BuildTests/QuickStart/QuickStartTestBase.php
    @@ -0,0 +1,43 @@
    +class QuickStartTestBase extends BuildTestBase {
    

    Missing doc blocks for class, methods and props

  9. +++ b/core/tests/TestSuites/BuildTestSuite.php
    @@ -0,0 +1,27 @@
    +   * Factory method which loads up a suite with all functional tests.
    

    'all build tests'

  10. +++ b/core/tests/bootstrap.php
    @@ -133,10 +133,11 @@ function drupal_phpunit_get_extension_namespaces($dirs) {
    -  /** @var \Composer\Autoload\ClassLoader $loader */
    +  /* @var \Composer\Autoload\ClassLoader $loader */
    

    unrelated change

  11. +++ b/core/tests/Drupal/BuildTests/Framework/DrupalMinkClient.php
    @@ -0,0 +1,66 @@
    +      $this->redirects[serialize($this->history->current())] = TRUE;
    

    \Symfony\Component\BrowserKit\Client::$redirects is a private property, so this won't work right?

Mile23’s picture

Assigned: Unassigned » Mile23

Thanks, @Lendude. Working on it.

Mile23’s picture

Assigned: Mile23 » Unassigned
Issue summary: View changes
Issue tags: -Needs issue summary update

Updated IS. No doubt it will evolve further.

#117.2: workspaceDir and serverProcess are private because you should interact with getWorkspaceDirectory() and standUpServer()/stopServer(). This is the kind of situation where the Drupal culture is wrong. :-)

#117.11: The code does manage to follow meta redirects, and DrupalMinkClientTest is passing... Will require some more attention.

Tried to address everything else.

After the test build, we should set this to NW to look at DrupalMinkClient per #117.11.

Mile23’s picture

::facepalm::

Status: Needs review » Needs work

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

heddn’s picture

+++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
@@ -113,21 +118,20 @@ protected function setUp() {
+      $fs->chmod($ws, 0775, 0000, TRUE);

I ran into issues with chmod on my local for symlinked copies of certain files. Chmod failed, but the remove worked just fine. Do we need chmod at all? Why is it there?

Mile23’s picture

Do we need chmod at all? Why is it there?

It's there because core will chmod files and then you can't delete them.

The reason it happens to the workspace directory instead of just sites/ is because we allow the test to say it has a working_dir within the workspace directory, so there could be an infinite number of sites/ directories that won't get chmodded and will thus break the teardown phase.

After I fixed my own silly fs error above, I see fails like this:

1) Drupal\BuildTests\TestSiteApplication\InstallTest::testInstall
Symfony\Component\Filesystem\Exception\IOException: Failed to chmod file "/var/folders/2l/sr2nx_yj4d3fb8fxfr0rbc680000gn/T/build_workspace_497a904b095822bc0cb51b0bf1a4b384/vendor/bin/composer".

So that's vendor/bin/composer which is an alias, which apparently causes $fs->chmod() to freak out.

We can't say "chmod everything except vendor/" because of the working_dir concept above. vendor could be inside any working directory.

So here's the fixed version of #120 and also the fixed version without the chmod in tearDown(). Let's see what happens.

Status: Needs review » Needs work

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

heddn’s picture

heddn’s picture

Let's see how it gets treated. Here we keep chmod but remove symlinks.

heddn’s picture

+++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
@@ -120,9 +120,14 @@ protected function tearDown() {
+        return $file->isLink() ? FALSE : TRUE;

This should be:

-        return $file->isLink() ? FALSE : TRUE;
+        return !$file->isLink();
heddn’s picture

FileSize
60.63 KB
833 bytes
Mile23’s picture

+1 on the concept. Thanks, @heddn.

Status: Needs review » Needs work

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

heddn’s picture

Status: Needs work » Needs review
FileSize
586 bytes
60.64 KB

I'm confused if this works.

Status: Needs review » Needs work

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

Mile23’s picture

Status: Needs work » Needs review
FileSize
61.63 KB
2.63 KB

Let's experimentally find out if we can't chmod sites/default/default.services.yml because it belongs to a different user.

Please never commit this. :-)

Status: Needs review » Needs work

The last submitted patch, 133: 3031379_133.patch, failed testing. View results

Mile23’s picture

Status: Needs work » Needs review
FileSize
61.63 KB
829 bytes

Derp. Let's see if we can't remove the config file because it's a different user.

Please never commit this either.

Edit: Yes, under drupalci it's a different user.

Status: Needs review » Needs work

The last submitted patch, 135: 3031379_135.patch, failed testing. View results

heddn’s picture

Assigned: Unassigned » heddn

That last failure actually makes a lot of sense. We're using a console command to install. So it will run as the root user. However, for deleting, we don't really care about _file_ permissions. We want to alter directory permissions. Let's adjust the iterator to only find directories. Working on that.

heddn’s picture

Assigned: heddn » Unassigned
Status: Needs work » Needs review
FileSize
61.63 KB
975 bytes

Status: Needs review » Needs work

The last submitted patch, 138: 3031379-138.patch, failed testing. View results

heddn’s picture

Status: Needs work » Needs review
FileSize
647 bytes
61.64 KB

Status: Needs review » Needs work

The last submitted patch, 140: 3031379-140.patch, failed testing. View results

heddn’s picture

FileSize
61.69 KB
heddn’s picture

FileSize
61.59 KB
heddn’s picture

Not to self: check into why we have some PHP 7.4 failures in #142.

heddn’s picture

FileSize
61.49 KB
heddn’s picture

Status: Needs work » Needs review
FileSize
60.67 KB
2.27 KB

That seemed to fix things. Here's an interdiff back to 135.

heddn’s picture

Status: Needs review » Needs work
  1. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,392 @@
    +      $this->fail('Process failed with exit code: ' . $process->getExitCode() . ' Message: ' . $ex->getMessage());
    

    $process is null in the case of this exception being thrown. This should be fixed.

  2. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,392 @@
    +    $server = [
    +      $this->phpFinder->find(),
    +      '-S',
    +      $hostname . ':' . $port,
    +      '-t',
    +      $full_docroot,
    +    ];
    ...
    +      ->start();
    

    We need to use .ht.router.php instead of -t $full_docroot.

    It was added for a reason and should be used for all those reasons. See https://www.drupal.org/node/2913019

heddn’s picture

Also the built in web server is single threaded. For many things, this is OK. But we should describe the limitations that this gives to a test. Things that result in a new HTTP request to the local server (ajax, http callbacks) will timeout and fail.

heddn’s picture

FileSize
60.68 KB

First a re-roll.

heddn’s picture

Status: Needs work » Needs review
FileSize
4.1 KB
60.67 KB

Another item of note, the .ht.router.php file was only added in 8.5.0, we won't be able to test with earlier versions of Drupal and the build test. I think that is probably OK.

heddn’s picture

+++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
@@ -0,0 +1,400 @@
+   * @param string|null $docroot

We typehint an optional null, but then still require it. I figured this out after dogfooding this patch. Let's clean this up and make it truly optional.

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

Status: Needs review » Needs work

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

heddn’s picture

Status: Needs work » Needs review
FileSize
4.18 KB
4.18 KB
heddn’s picture

FileSize
61.21 KB

Status: Needs review » Needs work

The last submitted patch, 155: 3031379-154.patch, failed testing. View results

heddn’s picture

Status: Needs work » Needs review
FileSize
1.84 KB
61.23 KB
heddn’s picture

Yeah, back to green.

Ghost of Drupal Past’s picture

This is incredible. Wow. I am totally floored. It'd help Drupal adoption so much to know updates are much smoother. Yay! We don't dare to go 8.7 right now, the update issues in the core tracker scare us and this is why I am reviewing this: Smartsheet needs this.

I can't say I have read this throughly but I tried to be useful and not persnickety. Feel free to ignore it all.

  1. +++ b/core/composer.json
    @@ -22,6 +22,8 @@
    +        "symfony/filesystem": "~3.4.0",
    

    Shouldn't these two be in require-dev?

  2. +++ b/core/scripts/run-tests.sh
    @@ -865,9 +866,12 @@ function simpletest_script_run_one_test($test_id, $test_class) {
    +  $tmp_dir = FileSystem::getOsTemporaryDirectory() . '/run_tests_' . rand(10000,99999) . microtime(TRUE);
    
    +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,399 @@
    +    $this->workspaceDir = sys_get_temp_dir() . '/build_workspace_' . md5($this->getName() . microtime(TRUE));
    

    I just saw FileSystem::getOsTemporaryDirectory being used...

  3. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,399 @@
    +    $message = 'URI should be relative. Example: some/path?foo=bar';
    

    That's odd. I'd expect /some/path?foo=bar. One, $_SERVER['REQUEST_URI'] starts with a slash so I'd expect something called $request_uri would similarly do so. Two, D8 by and large uses paths starting with a slash.

  4. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,399 @@
    +    $request = 'http://localhost:' . $this->getPortNumber() . '/' . $request_uri;
    

    Yeah, just drop the slash from here.

  5. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,399 @@
    +    // Wait until the web server has started. It is started if the port is no
    

    Or some other process have started on it. I'd guess we only need to consider our own processes, do we need simple file based locking for ports?

  6. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,399 @@
    +    // Use implode() because $working_dir can be NULL.
    

    Yeah but implode casts NULL to empty string same as concat does: https://3v4l.org/Z1rUG

  7. +++ b/core/tests/Drupal/BuildTests/Framework/ExternalCommandRequirementsTrait.php
    @@ -0,0 +1,115 @@
    +    $command = $this->isWindows() ? "where $command" : "which $command";
    

    https://github.com/symfony/process/blob/master/ExecutableFinder.php ?

  8. +++ b/core/tests/Drupal/BuildTests/Framework/Tests/DrupalMinkClientTest.php
    @@ -0,0 +1,78 @@
    +      'drupal-1' => ['<html><head><meta http-equiv="Refresh" content="0; URL=/update.php/start?id=2&op=do_nojs" /></body></html>', 'http://www.example.com/update.php/start?id=2&op=do_nojs'],
    

    We are hardwiring the expectation that MySQL has an autoincrement of 1. Of course that's OK but, hrm, hrm could we use \d+ here instead somehow?

  9. +++ b/core/tests/Drupal/BuildTests/Update/UpdateTest.php
    @@ -0,0 +1,97 @@
    +    file_put_contents($this->getWorkspaceDirectory() . '/sites/default/settings.php', '$settings[\'update_free_access\'] = TRUE;', FILE_APPEND);
    

    Move the contents into a variable and use heredoc? Or is it just me who breaks out in hives seeing \'? It's 100% fine to say "that's a you problem".

  10. +++ b/core/tests/Drupal/BuildTests/Update/UpdateTest.php
    @@ -0,0 +1,97 @@
    +    $assert->responseHeaderEquals('X-Generator', 'Drupal 8 (https://www.drupal.org)');
    

    Channeling my inner Gábor Hojtsy, do we want another \d+ here as well? :)

  11. +++ b/core/tests/bootstrap.php
    @@ -133,10 +133,11 @@ function drupal_phpunit_get_extension_namespaces($dirs) {
    +  /* @var \Composer\Autoload\ClassLoader $loader */
    

    That's strange, /** is the standard doxygen opener.

Mile23’s picture

Thanks for the review. Here's some easy responses while I'm not at the work machine:

3,4: Yah, either way. Starting with a slash is probably fine. Looking at our routing.yml files, I see we standardized on that, too.

5: We always manage the process. Ports are ‘locked’ by trying to use them and then failing, so we go on to the next one.

8: We’re only testing whether our mink client correctly extracts the meta for a refresh, not whether those URLs are valid or otherwise for their purpose. That URL looks like a typical batch process that should generate a refresh.

9,10: UpdateTest is a PoC test to show that the framework meets our needs. Only the tests in Drupal\BuildTests\Framework\Tests\ are actually testing the framework itself. The others will be scrapped before the commit, so we can bikeshed them in other issues. :-)

11: Tell it to NetBeans. :-)

catch’s picture

Read through fairly quickly. No useful feedback apart from the nits below and to say this looks amazing.

  1. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,399 @@
    +  /**
    +   * Make a local test server using PHP's internal HTTP server.
    +   *
    

    Extremely minor but tense should be 'Makes...'.

  2. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestInterface.php
    @@ -0,0 +1,177 @@
    +  /**
    +   * Assert that a command runs without error.
    +   *
    

    Same tense issues here.

  3. +++ b/core/tests/Drupal/BuildTests/Framework/Tests/ExternalCommandRequirementTest.php
    @@ -0,0 +1,78 @@
    +      $this->fail('The external command should be available, so we should not have skipped.');
    

    Should this include 'available_command' so it's obvious what the fail is?

  4. +++ b/core/tests/Drupal/BuildTests/QuickStart/InstallTest.php
    @@ -0,0 +1,49 @@
    +
    +/**
    + * Test whether we can install a Drupal site using the quickstart CLI.
    + *
    

    Wow.

  5. +++ b/core/tests/Drupal/BuildTests/Update/UpdateSiteDrushTest.php
    @@ -0,0 +1,67 @@
    +    $this->assertCommand('git stash');
    +    // We have to fetch the tags for this shallow repo. It might not be a
    +    // shallow clone, therefore we use executeCommand instead of assertCommand.
    +    $this->executeCommand('git fetch --unshallow  --tags');
    +    $this->assertCommand("git checkout 8.5.0");
    +    $fs = new Filesystem();
    +    $fs->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700, 0000);
    +    $this->assertCommandErrorOutputContains('Generating autoload files', 'COMPOSER_DISCARD_CHANGES=true composer install --no-dev --no-interaction');
    +    $this->executeCommand('drush site-install --db-url=sqlite://db.sqlite -y');
    +
    

    wow.

  6. +++ b/core/tests/Drupal/BuildTests/Update/UpdateSiteDrushTest.php
    @@ -0,0 +1,67 @@
    +
    +    // Perform the update steps.
    +    $this->assertCommand('drush updb -y');
    +
    

    There's obviously no content in the freshly installed database, so we should have a follow-up to create nodes, terms, menu content links etc. and update with those.

+++ b/core/tests/Drupal/BuildTests/Update/UpdateTest.php
@@ -0,0 +1,97 @@
+
+  public function provideVersions() {
+    return [
+      ['8.6.x'],
+      ['8.7.x'],
+      ['8.8.x'],
+    ];
+  }
+

This is really quite something and opens up so many possibilities.

heddn’s picture

Assigned: Unassigned » heddn

I'll work on the last few points.

heddn’s picture

This should cover the outstanding items from #159-161.

Note, I added a new assertion for confirming the site is a drupal site based on all the places we seem to have those 3 or 4 lines copied. And while doing that, it caused me to consider that we should also make findAvailablePort and instantiateServer part of the public interface. Reason is that to work around php's built-in server only allowing a single thread, we need an easy way for consumers to spin up new servers.

Making it public lets us the following pattern:

    $new_port = $this->findAvailablePort();
    $new_server = $this->instantiateServer(static::$hostName, $new_port);
heddn’s picture

FileSize
21.1 KB
64.11 KB
Mile23’s picture

Re: #163: That's why it's not part of the interface, specifically so you know you shouldn't do that. That way you write an assertion instead of starting up the server. We already have one process-isolated server per test class, and a test class shouldn't have multi-threading server needs. IOW: Don't write tests more complicated than that.

Or is there something I'm not getting?

Ghost of Drupal Past’s picture

This is absolutely lovely, thanks for the quick update! I still have the concern where two tests running on the same machine in parallel will try to use the same port. If they start at the same time, they will find the same port open, attempt to start a server and one of them will succeed and both of them will find the port is no longer available so they will both presume its their server and then all sort of brokenness ensues. It might be I am misunderstanding the mechanism of course (I certainly misunderstood a unit test above to be functional) or it can be we no longer run tests in parallel so these concern might be moot.

heddn’s picture

Working on #165/166.

heddn’s picture

FileSize
3.29 KB
63.9 KB

Quick reverse of api additions re: #163/#315. #166 up next.

Mile23’s picture

Re #166: Parallel HTTP happens by assigning different ports to different tests/docroots. They operate on a round-robin query against a range of port numbers. If the port is open, that means another process has already claimed it and we can't use it.

If it's a race for querying if the port is open (and thus already claimed), then it's would also be a race to find out if the port is open and file locked. Either way, if you find an available port, someone else could grab it before you do. If we only care that the port is already opened, then we can do that more quickly, and thus run less chance of a race condition problem. Using a file lock would be a time liability.

I experimented with symfony's file lock system and it was just too complicated for basically no benefit.

Concurrency on the PHP HTTP server is an important concern because we want to design this thing to run in concurrent tests from the very beginning.

We also want to hide the implementation details from the test author, because it could be any kind of HTTP server in the future, and we're really testing 'build' -> 'process' -> 'query to verify' and not 'set up HTTP server.'

heddn’s picture

FileSize
6.29 KB
66.65 KB

I made these changes before I read #169, but if we decide they aren't necessary they are pretty easy to strip back out.

In defence of adding these changes, I tested concurrency on local and found out that we actually _did_ have some fragile tests. Specifically testPortMany when run in concurrence with really any other test is an issue. I opened three terminals, ran vendor/bin/phpunit -c core core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php concurrently and had random fails 100% of the time.

Mile23’s picture

Re: #170 Then by all means let's do something else. :-)

Ghost of Drupal Past’s picture

I must admit I do not understand #169. Here's what I think happens:

  1. Process #1 calls findAvailablePort, gets 8000
  2. Process #2 calls findAvailablePort, gets 8000
  3. Process #1 starts PHP server on port 8000
  4. Process #2 starts PHP server on port 8000
  5. Process #1 waits until port 8000 is no longer available
  6. Process #2 waits until port 8000 is no longer available
  7. Both processes will talk to whichever PHP server was faster.

I do not think we need to lock this whole thing, it's enough to add the port to the name of the lock and then there is very little time wasted. It'd go like this:

  1. Process #1 calls findAvailablePort. It first tries to get the lock on 8000. When it gets it, then it tests port 8000 for availability. If it's not available, it drops the lock and continues.
  2. Process #2 calls findAvailablePort. It first fails to get the lock on 8000, locks 8001 and succeeds.

There we go.

Ghost of Drupal Past’s picture

This is how I imagine it. As always: feel free to drop it, no hard feelings, I just felt that itch :)

clemens.tolboom’s picture

Issue summary: View changes
clemens.tolboom’s picture

Issue summary: View changes
heddn’s picture

Fixes in #173 seem reasonable. Additional nit fixes which I comment on below are fixed in this patch.

  1. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,477 @@
    +      $store = new FlockStore(DrupalFilesystem::getOsTemporaryDirectory());
    

    I noticed there's also a semaphore store too, but then later noticed that Windows doesn't support it. So for posterity's sake, commenting that's why we should use flock over semaphore. Good job.

  2. +++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
    @@ -0,0 +1,172 @@
    +    foreach (range(1, $count) as $instance) {
    

    $instance is never used.

  3. +++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
    @@ -0,0 +1,172 @@
    +    foreach ($processes as $port => $process) {
    

    There isn't a need to reloop, just do this in the previous loop.

greg.1.anderson’s picture

Status: Needs review » Needs work
  1. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,477 @@
    +    while ($available_port = $this->findAvailablePort($port)) {
    

    Given the experience we had with temporary folder creation in the scaffold tests, I have reservations about this port-selection technique. Once this code is committed, then every test that runs is going to compete for a port on every test run. Drupal.org runs a lot of tests, and sometimes a lot of tests start up at the same time. This code creates a critical section that will commonly cause port conflicts. Every test process will look for a free port starting at 8000 and scanning up one port at a time. If two jobs start at about the same time, then they will invariably choose the same port number if the second job asks for its port number while the Process above is launching.

    I have two recommendations. To avoid having one test use the port for a web server started for a different test, we need to be able to detect errors from the `start()` method, and pick a new port if it fails. This implies that port selection should be postponed until the web server is launched (in this method).

  2. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,477 @@
    +    for ($port = $min; $port <= $max; $port++) {
    

    Always starting with the same port number and always scanning up one port at a time guarantees that every job that starts at about the same time will pick the same open port.

    To make the pigeon-hole-principal a bit better:

    Use a range from 8100-8799. (The port numbers you use on CI do not matter, but it's good to avoid 8000, 8080 and 8888 when testing local. This matters 1/10, pick another, wider range if you prefer, the wider the better.)

    Do not always start at the same place in the range. Pick a random number such that the first port checked may be anywhere between the beginning and end of the range.

    If you still always go up just 1 port at a time when scanning, just the changes above will give greatly improved results. However, if a bunch of consecutive ports happen to be selected by chance, scanning by 1 each time makes it more and more likely that any given clump will grow by one. To avoid this problem, you could pick a small random prime, and skip that many port numbers each time you retry. Use a modulo to keep the trial port inside the range. Optional. Just using a larger range (way up into the 30,000s?) is probably even better in terms of impact than this improvement.

    You cannot avoid the pigeonhole principal, but these techniques will increase the utility of the range available.

greg.1.anderson’s picture

Status: Needs work » Needs review

Nevermind about #177, this was already taken care of elsewhere in the code in #173. I think the current technique should work.

catch’s picture

To avoid this problem, you could pick a small random prime, and skip that many port numbers each time you retry.

Could we not just randomise the port selection, if we're unlucky exclude the port we just checked then go for another random port? Even if there's not a race condition in the current code, it might be more efficient. Right now to run at 16 concurrency we'd always have to run through up to 15 ports before finding one.

greg.1.anderson’s picture

Yeah, randomizing the port selection was my first suggestion. The part you quoted was my third suggestion, which reduces clumping when selecting a lot of ports in a narrow number space. As I also said, just increasing the range available for ports is probably more effective than varying the increment size. The main win is with randomizing the port selection.

Since the current patch is using a lock file to mark ports as used, rather than attempting to launch the browser as I first assumed, randomizing the initial port selection is less important. It would still be an improvement, saving us from quite a few attempts to grab a lock each run. Trying to grab an in-use lock is probably fast, but we could still make this improvement.

Ghost of Drupal Past’s picture

FileSize
1.22 KB
67.41 KB

I was wondering how mt_rand() will behave in two processes starting the same time and decided to not wonder, we are PHP 7.0 so we can just random_bytes() to find a random port. I dropped min and max and just pick a random number between 1025 and 65535. random_bytes() is not the friendliest of API for this purpose but it could be worse. I measured hexdec(bin2hex()) vs unpack('n', )[1] and the latter is faster (it is faster than bindec even but bindec doesn't seem to work for this purpose, php is murky when it comes to binary data). I kept the locking though.

Status: Needs review » Needs work

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

greg.1.anderson’s picture

Getting really close!

  1. +++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
    @@ -0,0 +1,171 @@
    +    $this->assertNotEquals($port, $this->findAvailablePort());
    

    Teardown does not appear to release our locks; in fact, it seems that we never release any of our locks, unless we fail to acquire a port. It seems that the easiest thing to do would be to make findAvailablePort() add to a list of all acquired locks, and release them all in teardown. Otherwise, there's a chance we might run out of locks. Probably not a big chance; I presume that locks are all released when the phpunit process terminates.

  2. +++ b/core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php
    @@ -0,0 +1,45 @@
    +    $fs->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700, 0000);
    

    If this is going to ever do a re-install, we'd also need to chmod settings.php, and maybe also copy default.settings.php over settings.php. Perhaps in this instance, a re-install is not possible, and chmoding the sites/default directory is not necessary. I think I'd favor being more conservative, and do all of the re-install steps. Maybe make a method to do this cleanup in case it's needed in other tests.

I didn't look into the test failures, but perhaps the range of ports scanned is now wider than supported by the drupalci infrastructure.

Mixologic’s picture

  1. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,473 @@
    +    // Wait until the web server has started. It is started if the port is no
    +    // longer available.
    +    while ($available_port = $this->findAvailablePort($port)) {
    +      if ($available_port !== $port) {
    +        return $ps;
    +      }
    +    }
    ...
    +  protected function findAvailablePort() {
    +    $counter = 100;
    +    while ($counter--) {
    +      $port = unpack('n', random_bytes(2))[1] % 64510 + 1025;
    

    Checking to see if the webserver is available by determining that its port is *not* available is why the tests failed. findAvailablePort should either only pick the random number if its not passed in, or we should abstract out the fsockopen to an internal method of "is port open" etc.

  2. <del>
    +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,473 @@
    +      if ($this->lockAcquired($port)) {
    +        $fp = @fsockopen(static::$hostName, $port, $errno, $errstr, 1);
    +        // If fsockopen() fails to connect, probably nothing is listening.
    +        // It could be a firewall but that's impossible to detect, so as a
    +        // best guess let's return it as available.
    +        if ($fp === FALSE) {
    +          return $port;
    +        }
    +        else {
    +          $this->lockRelease($port);
    +          fclose($fp);
    +        }
    +      }</del>
    

    This part looks suspiciously like the question asked in this stack overflow post: https://stackoverflow.com/questions/39657564/check-if-anything-is-listen...

    We should be checking if a resource is returned from fsockopen.

    Nevermind. FALSE is the correct inversion here.

greg.1.anderson’s picture

Yes, overloading findAvailablePort() to both find a port and determine if a port is available is confusing. Let's not confuse it further by inhibiting the random port selection; instead, let's take the second alternative suggested in #184 and factor out a function just to check to see if a port is in use.

Ghost of Drupal Past’s picture

FileSize
4.46 KB
67.11 KB

The tests failed because of this expectation $this->assertEquals($this->findAvailablePort(), $this->findAvailablePort()); which obviously no longer happens. And this was not just an assert, the server init routine relied on subsequent calls to findAvailablePort returning the same port and busywaited for this port to change. Even without any random that can fail:

  1. Test #1 runs on port 8000
  2. Towards the end, in parallel we start test #2, 8000 is not available, so PHP server is started on 8001
  3. Just the right moment test #1 finishes
  4. Now port 8000 is available and instantiateServer returns before PHP server is ready.

I removed that assert and with it the entire test -- I split findAvailablePort into findAvailablePort and checkAvailablePort and added an assert for the latter into the remaining testManyPort. I also removed the busywaiting. Interdiff is against the last passing test, previous attempt was not great.

Ghost of Drupal Past’s picture

Status: Needs work » Needs review

Note #183 is not addresed yet (and it's unlikely I will do it).

greg.1.anderson’s picture

I don't think that #183.2 is necessary. I'll do #183.1 later.

Ghost of Drupal Past’s picture

Sorry, I meant #183.2 , my initial writeup got lost :( #183.1 is not necessary,

https://symfony.com/doc/current/components/lock.html

If you don't release the lock explicitly, it will be released automatically on instance destruction.

If you deem #183.2 not necessarily then we might be good to go if the test is green.

greg.1.anderson’s picture

Status: Needs review » Reviewed & tested by the community

Oh, cool!

I think this is, in fact, good to go. I only contributed a few lines to this very large patch, so I think it's fair for me to RTBC it. A lot of review comments from a lot of reviewers have been addressed here. Good work.

jibran’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/tests/Drupal/BuildTests/Update/UpdateSiteDrushTest.php
@@ -0,0 +1,61 @@
+class UpdateSiteDrushTest extends BuildTestBase {

+++ b/core/tests/Drupal/BuildTests/Update/UpdateTest.php
@@ -0,0 +1,91 @@
+class UpdateTest extends QuickStartTestBase {

These tests consist of a bunch of bash commands. I'm failing to understand why these can't just be converted to bash scripts and run directly on drupalci using .drupalci.yml?

Why are we writing a whole new testing suite to achieve the task which can be completed by writing some ci commands?

Mixologic’s picture

The bash functionality of drupalci.yml is intended to do minor setup work. It is *not* intended to be a bash testing framework or anything resembling that. Please do not place bash scripts into drupalci.yml and then rely on the exit codes of those scripts as evidence of build success or failure.

It is preferable to have these bash scripts execute within the context of phpunit, which frames them as a standard test, gives us the results in a standard way, and is runnable in environments *other than* drupalci.

Additionally, we want any developer to be able to write and execute these tests locally, and running phpunit is typical. Setting up a local drupalci, is not very typical.

That being said, I think this is still NW because of the issue summary:

Remove the proof-of-concept tests from the patch before committing. Create followups as necessary to re-add them.

Mile23’s picture

Status: Needs work » Needs review
Issue tags: -Needs followup
FileSize
55.52 KB
11.61 KB

These tests consist of a bunch of bash commands. I'm failing to understand why these can't just be converted to bash scripts and run directly on drupalci using .drupalci.yml?

+1 on #192, but also..

We can't use bash to make assertions like we can with Mink. So we could use bash to build out a site, but then when it came time to make a request against a web server we'd have to build that in bash, as well. Then when we needed to assert that, for instance, we got the right headers from an HTTP request, we'd have to add bash to curl or wget or something to do that. And so on.

Making a minimal build system in PHP is preferable.

Added a child issue: #3082230: [meta] Convert some tests to Build tests

And here's a patch without the proof of concept tests. :-)

Mile23’s picture

Def needs change record, and a documentation page.

Ghost of Drupal Past’s picture

I turned to Stackoverflow because that random_bytes line bothered me. Here's a better one. We all learn every day: I didn't know about random_int().

heddn’s picture

heddn’s picture

OK, those docs are now populated and can be reviewed along with the patch here.

alexpott’s picture

Here are some nits. First step on a larger review.

  1. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,491 @@
    +    return 'drupal-build-test-' . $name;;
    

    Double ;

  2. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestInterface.php
    @@ -0,0 +1,194 @@
    + * @see \Drupal\BuildTest\Framework\BuildTestBase
    

    * @see \Drupal\BuildTests\Framework\BuildTestBase - the patch is missing a s

  3. +++ b/core/tests/Drupal/BuildTests/Framework/DrupalMinkClient.php
    @@ -0,0 +1,66 @@
    +   * @see Drupal\BuildTests\Framework\DrupalMinkClient::followMetaRefresh()
    

    Should be * @see \Drupal\BuildTests\Framework\DrupalMinkClient::followMetaRefresh() - missing a leading \

  4. +++ b/core/tests/Drupal/BuildTests/Framework/DrupalMinkClient.php
    @@ -0,0 +1,66 @@
    +      $this->redirects[serialize($this->history->current())] = TRUE;
    

    I think this might be not doing what we think it is because $this->redirects is a private on abstract class this extends. See https://3v4l.org/RJsbR

    This was brought up before in #117 by @lendude but it does seem to have been adequately resolved beyond "it's working".

  5. +++ b/core/tests/Drupal/BuildTests/Framework/ExternalCommandRequirementsTrait.php
    @@ -0,0 +1,113 @@
    +  /**
    +   * Determine if the operating system is Windows.
    +   *
    +   * @return bool
    +   *   TRUE if operating system is Windows, else FALSE.
    +   */
    +  private function isWindows() {
    +    return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    +  }
    

    I think this is unused.

  6. +++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
    @@ -0,0 +1,152 @@
    +    // Load up a process with our expecations. assertCommand() would fail a call
    

    expectations

heddn’s picture

I'll leave fixing 198.4 to @Mile23 or some else. I briefly looked at it and had some ideas, but it involved copying more of https://github.com/symfony/symfony/blob/4.4/src/Symfony/Component/Browse... into our implementation, I couldn't come up with a lot of options.

I also don't feel that having meta refresh features for the first rev is critical and could be a follow-up and we could debate how to solve it in that issue.

alexpott’s picture

  1. +++ b/composer.json
    @@ -22,7 +22,10 @@
    +        "symfony/filesystem": "~3.4.0",
    +        "symfony/finder": "~3.4.0",
    +        "symfony/lock": "~3.4.0"
    

    symfony/lock is a new dependency and as such need release manager +1 and needs a dependency evaluation - see https://www.drupal.org/core/dependencies

    symfony/filesystem and symfony/finder are new direct dependencies but are already included because they are amongst our dependencies dependencies.

  2. +++ b/core/scripts/run-tests.sh
    @@ -865,9 +866,12 @@ function simpletest_script_run_one_test($test_id, $test_class) {
    +  $tmp_dir = FileSystem::getOsTemporaryDirectory() . '/run_tests_' . rand(10000,99999) . microtime(TRUE);
    
    +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,491 @@
    +      $port = random_int(1024, 65535);
    

    rand() vs random_int() ... maybe we should swap to rand in ::findAvailablePort() because cryptographic security is not a concern at all here.

  3. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,491 @@
    +    $this->assertFileExists($this->workspaceDir);
    ...
    +      $this->assertDirectoryNotExists($ws);
    

    We shouldn't be making assertions in teardown and setup either.

  4. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,491 @@
    +    $this->assertStringStartsWith('/', $request_uri, $message);
    +    // Try to make a server.
    +    $this->standUpServer($docroot);
    +    $this->assertTrue($this->serverProcess->isRunning(), 'PHP HTTP server not running.');
    

    I don't think this this method should be making assertions... for the same reason as below.

  5. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,491 @@
    +    $this->assertFileExists($router);
    

    Making assertions in the base class means that tests which don't make any assertions will be counted as risky. I think we should avoid them. We can check for the file existing and throw an exception if it doesn't instead.

  6. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestInterface.php
    @@ -0,0 +1,194 @@
    +interface BuildTestInterface {
    

    Is there any compelling reason to create this interface? It feels really odd to create an interface here. The idea of implementing interface for our different test types feels odd. It's not like they are swappable.

  7. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestInterface.php
    @@ -0,0 +1,194 @@
    +  public function assertCommand($command_line, $working_dir = NULL);
    

    This has the same thing as the visit asserts. It's executing the command and then making an assertion. I'm not sure that this is good practice. I would more expact to see code like

      $this->executeCommand('some cli command');
      $this->assertCommandSuccessful();
      $this->assertCommandErrorOutputContains('some error');
        $this->assertCommandOutputContains('some output');
    
  8. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestInterface.php
    @@ -0,0 +1,194 @@
    +  public function assertVisit($request_uri = '', $expected_status_code = 200, $docroot = NULL);
    ...
    +  public function assertDrupalVisit($request_uri = '/', $expected_status_code = 200, $docroot = NULL);
    ...
    +  public function visit($request_uri = '', $docroot = NULL);
    

    All these methods result in the visiting a Url. The assertVisit methods are interesting because they are not *just* asserts... they are visit a url and then make assertions. This is not usual behaviour for assertions. Assertions never normally change the state of the system whereas these do. I also find all three of these living on the same interface interesting and a bit confusing.

  9. +++ b/core/tests/Drupal/BuildTests/Framework/ExternalCommandRequirementsTrait.php
    @@ -0,0 +1,113 @@
    +/**
    + * Allows test classes to require external command line applications.
    + *
    + * Use annotation such as '(at)requires externalCommand git'.
    + *
    + * This trait is assumed to be on a subclass of \PHPUnit_Framework_TestCase, and
    + * overrides \PHPUnit_Framework_TestCase::checkRequirements().
    + */
    +trait ExternalCommandRequirementsTrait {
    

    It's a shame that PHPUnit doesn't have a more generic way of adding a new type of @requires. One thing that gives me pause here is that adding this means we are going to have to support different versions of this in #2950132: Support PHPUnit 7 optionally in Drupal 8, while keeping support for ^6.5 as undoubtedly ::checkRequirements() has got a return type. And yep it does - see https://github.com/sebastianbergmann/phpunit/blob/dfad4702af08b4d706dbc5... but what's more it has become private so our override won't work when we upgrade. :(

    I think we should consider baking this functionality into BuildTestBase::setUp() and not worrying about re-usability.

  10. +++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
    @@ -0,0 +1,152 @@
    + * @requires externalCommand composer
    + * @requires externalCommand drush
    + * @requires externalCommand git
    

    I can't see where composer, git or drush is actually used by this test.

  11. +++ b/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php
    @@ -97,6 +98,18 @@ protected function execute(InputInterface $input, OutputInterface $output) {
    +    // Make sure there is an entry in sites.php for the new site.
    +    $fs = new Filesystem();
    +    if (!$fs->exists($root . '/sites/sites.php')) {
    +      $fs->copy($root . '/sites/example.sites.php', $root . '/sites/sites.php');
    +    }
    +    $parsed = parse_url($base_url);
    +    $port = $parsed['port'] ?? 80;
    +    $host = $parsed['host'] ?? 'localhost';
    +    // Remove 'sites/' from the beginning of the path.
    +    $site_path = substr($this->siteDirectory, 6);
    +    file_put_contents($root . '/sites/sites.php', "\$sites['$port.$host'] = '$site_path';", FILE_APPEND);
    

    Feels odd to be changing existing tests - howe come that's necessary.

  12. +++ b/core/tests/Drupal/Tests/Scripts/TestSiteApplicationTest.php
    @@ -49,6 +49,7 @@ protected function setUp() {
    +    $this->markTestIncomplete('Replace with build test.');
    
    @@ -67,6 +68,7 @@ public function testInstallWithNonExistingFile() {
    +    $this->markTestIncomplete('Replace with build test.');
    
    @@ -106,6 +108,7 @@ public function testInstallWithNonSetupClass() {
    +    $this->markTestIncomplete('Replace with build test.');
    
    @@ -206,6 +209,7 @@ public function testInstallScript() {
    +    $this->markTestIncomplete('Replace with build test.');
    
    @@ -243,6 +247,7 @@ public function testInstallInDifferentLanguage() {
    +    $this->markTestIncomplete('Replace with build test.');
    
    @@ -317,6 +322,7 @@ public function testUserLogin() {
    +    $this->markTestIncomplete('Replace with build test.');
    

    I don't understand why we have to remove existing coverage here. We're adding a new test type and sure we might want to replace these tests in the future but why are we removing them now?

Ghost of Drupal Past’s picture

> rand() vs random_int() ... maybe we should swap to rand in ::findAvailablePort() because cryptographic security is not a concern at all here.

It shouldn't matter now -- as I mentioned above I just didn't want to figure out whether mt_rand behaves pathologically when running in parallel but now that we lock, how we pick a candidate doesn't matter. We could've stayed with the sequence. It's just faster to use a random value, any random, because there's much less to lock this way. If we change from random_int then it should be to mt_rand() because we want larger than 32767 from rand(). Also mt_rand is slightly faster than rand and significantly faster than random_int, so let's do it. I won't roll a new one with that one change, there's enough to do and it's trivial to change with the others.

Mile23’s picture

#200.6:

Is there any compelling reason to create this interface?

I added that because if there's an interface you don't have to mark other methods as @internal. Also you have to argue about whether a given method belongs in the interface or not, so you have an extra check on bloaty bloatness.

Some of the class properties are private, as well, for similar reason: They're implementation details, and test authors should just be writing commands and assertions and not petitioning for a new convenience command.

This can all change around as needed... I'm not married to it. And hey, I got to argue it over this one time so mission accomplished.

7 & 8: In order to make an API like this:

$this->executeCommand('some cli command');
$this->assertCommandSuccessful();

... you have to manage the state of the process within the class. Instead of doing that, we return the process object to the caller, and they can assert whatever they want against it.

$process = $this->assertCommand('stuff');
$this->assertEquals('something happened', $process->getErrorOutput());

or...

$this->assertEquals(
  'something happened',
  $this->assertCommand('stuff')->getErrorOutput()
);

This is much clearer to me, and also makes the class less complex because you're not keeping track of state.

We could remove assertCommand() and have the user always say executeCommand() and then make an assertion against the process object about exit code or whatever they want to. But if we do that then we lose the benefit of uniform reporting on fails, which everyone has to recreate in order to debug their tests anyway.

The same pattern applies to assertVisit(). We do manage the state of Mink in the class, but really that's just using the Mink API.

I'm more married to this than 6 above. :-)

9: Sigh. This is why we can't have nice things.

Mile23’s picture

#200.1: Want to make this framework a component? :-) Then it and symfony/lock could be removed with —no-dev.

#200.3,4: $fs->mkdir() and $fs->remove() throw an exception if there’s an error, so we just remove the assertions.

#200.5: I think we don’t need to assert the existence of .ht.router.php at the framework level for a few reasons:
- Not all versions of core have this file.
- Assuming its existence is a Drupalism. Corollary: Not all HTTP requests will be against a Drupal site.
- We’re trying to test against the build and update processes for core, and if the installation is missing that file, then the bug mentioned in https://git.drupalcode.org/project/drupal/blob/8.8.x/.ht.router.php#L18 should be seen in the test. Maybe add a note on #3082230: [meta] Convert some tests to Build tests that specific test implementations need to assert the existence of .ht.router.php and/or visit a route with a ‘.’ in them to check this.

That assertion was added in #147 so how about it @heddn?

Anyway, removed the assertion from setUp().

#200.8: I’m not a fan of assertDrupalVisit(), because we might change our generator string. In fact, Drupal 9 will change it. Removing assertDupalVisit() here, but open to putting it back if someone has a way.

The rest is the same pattern as the other doSomething()/assertDoSomething() methods. What if we just take the word ‘assert’ out of the method name? So for instance assertCommandErrorOutputContains() becomes commandErrorOutputContains().

Also simplified some of those assertions in assertVisit() and visit(). We get an exception if the server wasn’t able to be set up, so that should error out the test.

#200.11,12: Reverted.

Still outstanding: #198.4/#117.11, #200.1, and #200.9.

Mile23’s picture

#200.9:

It's a shame that PHPUnit doesn't have a more generic way of adding a new type of @requires.

Drupal\KernelTests\Core\Image\ToolkitGdTest will need some love in this regard, and probably others. #2950132: Support PHPUnit 7 optionally in Drupal 8, while keeping support for ^6.5 doesn’t seem to cover this.

It turns out we need ExternalCommandRequirementsTrait to be in a trait so that we can test it. Since we’re testing annotations, we can’t mock a class, and if we annotate a BuildTestBase class, it’s discovered by the testing system. So we make it a trait and then test the trait methods on a non-test class. Let’s hear it for composition!

Still outstanding: #198.4/#117.11 and #200.1.

Mile23’s picture

FileSize
11.35 KB

Interdiff....

Ghost of Drupal Past’s picture

Addressing 200.1 the only concern I can see https://github.com/symfony/lock/commit/a48ab1443c39e8fc6950a4edc2a16c383... is this very recent change, otherwise Lock -- at least as far as I can see -- is a very stable and problem free component, especially what little we use of it. https://github.com/symfony/lock/blob/master/CHANGELOG.md

If we added a use statement for LockFactory and did a class_exists(LockFactory) check to decide to call new Factory or new LockFactory (they are the exact same class, just renamed), we would be compatible up to and including Symfony 5.0. (use statements for nonexisting classes are fine)

Mile23’s picture

#198.4/#117.11:

In an ideal world, Drupal 8.8 would require PHP 7.1.x. I say this because it'd be lovely to just say that we require-dev symfony/browser-kit ^4.2 and then be done. :-)

But Drupal 8.8 needs to run on PHP 7.0.8, so here's DrupalMinkClient with reflection to access a private property in order to get meta refresh redirects from Mink.

Also we check if symfony/browser-kit supports meta refreshes, and use that instead if we can. I manually tested this by having composer.json require symfony/browser-kit ~4.2 and then updating.

Still outstanding: #200.1, tho checkout #206.

alexpott’s picture

Category: Plan » Task
  1. I'm still very unsold on the idea of asserts executing the command / visiting the site under test. In my mind asserts are not meant to change the state of the system.
  2. I'm not convinced by the arguments for an interface on abstract test classes in #202. The interface itself feels like bloat.
  3. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,500 @@
    +  /**
    +   * Our native host name, used by PHP when it starts up the server.
    +   *
    +   * Requests should always be made to 'localhost', and not this IP address.
    +   *
    +   * @var string
    +   */
    +  protected static $hostName = '127.0.0.1';
    

    How come this is static and why does is not private?

  4. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,500 @@
    +  /**
    +   * Port that will be tested.
    +   *
    +   * Generated internally. Use getPortNumber().
    +   *
    +   * @var int
    +   */
    +  protected $hostPort;
    

    If we're using privates - which I think makes heaps of sense on abstract testbase classes - why this is not private? There's a public accessor.

  5. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,500 @@
    +  /**
    +   * The Mink session manager.
    +   *
    +   * @var \Behat\Mink\Mink
    +   */
    +  protected $mink;
    

    There's a getter... so making this private would prevent interesting side effects.

  6. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,500 @@
    +  /**
    +   * PHP executable finder.
    +   *
    +   * @var \Symfony\Component\Process\PhpExecutableFinder
    +   */
    +  protected $phpFinder;
    

    I'm not sure this needs to be a class property.

  7. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,500 @@
    +  /**
    +   * @var \Symfony\Component\Lock\Factory
    +   */
    +  protected $lockFactory;
    +
    +  /**
    +   * @var \Symfony\Component\Lock\Lock[]
    +   */
    +  protected $locks;
    

    I'd be tempted to make these private too because you've got public methods to get a lock etc and what do tests extending this need to do with this?

  8. +++ b/core/tests/Drupal/BuildTests/Framework/ExternalCommandRequirementsTrait.php
    @@ -0,0 +1,106 @@
    +/**
    + * Allows test classes to require external command line applications.
    + *
    + * Use annotation such as '(at)requires externalCommand git'.
    + */
    +trait ExternalCommandRequirementsTrait {
    

    Why is everything static in this class? Is it to indicate this trait has no side effects on $this? Also should we consider making all the methods and property private? Tests that extends BuildTestBase have no business futzing with this.

Mile23’s picture

Thanks for the review, @alexpott.

OK. All BuildTestBase properties other than $destroyBuild are now private, which is totally fine with me.

A tiny bit of refactoring for instantiateServer() around static::$hostName, because a test was using it.

Removed the interface, moved docblocks to BuildTestBase.

Why is everything in ExternalCommandRequirementsTrait static? Because of this:

+++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
@@ -0,0 +1,500 @@
+  /**
+   * {@inheritdoc}
+   */
+  public static function setUpBeforeClass() {
+    parent::setUpBeforeClass();
+    static::checkClassCommandRequirements();
+  }

This means that all the methods other than checkMethodCommandRequirements() need to be static. And it’s a coin toss as to whether checkMethodCommandRequirements() should also be static. It only needs the method annotations from the concrete class.

I haven’t refactored the assertions to not change state because I have a place to be in a few minutes, will get to that. Any comments welcome in the meantime.

Still outstanding: #200.1, #208.1

Update: Forgot to re-delete the interface after copying all the docblocks from it. So still outstanding: #208.2.

heddn’s picture

re: removal of assertDrupalVisit we had 3 or 4 instances of those same 3 assertions. So having a helper that does it so we don't constantly do it is a nice-to-have. Especially as it does a version agnostic regex (based off earlier feedback).

Removing .ht.router.php check is fine. Not having it does lead to very strange and difficult to troubleshoot test scenarios, so we want to make sure the notes/docs spell out those limitations well.

As far as assert vs doSomething, there's been a couple times I wanted to check against error response, non-error response and other things. We have this same feature w/ mink and getSession. I think I'm more in favor of visit, followed by assertions on the last visit.

Mile23’s picture

OK, so now you say executeCommand() and then call the assertion helper methods. You can also say $process = executeCommand() and then assert against $process.

Similarly, you say $this->visit('/path', '/working/dir') and get back a Mink object. You can assert against the Mink object, or use $this->getMink(). There's still a caveat in that when you make a request against a different $working_dir, you have to get a new local Mink session. There's not a good solution to this.

Added back assertDrupalVisit() which only acts against the existing Mink object.

Normalized on $working_dir so it's clear what's going on.

Re-added the PoC test dealing with quick start, in order to demo how these work.

For commands you can say this:

    $finder = new PhpExecutableFinder();
    $process = $this->executeCommand($finder->find() . ' ./core/scripts/drupal install ' . $profile, $working_dir);
    $this->assertCommandSuccessful();
    $this->assertCommandOutputContains('Username:');

For Mink assertions you can say this:

    $this->visit('/does-not-exist')->assertSession()->statusCodeEquals(404);
    $this->assertDrupalVisit();

Still outstanding: #200.1

heddn’s picture

Issue summary: View changes

This is looking very nice. Paul, thanks for pushing it forward some more.

Do we need to do #206? Or is that follow-up material?

catch’s picture

Just to say from a release management perspective adding symfony/lock as a dev dependency seems fine. Obviously if we ever want to use it outside dev we'd have to separately evaluate it.

heddn’s picture

Issue summary: View changes
Mile23’s picture

OK, so here we are without the PoC tests, ready for a review.

Mile23’s picture

Per Slack conversation w/ @alexpott... Let's try reverting SimpletestResultsForm. I'm pretty sure those changes are left over from the very beginning of this process when stuff was breaking, and from running under run-tests.sh to make sure everything worked.

alexpott’s picture

  1. +++ b/core/scripts/run-tests.sh
    @@ -866,9 +867,12 @@ function simpletest_script_run_one_test($test_id, $test_class) {
    +  $tmp_dir = FileSystem::getOsTemporaryDirectory() . '/run_tests_' . rand(10000,99999) . microtime(TRUE);
    +  mkdir($tmp_dir);
    +  $command = escapeshellarg($php) . ' -d sys_temp_dir=' . escapeshellarg($tmp_dir);
    

    I ponder if we should only be doing this in concurrency > 1. One reason for this is that it makes run-tests.sh and therefore DrupalCI a bit different from running the tests via PHPUnit's runner which is a concern. Also are we sure that we're not going to hit a problem where rand(10000,99999) . microtime(TRUE); is not unique? Looks like a random fail waiting to happen.

  2. +++ b/core/scripts/run-tests.sh
    @@ -910,6 +914,11 @@ function simpletest_script_command($test_id, $test_class) {
    +  if (is_dir($tmp_dir)) {
    +    $fs = new SymfonyFilesystem();
    +    $fs->remove($tmp_dir);
    +  }
    

    I wonder if we should use \Drupal::service('file_system')->deleteRecursive($test_directory, ['\Drupal\Tests\BrowserTestBase', 'filePreDeleteCallback']); here instead.

    One we're doing this already later on in this method and two the filePreDeleteCallback is obviously borne from experience.

alexpott’s picture

The changes been discussed in #217 came from #66 and were implemented in #69.

+++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
@@ -0,0 +1,589 @@
+    // Set up the workspace directory.
+    // @todo Glean working directory from env vars, etc.
+    $this->workspaceDir = DrupalFilesystem::getOsTemporaryDirectory() . '/build_workspace_' . md5($this->getName() . microtime(TRUE));

Given we're doing this - do we need the temp dir stuff in run-tests.sh? I would have thought this is a one or the other situation. I think I prefer the solution being inside BuildTestBase as that keeps everything runner agnostic.

greg.1.anderson’s picture

greg.1.anderson’s picture

  1. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -0,0 +1,589 @@
    +    $fs->mkdir($this->workspaceDir);
    

    It would probably be more reliable to use something like:

    $this->workspaceDir = $fs->tempnam(DrupalFilesystem::getOsTemporaryDirectory(), '/build_workspace_' . md5($this->getName() . microtime(TRUE)));

  2. +++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
    @@ -0,0 +1,150 @@
    +    // The composer.json file should not exist, and if the command can't be run
    +    // (because the system doesn't have 'test' or is Windows) it should return
    +    // 1.
    +    $process = $this->executeCommand('test -f composer.json');
    +    $this->assertEquals(1, $process->getExitCode());
    

    Is there some reason we use executeCommand here rather than assertFalse on fileExists / is_file?

greg.1.anderson’s picture

Use $fs->tempnam to ensure that the name we pick does not already exist on disk.

Avoid asserting on exec test when we can instead assert on file_exists.

alexpott’s picture

  1. +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
    @@ -155,9 +155,9 @@ protected function setUp() {
    -    $this->workspaceDir = DrupalFilesystem::getOsTemporaryDirectory() . '/build_workspace_' . md5($this->getName() . microtime(TRUE));
         $fs = new SymfonyFilesystem();
         $fs->mkdir($this->workspaceDir);
    +    $this->workspaceDir = $fs->tempnam(DrupalFilesystem::getOsTemporaryDirectory(), '/build_workspace_' . md5($this->getName() . microtime(TRUE)));
    

    I think tempnam generates a file and not a directory and also with the patch we're making the dir before doing $fs->tempnam().

  2. +++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
    @@ -17,11 +17,8 @@ class BuildTestTest extends BuildTestBase {
    -    // The composer.json file should not exist, and if the command can't be run
    -    // (because the system doesn't have 'test' or is Windows) it should return
    -    // 1.
    -    $process = $this->executeCommand('test -f composer.json');
    -    $this->assertEquals(1, $process->getExitCode());
    +    // The composer.json file should not exist to start.
    +    $this->assertFalse(file_exists('composer.json'));
    

    If this is the right change then we should use $this->assertFileNotExists() - when reviewing this before I wondered if this was done on purpose because it seems overly complex.

greg.1.anderson’s picture

Argh I flubbed that last patch. I'll fix it up.

I think that maybe the point of the second test was only to confirm that executeCommand returns a non-zero status code when an error happens. That is, we don't care that composer.json does not exist, it's executeCommand that we're testing.

I'll clarify this as well.

Status: Needs review » Needs work

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

greg.1.anderson’s picture

Status: Needs work » Needs review
FileSize
50.47 KB
1.71 KB

Hopefully un-flubbed.

greg.1.anderson’s picture

greg.1.anderson’s picture

Status: Needs review » Reviewed & tested by the community

Since my main contribution here is backing out my previous contribution and some coding standards changes, I think it's okay for me to RTBC. Reviewed, and looks good.

Mile23’s picture

Status: Reviewed & tested by the community » Needs work

Still needs #222.

greg.1.anderson’s picture

@Mile23: I already did #222.1 by calling remove and mkdir after tempnam. I did #222.2 by changing the test. My assumption is that what we want to test here is the 'exec' wrapper function. Testing to see whether composer.json did or did not exist seems superfluous, as composer.json does not seem to be relevant to the rest of the test functions. If I've misjudged, then we should perhaps add in the assertFileNotExists test, or maybe just put the test back the way it was with a more clear comment.

alexpott’s picture

Status: Needs work » Needs review
+++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
@@ -0,0 +1,585 @@
+    // Set up the workspace directory.
+    // @todo Glean working directory from env vars, etc.
+    $fs = new SymfonyFilesystem();
+    $this->workspaceDir = $fs->tempnam(DrupalFilesystem::getOsTemporaryDirectory(), '/build_workspace_' . md5($this->getName() . microtime(TRUE)));
+    $fs->remove($this->workspaceDir);
+    $fs->mkdir($this->workspaceDir);

Still not a huge fan of this. I think the current construction can allow for race conditions between the remove and the mkdir - if another process does tempnam as that point. Also $this->getName() only returns the test method so I would haver thought we need get_class() to make it more unique.

I propose that we do this the same way we are guaranteeing port uniqueness - we have a locking system there for us to use. So we can do something like:

    // Set up the workspace directory.
    // Ensure we're using a unique workspace.
    do {
      $dir = uniqid('build_workspace_', TRUE);
      $lock_acquired = $this->lockAcquired($dir);
    } while (!$lock_acquired);
    $this->workspaceDir = DrupalFilesystem::getOsTemporaryDirectory() . '/' . $dir;
    $fs = new SymfonyFilesystem();
    $fs->mkdir($this->workspaceDir);

And then in teardown we can do $this->lockRelease(basename($this->workspaceDir)); when $this->destroyBuild is true.

greg.1.anderson’s picture

The current implementation is a bit ironic, as the conditions for failing during the race condition is that the same code is running at exactly the same time (two processes going through tempnam, remove, mkdir at the same time), and this is guarded by the md5 of two entities that will always be the same when the same code runs at the same time. This assumes that less than one microsecond passes between the call to microtime() and when remove() deletes the temp file. If this never happens, then there will never be a conflict, but let's pretend this will always happen (both calls in the same microsecond), since we cannot prove that it will never happen.

I don't really like the lock solution here; even though it is code that we have available, it is more complicated than necessary. With a slight adjustment to the code, we can use tempnam as the lock. This gets rid of the while, and also obviates the need to add delayed cleanup for the lock. I'll post a new patch that demonstrates this in a few minutes.

greg.1.anderson’s picture

I changed my mind. My solution still requires deferred cleanup, which would mean adding another class variable to store the temporary file. In the balance, maybe adding more state variables is worse than having the while. I'll do it as you suggested.

greg.1.anderson’s picture

Updated patch with suggested solution from #230.

Status: Needs review » Needs work

The last submitted patch, 233: 3031379-226.patch, failed testing. View results

greg.1.anderson’s picture

Test failure due to a spurious timeout connecting to Packagist. Requeued.

heddn’s picture

Status: Needs work » Needs review
+++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
@@ -172,6 +177,9 @@ protected function tearDown() {
+      // Release our workspace lock
+      $this->lockRelease(basename($this->workspaceDir));

Once a build is finished, I wonder if we should always release the lock. Is there any way to ever release these locks if we preserve the build? I'd be worried that these locks could start to pile up.

heddn’s picture

Status: Needs review » Needs work
+++ b/core/tests/Drupal/BuildTests/Framework/ExternalCommandRequirementsTrait.php
@@ -0,0 +1,106 @@
+  private static $existingCommands = [];

This being private causes issues with class inheritance, due to the way traits operate at runtime. Unless we want to require all implementing classes to also use the trait directly, we should make this protected.

Otherwise we get:

1) Drupal\Tests\automatic_updates\Build\ModifiedFilesTest
Error: Cannot access  property Drupal\Tests\automatic_updates\Build\ModifiedFilesTest::$existingCommands in /var/www/html/core/tests/Drupal/BuildTests/Framework/ExternalCommandRequirementsTrait.php:75
heddn’s picture

In #210 I said that checking for .ht.router.php didn't need to be done. However, I'm not so sure I'm so cool with that any more. If it is available, then I feel we should use it. I have to be very careful with the URLs I visit. If they include any periods in them, then the test fails.

greg.1.anderson’s picture

#236: Locks are automatically released when the instance is destroyed, so I'm not even sure we need to explicitly release it in teardown.

#237: There are only problems with accessing private members if you try to do that directly, without a wrapper method. Private fields behave as you would expect them to if you provide methods for manipulating $existingCommands. We should do this rather than making this field protected.

#238: I really think this should be a follow-on issue. I think it is important to get this to a committable state, and then worry about refinements. We can just document limitations in visited URLs.

Mile23’s picture

#236: Locks are automatically released when the instance is destroyed, so I'm not even sure we need to explicitly release it in teardown.

PHPUnit test objects are held for quite a while in the result object, like all the way to the end of the testing process. Under run-tests.sh this isn't such a big deal because we do one test class per process, but under phpunit tool it could be a problem. Also, it's just better style to explicitly clean up.

#238: I really think this should be a follow-on issue. I think it is important to get this to a committable state, and then worry about refinements. We can just document limitations in visited URLs.

+1. In fact, here's a new child issue: #3085728: Add access rules for build tests

Mile23’s picture

Status: Needs work » Needs review
FileSize
49.98 KB
5.25 KB

OK, Re: #220:

That test is left over from early dev times when I was checking to make sure the workspace directory was correct.

Now, fixing it, I realize that it's probably a good idea to always make sure the workspace + working_dir exists (otherwise symfony Process tells us it's using cwd(), which we don't want).

heddn’s picture

FileSize
49.97 KB
1.04 KB

This addresses #237.
So, one could argue that we should have a test for the use of self vs static... but we do have follow-ups and this is easily caught with them. So I'm suggesting we just wait for those to test this. I'm fine w/ follow-ups for the rest of the feedback. LGTM.

Mile23’s picture

Assigned: heddn » Mile23

Edit: Comment made in error.

Mile23’s picture

Assigned: Mile23 » Unassigned
Mixologic’s picture

Status: Needs review » Reviewed & tested by the community

Since this is adding a new test type, and has no potential to interfere with any existing installations, I think its time we got this in, and start exercising it so we can iterate on any additional polish.

Both automatic updates and composer initiative could strongly benefit from this, so, lets not let perfect be the enemy of the good, and if there are more things needed, we can add more issues to shine it up.

greg.1.anderson’s picture

+1 #245

alexpott’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Fixed
Issue tags: +8.8.0 release notes

Committed 8524be362e and pushed to 8.8.x. Thanks!

diff --git a/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
index 3f8a13dd0f..61baf4f3c2 100644
--- a/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
+++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
@@ -4,7 +4,6 @@
 
 use Drupal\BuildTests\Framework\BuildTestBase;
 use org\bovigo\vfs\vfsStream;
-use Symfony\Component\Filesystem\Filesystem;
 use Symfony\Component\Finder\Finder;
 
 /**

Fixed unused use on commit.

  • alexpott committed 8524be3 on 8.8.x
    Issue #3031379 by heddn, Mile23, greg.1.anderson, Charlie ChX Negyesi,...
Mile23’s picture

!

Thanks everyone. :-)

Mile23’s picture

CR has out of date demo code. I'll update later today if no one else gets to it.

Next step: #3085728: Add access rules for build tests

Mile23’s picture

Updated CR and also docs page but the docs could probably use more work.

Docs: https://www.drupal.org/docs/8/phpunit/build-testing

Krzysztof Domański’s picture

New test can randomly fail.

https://www.drupal.org/pift-ci-job/1426940

There was 1 error:

1) Drupal\BuildTests\Framework\Tests\BuildTestTest::testPortMany
RuntimeException: Unable to start the web server.

/var/www/html/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php:426
/var/www/html/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php:127

ERRORS!
Tests: 4, Assertions: 29, Errors: 1.
✗	
Unknown
fail: [run-tests.sh check] Line 0 of :
FATAL Drupal\BuildTests\Framework\Tests\BuildTestTest: test runner returned a non-zero error code (2).
Mixologic’s picture

Hmm. there's four other instances of this.

https://www.drupal.org/pift-ci-job/1425967
https://www.drupal.org/pift-ci-job/1426830
https://www.drupal.org/pift-ci-job/1426933
https://www.drupal.org/pift-ci-job/1427002

Is this indicative that the findAvailablePort() isnt working as intended, or is it that we're assuming too much in the test itself?

Also, should we NW this and revert or file a followup?

greg.1.anderson’s picture

I wouldn't want to revert this for a few spurious test failures. Let's just file a follow-up to fix the bug.

greg.1.anderson’s picture

Mixologic’s picture

yeah, reverting is rough because randofails are so difficult to replicate, and therefore an impossible bar to know if you have actually fixed it or not.

My main concern was whether or not the *timing* is bad, with people trying to get stuff into core. But given that it happened about 5 times in a couple of days, Im thinking the drag on velocity isnt nearly as bad as the difficulty of getting to the root of this without being able to see that its not happening anymore.

alexpott’s picture

@greg.1.anderson something odd happened... #233 interdiff was not actually applied to the patch in that comment. So that code is not in HEAD. Going to create a follow-up for that - see #3086235: Ensure BuildTestBase workspace directories are unique

mikelutz’s picture

As long as we are following up, this appears to introduce a dependency on symfony/browser-kit, but does not declare the dependency. It's currently only installed becuase it's required y behat/mink-browser-kit-driver.

It's also getting installed at version 4.3 on php 7.1 with a composer update, but that adds string typehinting to the request method which we aren't compatible with pre-php 7.2

Can we explicitly require-dev symfony/browser-kit: ^3.4.0 (and we should be fine with type-hinting when we update to 4.3 on Drupal 9 with a minimum php version of 7.2)

mondrake’s picture

+1 on #258 - that’s what also #3044175: [DO NOT COMMIT] Vendor minimum testing issue is doing

xjm’s picture

Status: Fixed » Needs work

This is currently tagged for the 8.8.0 release notes, but it sounds like a (massively useful) non-disruptive API addition. If there's a disruption, the release note should briefly summarize it. If it's just something we want to celebrate, let's tag it "8.8.0 highlights" instead.

heddn’s picture

Status: Needs work » Needs review
Issue tags: -8.8.0 release notes +8.8.0 highlights

Non-disruptive. Retagging.

xjm’s picture

Status: Needs review » Fixed

Thanks @heddn!

Status: Fixed » Closed (fixed)

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