Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DX improvements in new monorepo #1012

Closed
2 of 3 tasks
thelovekesh opened this issue Feb 24, 2024 · 43 comments
Closed
2 of 3 tasks

DX improvements in new monorepo #1012

thelovekesh opened this issue Feb 24, 2024 · 43 comments
Assignees
Labels
Infrastructure Issues for the overall performance plugin infrastructure

Comments

@thelovekesh
Copy link
Member

thelovekesh commented Feb 24, 2024

As per #654, we are moving published modules as standalone plugins, making the current repo a mono repo for multiple plugins.

This brings a new challenge to streamline the current tooling and build setup used to maintain coding standards, testing, and publishing the plugin. With this, we are exploring integrating existing workflows in such a way that it doesn't hurt the DX and keeps the maintenance cost low.

Tasklist

@thelovekesh thelovekesh self-assigned this Feb 24, 2024
@thelovekesh thelovekesh added the Infrastructure Issues for the overall performance plugin infrastructure label Feb 24, 2024
@thelovekesh
Copy link
Member Author

thelovekesh commented Feb 26, 2024

But assuming that the tests all remain in /tests and don't also move to the /plugins directory, there would still need to be a plugin-specific testsuite in the plugin's dedicated phpunit.xml.dist: from (#934 (comment))

@westonruter I think it would make more sense to include the plugin test in the plugin directory itself. It will help us in multiple ways:

  • With this new approach, we won't need to switch between directories or hunt down related files scattered throughout multiple directories.

  • When a plugin becomes part of the core, removing it is much easier. Simply delete the dedicated directory, and there's no need for a tedious search and delete across the entire plugin.

  • No need to modify configurations based on changes in the plugins/ directory.

  • This new approach ensures a smooth testing experience with just one command for any plugin.

    ./vendor/bin/phpunit -c plugins/foo/phpunit.xml.dist # Run tests for plugin: Foo 
    ./vendor/bin/phpunit -c plugins/bar/phpunit.xml.dist # Run tests for plugin: Bar

Please take a look at the POC - #1013

@westonruter
Copy link
Member

I do like the idea of organizing the tests into the plugin's directory. It's more helpful for DX to keep the plugin-related code together than it is to keep all test-related code together.

@felixarntz
Copy link
Member

+1 on the idea to keep the plugin's source and test code in a similar location. For example, a plugin plugins/auto-sizes could have its tests in a plugins/auto-sizes/tests directory. This should be relatively straightforward and we would just need to ensure the tests folder is omitted from the standalone plugin builds.

What I'm not a fan of is the separate phpunit.xml.dist files. What's the benefit of that? A partial reason for not going with individual files is to ease maintenance, I don't see a benefit of having the almost identical PHPUnit config file duplicated for every plugin in the monorepo. IMO it's easier from a maintenance perspective to have a single file, and add the test suite for a new plugin once it's added. It's a one-and-done situation, and there are already other places in the overall codebase anyway that need to be touched for administrative purposes in this scenario (e.g. the .github/CODEOWNERS file).

So from my end:

  • +1 to putting the actual tests (the PHP files) together with the standalone plugins
  • -1 to having an individual PHPUnit config file for each plugin

@westonruter
Copy link
Member

@felixarntz A benefit to having a separate phpunit.xml.dist file for each plugin, which I @thelovekesh first shared in #934 (comment), is that the test suite would no longer be bound 1:1 to the plugin. A plugin could benefit from having multiple test suites. For example, there could be an external-http test suite for a given plugin which is slower. Nevertheless, the bootstrap logic is currently bound up with the test suite name being exactly the same as the slug for the standalone plugin. So if a new test suite is needed for a plugin, this bootstrapping logic wouldn't work anymore. By locating the phpunit.xml.dist in each plugin's respective directory, there wouldn't be any more need for hacking the --testsuite arg.

// Check if we use the plugin's test suite. If so, disable the PL plugin and only load the requested plugin.
$plugin_name = '';
foreach ( $_SERVER['argv'] as $index => $arg ) {
if (
'--testsuite' === $arg &&
isset( $_SERVER['argv'][ $index + 1 ] ) &&
'performance-lab' !== $_SERVER['argv'][ $index + 1 ] &&
file_exists( TESTS_PLUGIN_DIR . '/plugins/' . $_SERVER['argv'][ $index + 1 ] )
) {
$plugin_name = $_SERVER['argv'][ $index + 1 ];
}
}
if ( $plugin_name ) {
$plugin_test_path = TESTS_PLUGIN_DIR . '/plugins/' . $plugin_name;
tests_add_filter(
'plugins_loaded',
static function () use ( $plugin_test_path, $plugin_name ) {
// Check if plugin has a "plugin/plugin.php" file.
if ( file_exists( $plugin_test_path . '/' . $plugin_name . '.php' ) ) {
require_once $plugin_test_path . '/' . $plugin_name . '.php';
} elseif ( file_exists( $plugin_test_path . '/load.php' ) ) {
// Check if plugin has a "plugin/load.php" file.
require_once $plugin_test_path . '/load.php';
} else {
echo "Unable to locate standalone plugin bootstrap file in $plugin_test_path.";
exit( 1 );
}
},
1
);
} else {
// Force plugin to be active.
$GLOBALS['wp_tests_options'] = array(
'active_plugins' => array( basename( TESTS_PLUGIN_DIR ) . '/load.php' ),
);
// Add filter to ensure the plugin's admin integration and all modules are loaded for tests.
tests_add_filter(
'plugins_loaded',
static function () {
require_once TESTS_PLUGIN_DIR . '/admin/load.php';
require_once TESTS_PLUGIN_DIR . '/admin/server-timing.php';
require_once TESTS_PLUGIN_DIR . '/admin/plugins.php';
$module_files = glob( TESTS_PLUGIN_DIR . '/modules/*/*/load.php' );
if ( $module_files ) {
foreach ( $module_files as $module_file ) {
require_once $module_file;
}
}
},
1
);
}

The standalone plugins can still share the bootstrapping logic as I mentioned in #1013 (comment) while they each have their own phpunit.xml.dist.

@felixarntz
Copy link
Member

@westonruter I'm not sure the value is worth the tradeoff. I can't envision a single plugin would use multiple test suites. Certainly there are a few complex plugins that may make use of that, but we haven't used it so far, and I'm not sure when or why we would.

While for PHPCS I see a benefit of individual files because of minor configuration changes, for PHPUnit the config files typically look the same for every plugin. I'd say we should reevaluate this if we encounter a real need for it, but otherwise it creates overhead of duplicate files without a real benefit.

@westonruter
Copy link
Member

westonruter commented Feb 27, 2024

@felixarntz The Embed Optimizer plugin could, for example, have an external-http test suite that pulls down oEmbed data from each of the providers rather than using fixtures. I suppose that could also be achieved with an environment variable.

Another benefit is the DX improvement. If you're working on Speculation Rules, you could just run phpunit right from the /plugins/speculation-rules plugin and it would just run the tests for that plugin. Otherwise, you'd have to do phpunit --testsuite speculation-rules, or if using our NPM wrappers, in reality you'd have to do the very unergonomic:

npm run test-php -- -- -- --testsuite=speculation-rules

@felixarntz
Copy link
Member

@westonruter

The Embed Optimizer plugin could, for example, have an external-http test suite that pulls down oEmbed data from each of the providers rather than using fixtures. I suppose that could also be achieved with an environment variable.

Again, IMO this is a theoretical benefit as we have no plans to make use of it at this point. If there is a need for it, we could re-evaluate. But as you're saying there may be more efficient ways to achieve it than duplicating the config files.

Another benefit is the DX improvement. If you're working on Speculation Rules, you could just run phpunit right from the /plugins/speculation-rules plugin and it would just run the tests for that plugin.

Would that use the correct PHPUnit version (the one locally installed via PL composer.json)? Given you wouldn't be in the directory that has composer.json and vendor, I'm not 100% sure about it.

Otherwise, you'd have to do phpunit --testsuite speculation-rules, or if using our NPM wrappers, in reality you'd have to do the very unergonomic:

npm run test-php -- -- -- --testsuite=speculation-rules

Wouldn't we be able to simplify the call using our NPM wrappers? #1002 already adds a wrapper to simplify this for e.g. auto-sizes, so we could do that here too.

@westonruter
Copy link
Member

Would that use the correct PHPUnit version (the one locally installed via PL composer.json)? Given you wouldn't be in the directory that has composer.json and vendor, I'm not 100% sure about it.

That's true. Although that can be easily resolved with a shell script that sets up the environment to work on the project. Someone could just add this to their .bashrc:

function workhere() {
    export PATH="$PATH:$(pwd)/vendor/bin"
}

And then when navigating to the performance repo do:

$ workhere

And then the phpunit command would be scoped to the project.

Interestingly, PhpStorm (er, IntelliJ) does this automatically for Node scripts. (VSCode does not currently implement this.) When I'm in the built-in IDE terminal, I see my $PATH is includes ~/repos/performance/node_modules/.bin. So I can actually skip doing npm run wp-env or npx wp-env and just do wp-env. (Good to know!) Interestingly, PhpStorm does not implement this also for Composer. (But I just filed a feature request.)

Wouldn't we be able to simplify the call using our NPM wrappers? #1002 already adds a wrapper to simplify this for e.g. auto-sizes, so we could do that here too.

True. Although the need to pass -- -- -- in order to send args to PHPUnit remains, for example to add --filter or --group. Also, the npm wrappers add additional latency and noise to running commands, so it would be nice if we didn't have to use them.

@thelovekesh
Copy link
Member Author

thelovekesh commented Feb 27, 2024

A partial reason for not going with individual files is to ease maintenance, I don't see a benefit of having the almost identical PHPUnit config file duplicated for every plugin in the monorepo.

TBH the testsuite approach looks like having more maintenance cost. Why?

  • Modifying the main config for individual plugin tests becomes cumbersome as the number of plugins grows. Also adding/removing entries in the testsuites section becomes repetitive for every plugin addition/removal.
  • Adjusting the main bootstrap file for specific plugin needs is less flexible and potentially error-prone. I think in a monorepo, it makes sense to share the configs but editing them for each other is not ideal.
Plugin name resolution logic in bootstrap file

$plugin_name = '';
foreach ( $_SERVER['argv'] as $index => $arg ) {
if (
'--testsuite' === $arg &&
isset( $_SERVER['argv'][ $index + 1 ] ) &&
'performance-lab' !== $_SERVER['argv'][ $index + 1 ] &&
file_exists( TESTS_PLUGIN_DIR . '/plugins/' . $_SERVER['argv'][ $index + 1 ] )
) {
$plugin_name = $_SERVER['argv'][ $index + 1 ];
}
}
if ( $plugin_name ) {
$plugin_test_path = TESTS_PLUGIN_DIR . '/plugins/' . $plugin_name;
tests_add_filter(
'plugins_loaded',
static function () use ( $plugin_test_path, $plugin_name ) {
// Check if plugin has a "plugin/plugin.php" file.
if ( file_exists( $plugin_test_path . '/' . $plugin_name . '.php' ) ) {
require_once $plugin_test_path . '/' . $plugin_name . '.php';
} elseif ( file_exists( $plugin_test_path . '/load.php' ) ) {
// Check if plugin has a "plugin/load.php" file.
require_once $plugin_test_path . '/load.php';
} else {
echo "Unable to locate standalone plugin bootstrap file in $plugin_test_path.";
exit( 1 );
}
},
1
);

  • Running tests from within a specific plugin directory becomes non-trivial due to automatic config resolution and can hinder the development/debugging of the individual plugin(s).

  • While development using --testsuite requires users to know the specific test suite name within the global phpunit config to run tests for a particular plugin. Now we can include test command as npm/composer script for each plugin but that would also require taking a look at npm/composer scripts to determine for which plugin which test command to run.

  • Also adding tests commands as npm/composer script approaches require managing and maintaining separate entries for each plugin. As the number of plugins grows, this leads to an increasingly cumbersome script management experience.

  • If you want to work on the plugin from its directory, the current setup has limitations. With the current approach, it will resolve the config automatically.

So, the tradeoff between both is making changes to the main PHPUnit config + updating tests bootstrap file + maintaining it on use cases vs keeping separate PHPUnit config which is just a one-time copy from any other plugin.

@felixarntz
Copy link
Member

@thelovekesh I'm not sure I agree with that assessment in terms of maintenance cost.

I think the only difference from a maintenance perspective is the following:

  1. With individual files, you copy the PHPUnit config for every new plugin.
  2. With a single config, you copy the testsuite bit within the one PHPUnit config file.

Both of that isn't really different in the amount of effort. However with option 1 you end up with a lot of duplicated config, while with option 2 you don't. When something in PHPUnit changes that leads to a change in the configuration, we need to change it everywhere, instead of in a single place.

That's why I think maintenance cost of individual PHPUnit config files is higher.

The only way that this could be resolved without all the duplication would be if we could embed a PHPUnit config file from within the individual other PHPUnit config files (similar to how #1002 does for PHPCS). Though I'm not sure that is possible.

@westonruter
Copy link
Member

Just so we're on the same page, what is the duplicate code in question?

phpunit.xml.dist

<phpunit
bootstrap="tests/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite name="default">
<directory suffix=".php">./tests</directory>
<exclude>./tests/utils</exclude>
</testsuite>
</testsuites>
<groups>
<exclude>
<group>ms-required</group>
</exclude>
</groups>
</phpunit>

tests/multisite.xml

<phpunit
bootstrap="./bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<php>
<const name="WP_TESTS_MULTISITE" value="1" />
</php>
<testsuites>
<testsuite name="default">
<directory suffix=".php">./</directory>
<exclude>./utils</exclude>
</testsuite>
</testsuites>
<groups>
<exclude>
<group>ms-excluded</group>
</exclude>
</groups>
</phpunit>

Aside: Both configs include this:

	<groups>
		<exclude>
			<group>ms-excluded</group>
		</exclude>
	</groups>

But this group doesn't exist in the tests. So perhaps it is dead code?

If the duplicated code consists of:

<phpunit
	bootstrap="./tests/bootstrap.php"
	backupGlobals="false"
	colors="true"
	convertErrorsToExceptions="true"
	convertNoticesToExceptions="true"
	convertWarningsToExceptions="true"
	>
	<testsuites>
		<testsuite name="default">
			<directory suffix=".php">./tests</directory>
		</testsuite>
	</testsuites>
</phpunit>

This doesn't seem like a problem as it's not a lot of config. The bootstrap PHP logic can be reused across the plugins. If the concern is that the configs diverge, a script could be added which syncs them all.

@westonruter
Copy link
Member

When something in PHPUnit changes that leads to a change in the configuration, we need to change it everywhere, instead of in a single place.

Also, has this ever happened before? If not, perhaps it is a theoretical problem?

@thelovekesh
Copy link
Member Author

The only way that this could be resolved without all the duplication would be if we could embed a PHPUnit config file from within the individual other PHPUnit config files (similar to how #1002 does for PHPCS). Though I'm not sure that is possible.

@felixarntz I am like 99ish percent sure we can't do it in PHPUnit, the way we can do it for PHPCS due to the nature of both tools.

However with option 1 you end up with a lot of duplicated config,

I agree there will be a duplicated config but that would be very less + it doesn't ask for any changes in most of the cases. For example: This repo uses the same approach in which the initial config is always the same in all config files but it's helping to make the test suite self-containable for every package and they just need to worry about single thing at a time.

Aside, I think we should also add the maintaining overheads for maintaining scripts in npm/composer and need to know the specific test suite name within the global phpunit config to run tests for a particular plugin.

@felixarntz
Copy link
Member

felixarntz commented Feb 27, 2024

@westonruter @thelovekesh

Just so we're on the same page, what is the duplicate code in question?

Everything that the config contains except the <testsuite /> blocks will be duplicated if we have individual files per plugin.

Aside: Both configs include this:

	<groups>
		<exclude>
			<group>ms-excluded</group>
		</exclude>
	</groups>

But this group doesn't exist in the tests. So perhaps it is dead code?

Note that only the multisite config excludes ms-excluded, the single site config excludes ms-required.
This is similar to WordPress core to allow having specific tests that only apply to single site or multisite. It's a good practice for plugins as well to have that available for when it's needed.

This doesn't seem like a problem as it's not a lot of config. The bootstrap PHP logic can be reused across the plugins. If the concern is that the configs diverge, a script could be added which syncs them all.

I agree there will be a duplicated config but that would be very less + it doesn't ask for any changes in most of the cases.

It is not a lot of config, but it'll pile up. It's two files (single site and multisite), and that for every plugin. Why duplicate config when we don't have to? Why go the complicated effort to implement a script to sync multiple files when we could just use a single file in the first place?

Just for a numeric comparison, with individual files we're talking 43 lines of config for every plugin (from both PHPUnit config, single site + multisite), whereas with centralized files (one for single site, one for multisite as is now) we're talking 8 lines of config for every plugin, and no duplication.

When something in PHPUnit changes that leads to a change in the configuration, we need to change it everywhere, instead of in a single place.

Also, has this ever happened before? If not, perhaps it is a theoretical problem?

PHPUnit makes a lot of breaking changes between versions, so this does actually happen. For instance, unless that has been fixed by now, the current WP core PHPUnit config includes some deprecated stuff when used with the latest PHPUnit versions. So the format of the config files occasionally requires changes. It's not often probably, but it's not a theoretical problem.


This is probably a pretty nit-picky discussion from both sides. Ultimately, either way will work. I personally don't see the value of having multiple config files. All the considerations about workflow appear to require other local environment setup steps that aren't bundled into the repository (i.e. unlikely to be used by many contributors), and regarding working from within the plugin folder, you'll have to pull down the main repository anyway, and it's not very large, so I'm not sure what's the value in that. Last but not least, as I said before, adding a new plugin will always require some changes in the root of the repository too (e.g. .github/CODEOWNERS), so I don't see why modifying the main PHPUnit file at the same time to add a new test suite would be a hassle.

From my perspective, the only convincing argument for individual config files is if they needed to diverge between individual plugins, but there's no real use-case for that, and if we ran into one we could reconsider.

In any case, I think this discussion is probably being exhausted soon. If you feel strongly about individual files though, I'm not going to block it.

@thelovekesh
Copy link
Member Author

Just for a numeric comparison, with individual files we're talking 43 lines of config for every plugin (from both PHPUnit config, single site + multisite)

It seems like we might be able to avoid duplicating the multisite configuration. Instead, we could potentially achieve the same functionality by simply passing the WP_MULTISITE environment variable through our current setup. Of course, if there are specific use cases where ms-excluded or ms-require groups are necessary(currently I see none), we can certainly add them for those particular plugins.

@joemcgill
Copy link
Member

Personally, unless we're going for full separation of responsibilities for each plugin to maintain it's own dependencies, build process, and development tooling, I would prefer that all tests be located in the root project's test directory, and that the primary way that we run all of the tests be to run the full suite, which activates all plugins and runs each plugin's tests. We also need to be able to easily run each individual plugin's tests in isolation, but that already seems by passing a specific test suite.

One of the main benefits of keeping these in a mono-repo in the way that we are developing them is that we can gain efficiencies by maintaining a single set of tools and configurations that are shared across standalone plugins. If we do end up deciding on full separation, I think we should consider whether the performance-lab plugin should also be treated as one of the plugins in the mono-repo, and not the root of the project.

@thelovekesh
Copy link
Member Author

I think separation can be on a need-to-go basis like we did for #1002 which is beneficial in operating the concern of text-domain for WP I18n sniffs. We could have added the text domain in the main config file but that was overkill.

IMO testsuite feature should be used within the scope of a self-containable single bootstrap file. Currently, based on the testsuite we are modifying it for other use cases which doesn't seems ideal when the same can be achieved by having a different config.

@westonruter
Copy link
Member

A couple more bits of feedback for the plugin monorepo I've noticed:

First, only PHPCS for the PL plugin can have its phpcs.xml.dist overridden with a phpcs.xml, since it is not explicitly specified:

"lint": "build-cs/vendor/bin/phpcs",

However, for a plugin like Auto Sizes, it is explicitly set to phpcs.xml.dist:

"lint:auto-sizes": "@lint -- ./plugins/auto-sizes --standard=./plugins/auto-sizes/phpcs.xml.dist",

This isn't great as env-specific overrides can't be done while also using composer run-script lint:auto-sizes. Instead, someone would have to navigate to the auto-sizes dir and do, for example:

../../build-cs/vendor/bin/phpcs

Secondly, when using PhpStorm, the presence of multiple PHPCS configs is problematic since it unfortunately doesn't support this. What this means is that if I've configured PhpStorm to use the root phpcs.xml.dist for PHPCS:

image

I then get zero checks performed when working on a plugin, since it is configured to ignore /plugins. But if I set the PHPCS config to point to plugins/auto-sizes/phpcs.xml.dist, but am working on Speculation Rules, then I get WordPress.WP.I18n.TextDomainMismatch errors since it is expecting auto-sizes instead of speculation-rules.

So what I have to do is copy the root phpcs.xml.dist to phpcs.xml and modify it to not ignore /plugins while also including all of the standalone text domains:

--- phpcs.xml.dist	2024-03-23 10:18:11.563329799 -0700
+++ phpcs.xml	        2024-03-25 10:17:50.367229615 -0700
@@ -3,7 +3,7 @@
 	<description>WordPress Coding Standards for the Performance Lab Plugin</description>
 	<rule ref="bin/phpcs/phpcs.ruleset.xml"/>
 
-	<config name="text_domain" value="performance-lab,default"/>
+	<config name="text_domain" value="performance-lab,default,optimization-detective,webp-uploads,speculation-rules,dominant-color-images,embed-optimizer"/>
 
 	<rule ref="WordPress.Files.FileName.NotHyphenatedLowercase">
 		<exclude-pattern>includes/server-timing/object-cache.copy.php</exclude-pattern>
@@ -11,6 +11,7 @@
 
 	<file>.</file>
 	<exclude-pattern>./node_modules/*</exclude-pattern>
-	<exclude-pattern>./plugins/*</exclude-pattern>
 	<exclude-pattern>./vendor/*</exclude-pattern>
 </ruleset>

This isn't terrible, but it is something to be aware of for the DX of developers using PhpStorm.

@westonruter
Copy link
Member

Oh, I forgot perhaps the most important thing here which is broken. Currently when attempting to make a change to a standalone plugin, lint-staged is not checking any of the PHP files in /plugins, because PHP files are only checked with composer run-script lint which, as mentioned above, only uses the root phpcs.xml.dist which ignores /plugins/*:

performance/package.json

Lines 53 to 54 in 5e570fa

"*.php": [
"composer run-script lint",

I've opened #1089 to fix this.

Aside: Perhaps the Performance Lab plugin should be moved to plugins/performance-lab alongside the other plugins? This would make it more consistent.

@felixarntz
Copy link
Member

@westonruter

Perhaps the Performance Lab plugin should be moved to plugins/performance-lab alongside the other plugins? This would make it more consistent.

@joemcgill had suggested something along those lines before too, treat it more similar to the other plugins.

I think that idea makes sense, though I think we need to be careful timing-wise because that will affect lots of other workflow aspects which will then need to be changed. We should probably not make such a change in trunk until after the 3.0 release.

Probably the best way to get started is to open an issue to think through what that change would entail. This issue here is good as a brainstorming starting point, but let's be mindful of not making it too broad, which would complicate discussion and code.

@thelovekesh
Copy link
Member Author

@westonruter In #1090, I have moved lint and format scripts to npm which allows PHPCS to set its config file from the respective plugin directory and we can now run lint and format commands from any directory and npm will take care of it. Can you please test if this change is improving any existing workflows?

@felixarntz
Copy link
Member

Reposting here from #1065 (comment):

The discussion on whether to use individual PHPUnit test suites per plugin was never resolved, so IMO it's a bit premature that #1065 now implements it.

While it's possible that we won't find a solution that all of us fully agree with, we should still make an effort to facilitate / formalize a decision here before diving into the code to do it. I think we should take a step back here and instead continue the conversation in the issue. What concrete problems are we trying to solve here, and how would individual test suites per plugin address them?

@westonruter
Copy link
Member

@joemcgill Responding to #1065 (review) with my 2 cents:

I'm still unclear why it is more beneficial to move each plugins tests, config, and test data to their respective plugin's directory, rather than maintaining a single test folder that covers all of the plugins in the mono-repo.

To me the big benefits are keeping the tests closer to the code being tested improves the DX. Instead of there being two auto-sizes directories in separate parts of the repo, there is just one directory. Since these are standalone plugins, they are intended to work without any other plugin active. As such, development on a plugin would generally be done in isolation of the other plugins. So it makes sense to me that I would say, "OK, today I'm working on Embed Optimizer, so I'm going to cd plugins/embed-optimizer and live here for awhile." Having tests located elsewhere makes it more cumbersome, IMO.

There are a few things that I think could be downsides to this approach, or that are at least missing from the current implementation, including:

  • All tests are being run independently, meaning we wouldn't catch any compatibility failures between plugins. Since we generally intend for many of these to be run together, I think that's something we should be looking out for.

Since the plugins are standalone, is this necessary? If it is, the plugin bootstrap code for an individual plugin could still load the other plugins.

  • Inability to share test data between plugins.
  • Inability to share fixtures or test helpers between plugins.

Can't this shared data simply be stored up in ../../tests/data/ for example?

Ideally, as I mentioned in #1012 (comment), I think we would be better off having a single combined test suite that covered all of the plugins, but could be filtered to individual plugins for convenience, similar to how you can filter a large test suite by passing a --group or --filter arg to PHPUnit.

I'm not sure given that the plugins are now intended to be standalone, as I shared above. It seems that them running in isolation makes more sense as the default state rather than them all running together. But it would still be good to also be able to test them all running together, but perhaps that would be enabled via some environment variable?

And given that standalone plugins are indeed standalone, this makes sense to me as well that there would be a separate phpunit.xml.dist per standalone plugin.

@thelovekesh
Copy link
Member Author

While it's possible that we won't find a solution that all of us fully agree with, we should still make an effort to facilitate / formalize a decision here before diving into the code to do it.

Agree, and initially, it was worked as a POC to show and tell its working. The new PR just cherry-picked those changes from POC.


All tests are being run independently, meaning we wouldn't catch any compatibility failures between plugins. Since we generally intend for many of these to be run together, I think that's something we should be looking out for.

I think the same is also true for running them in isolation since they are standalone. Testing these plugins with each other should be part of integration tests IMO.

@felixarntz
Copy link
Member

felixarntz commented Apr 3, 2024

@westonruter @thelovekesh I agree with some of your points. Most importantly, +1 on that the PHPUnit tests should be focused on the standalone plugins, on their own (standalone). So I agree from a functionality perspective.

However, that doesn't mandate a specific folder structure. One of the benefits of a monorepo is that configuration can be reused, instead of being duplicated in every single plugin. This duplication results in a maintenance burden, such as having to update all N copies of a file with the same change once something in the workflow requires an update. All the "dotfiles" (config files) that individual GitHub repos typically come with, we shouldn't try to recreate that in every single plugin folder. At this point we're defeating the purpose of a monorepo, other than not having separate repos to maintain.

I don't feel strongly about where we locate the actual test directories, whether centrally in /tests/plugins/{plugin} or in /plugins/{plugin}/tests, because neither of those results in a maintenance burden IMO. Either would work for me personally.

But I feel quite strongly about not duplicating the PHPUnit config files for every single plugin, as they look the same everywhere. With PHPCS, we found a good solution as its syntax allows loading from another main config. It's a bummer PHPUnit doesn't seem to support this. But so far there has not been any requirement outlined that would actually justify a duplication because of distinct changes. I think as long as that's the case, having separate config files is only a theoretical benefit, resulting in only a maintenance burden via duplicate code.

@thelovekesh
Copy link
Member Author

However, that doesn't mandate a specific folder structure. One of the benefits of a mono repo is that configuration can be reused, instead of being duplicated in every single plugin.

@felixarntz I agree that we should try to reuse the config, but it also depends on whether the tools provide that flexibility. In the case of PHPCS we had that but as you mentioned PHPUnit does not provide it.

This duplication results in a maintenance burden, such as having to update all N copies of a file with the same change once something in the workflow requires an update. All the "dotfiles" (config files) that individual GitHub repos typically come with, we shouldn't try to recreate that in every single plugin folder.

I'm having difficulty envisioning a scenario where updating all configuration files would be required. I don't think tool configurations are updated frequently. If we are talking about such cases here, then it can also be true for PHPCS config. What if PHPCS changes the way how we extend configs? What if the text_domain config gets changed? In all these cases, all the config files will require changes so maintenance burden is also there.

Also would be happy to know what type of duplication we want to eliminate from phpunit.xml.dist that removes the maintenance burden.

At this point we're defeating the purpose of a monorepo, other than not having separate repos to maintain.

I don't think it's fair to call it a defeat of the purpose just from the PHPUnit config POV since mono-repos are subject to complex workflows, especially with toolings.

@felixarntz
Copy link
Member

@thelovekesh

I'm having difficulty envisioning a scenario where updating all configuration files would be required. I don't think tool configurations are updated frequently.

Probably not, but they do sometimes require updates. PHPUnit in particular moves much faster than e.g. WordPress and new versions often have breaking changes, including requirements in the config files.

Anyway, as mentioned before my point is that still nowhere in this issue or the PR I have seen mentioned any concrete use-case on where those configurations need to differ. Without a real reason, I don't see why we would want to separate the config files as it only addresses a theoretical concern.

If we are talking about such cases here, then it can also be true for PHPCS config. What if PHPCS changes the way how we extend configs?

It is. But I don't think it's worth discussing that, since PHPCS fortunately provides a way for us to reference another config. Plus, for WPCS we do have a real concern, such as the text_domain one you mention. But for PHPUnit no such problem has been raised to my knowledge.

Also would be happy to know what type of duplication we want to eliminate from phpunit.xml.dist that removes the maintenance burden.

Almost the entire config is duplicated, as all config files in your PR for instance look the same, except for the plugin reference. So unless we can reference a single main config from other config files, this creates a maintenance burden.

@thelovekesh
Copy link
Member Author

Probably not, but they do sometimes require updates. PHPUnit in particular moves much faster than e.g. WordPress and new versions often have breaking changes, including requirements in the config files.

I think that's true for its APIs and not for configs. Particularly in https://github.com/WordPress/wordpress-develop/commits/trunk/phpunit.xml.dist, I can't find any major changes in the config schema.

Anyway, as mentioned before my point is that still nowhere in this issue or the PR I have seen mentioned any concrete use-case on where those configurations need to differ. Without a real reason, I don't see why we would want to separate the config files as it only addresses a theoretical concern.

Here are some specific situations where these changes could be really helpful:

  • In a big mono-repo, it's normal to focus on one part at a time, like standalone plugins. But right now, we can't easily test them or tweak their settings without messing up our focus, which makes things harder from the DX perspective.

  • Whenever we want to test a plugin alongside others, we have to do some tricky stuff in the main bootstrap setup file, depending on what test suite we're using. Gradually plugins will increase and such hacks will be required which will cause a limbo state in the end. I think it's better if we streamline the process beforehand and keep such concerns separate at the standalone plugin level.

  • If we need to change the testing setup for a particular plugin, it's a long process(due to the tooling nature + config for multiple things at one place) instead of just adding a simple phpunit.xml override file at the plugin level.

  • When we need to set up things like global variables or constants for a plugin, we have to do it manually in the bootstrap file, again with the reference of the test suite name, even though PHPUnit offers better ways to handle this - https://docs.phpunit.de/en/9.6/configuration.html#appendixes-configuration-php

  • When we merge a standalone plugin into the main code, it should be easy, just by deleting its folder. But right now, it's not that simple because things are scattered over multiple places.

These are just some examples of why these changes would be helpful.

Almost the entire config is duplicated, as all config files in your PR, for instance, look the same, except for the plugin reference. So unless we can reference a single main config from other config files, this creates a maintenance burden.

I hold a different viewpoint on this matter. The only duplicated aspect seems to be the PHPUnit arguments, and it's improbable that they'll undergo frequent changes. In the past 11 years, the WP Core config has only added new arguments once, and that was three years ago.

<phpunit
	bootstrap="tests/bootstrap.php"
	backupGlobals="false"
	colors="true"
	convertErrorsToExceptions="true"
	convertNoticesToExceptions="true"
	convertWarningsToExceptions="true"
	defaultTestSuite="default"
	>
</phpunit>

The rest of the setup has been moved from the main PHPUnit configuration to the configurations for each plugin. So, any changes we make in the future would affect both the main setup and the individual plugin setups. Whether we keep them together or separate, updating them will require changes in multiple spots, making the maintenance effort similar either way.

@westonruter
Copy link
Member

Just to note that Jetpack is also a monorepo of plugins and each plugin has its own phpunit.xml.dist. For example, Jetpack Boost: https://github.com/Automattic/jetpack/tree/trunk/projects/plugins/boost

@joemcgill
Copy link
Member

Reading back through this conversation, I guess I have a bit of a different perspective on how we intend to use this repo and how we prioritize tradeoffs between treating each plugin as an individual unit vs thinking of these as a collection of features that are intended to be developed and used together, but happen to be published in a manner that allows individual use.

My perspective is that the Performance Lab program exists primarily to allow us to support developing and testing performance enhancements for WordPress in a collective manner, rather than as individual efforts. The benefits that we get from this strategy are many—including being able to share tooling, processes, configuration, and visibility of the projects being supported by the WordPress Performance Team. The fact that we've decided to implement and publish experimental features in the form of standalone plugins is merely tactical, as a result of #618, and not a primary objective of this program.

Understandably, this does lead to some confusion if the expectation is that each standalone plugin should be treated as an isolated unit that is unrelated to the rest of the repo. For example, from @westonruter's earlier comment:

Since these are standalone plugins, they are intended to work without any other plugin active. As such, development on a plugin would generally be done in isolation of the other plugins. So it makes sense to me that I would say, "OK, today I'm working on Embed Optimizer, so I'm going to cd plugins/embed-optimizer and live here for awhile." Having tests located elsewhere makes it more cumbersome, IMO.

This is not the primary intended development experience of this repo, IMO. Instead, the I believe the DX that should be prioritized is one where you are primarily working from the root directory of the repo, and running a development environment that is configured to automatically load all of the plugins from this repo (usually using the included .wp-env.json config for wp-env) so they can easily be tested in isolation AND as a collection of features that we expect will work together.

As such, I think having a shared set of Unit Tests for the entire codebase gives us the opportunity to take advantage of shared test functionality and test data across the test classes that cover different individual plugins developed within this repo rather than needing to duplicate data and test helpers across different plugins. For example, we have several plugins that implement different features related to image performance. Being able to share test data and develop shared test helpers, traits, etc. makes creating tests for these types of plugins easier.

Ultimately, if the preference is that we prioritize the DX for working on standalone plugins in isolation, then I think we may want to consider whether and to what extent each plugin should define its own dependencies, configuration, tooling, etc. rather than trying to manage some at the repo level and some at the plugin level.

@westonruter
Copy link
Member

westonruter commented Apr 4, 2024

As such, I think having a shared set of Unit Tests for the entire codebase gives us the opportunity to take advantage of shared test functionality and test data across the test classes that cover different individual plugins developed within this repo rather than needing to duplicate data and test helpers across different plugins. For example, we have several plugins that implement different features related to image performance. Being able to share test data and develop shared test helpers, traits, etc. makes creating tests for these types of plugins easier.

We can still do this even when the unit tests are located with each individual plugin, even when there are separate configs. The test helpers can be located in a root /tests directory which are included by individual plugins' tests as needed. For example, in /plugins/auto-sizes/tests/bootstrap.php

<?php
require_once __DIR__ . '/../../../tests/helpers.php';
// ...

@thelovekesh
Copy link
Member Author

Thanks @joemcgill.

Reading back through this conversation, I guess I have a bit of a different perspective on how we intend to use this repo and how we prioritize tradeoffs between treating each plugin as an individual unit vs thinking of these as a collection of features that are intended to be developed and used together, but happen to be published in a manner that allows individual use.

Viewed at a high level, these plugins represent performance-focused features, yet each holds its unique functionality at a granular level. For instance, WebP Uploads handles image generation, while Optimization Detective specializes in optimization tasks. Given their standalone nature, they deserve development flexibility. Integration, if required, should be managed through integration tests, maintaining both individual integrity and collaborative cohesion.

At a detailed level, the flexibility introduced by #1065 encompasses all possible combinations. For instance, if plugin X relies on Y, simply include Y while testing X in its bootstrap file. This approach also facilitates testing Unit Tests for X independently and integration tests separately with the Y plugin enabled, offering comprehensive flexibility.

My perspective is that the Performance Lab program exists primarily to allow us to support developing and testing performance enhancements for WordPress in a collective manner, rather than as individual efforts.

In any scenario, I believe that none of the Developer Experience (DX) initiatives affect this; instead, they streamline processes for improved efficiency.

The benefits that we get from this strategy are many—including being able to share tooling, processes, configuration, and visibility of the projects being supported by the WordPress Performance Team.

There seems to be a misunderstanding regarding how we approached extended configuration for PHPCS, and this misunderstanding is being generalized to other tools, which may not apply uniformly due to their differing natures and functionalities. For instance, PHPCS is primarily designed to import rulesets, allowing CS packages to expose their sniffs to the PHPCS runner. Conversely, in PHPUnit, the configuration intended for importation, or sharing does not revolve around its XML configuration but rather involves extensions and test runner wrappers.

Even though PHPUnit can share configurations when needed, we must recognize the unique features of each tool. Right now, we're using shared tooling, and we can adapt as necessary.

Ultimately, if the preference is that we prioritize the DX for working on standalone plugins in isolation, then I think we may want to consider whether and to what extent each plugin should define its own dependencies, configuration, tooling, etc. rather than trying to manage some at the repo level and some at the plugin level.

Our approach to single-plugin functionality within modules was effective, but the transition to a mono repo presents an opportunity to refine our performance features with a more concentrated focus. Similar to the streamlined management in a monorepo, we can now optimize our development process for greater efficiency and effectiveness.

@felixarntz
Copy link
Member

felixarntz commented Apr 5, 2024

@thelovekesh @joemcgill @westonruter I have spent some time looking around at other projects in regards to the points that are being discussed here, which has reshaped my perspective a little.

Sharing a few thoughts here:

Our approach to single-plugin functionality within modules was effective, but the transition to a mono repo presents an opportunity to refine our performance features with a more concentrated focus. Similar to the streamlined management in a monorepo, we can now optimize our development process for greater efficiency and effectiveness.

It's worth noting that modules were just as much standalone as the plugins are now, this was a mandate from the beginning of the repository. So we were using shared tooling from the beginning, while the production codebases of the modules (now plugins) were not allowed to rely on anything external.

we must recognize the unique features of each tool. Right now, we're using shared tooling, and we can adapt as necessary.

+1 to that. Some tools are more limited than others in catering for the workflows we would want to have. I think when we run into such limitations, we have three options:

  • Find ways to work within the limitations of the tool.
  • Implement our own tooling (e.g. a custom script) that works around the limitations of the third-party tool.
  • Use another third-party tool that doesn't have the limitation.

With the latter, we also have to consider many other factors, such as how well another tool is maintained, familiarity of the WordPress developer community with it, how appropriate it is for the job etc.

Most importantly though, I think we are getting stuck at this point in tooling discussions, but we have not even determined as a team what our developer experience should holistically look like.

An example is that some of us suggest developing from a specific plugin's directory, while others suggest developing from the repository root. Those are entirely different approaches we need to discuss more. Potentially we can later find a tooling approach that works for both. What are the benefits of one or the other? Do we want to optimize for one or the other?

I do agree with @joemcgill's assessment that this project and monorepo is a collective effort. So we should not just have individual plugin directories where the maintainers of said plugin can then implement anything in any way they like. A shared approach and, to some degree, shared tooling is crucial here. That said, this does not necessarily mean we can't have individual plugin specific tooling. But I think we need to think about tooling collectively.

We're already doing that in some instances. Having a central PHPCS config while also having a bare minimum PHPCS config per plugin which mostly imports the central one is a great solution that caters for both the collective and individual needs.

A more complicated example is PHPUnit (see #1065), where the PHPCS approach is not really feasible as the config files cannot be composed. Seeing that several other popular monorepo projects (e.g. https://github.com/symfony/symfony or https://github.com/Automattic/jetpack) are using phpunit.xml.dist files per subdirectory makes me more comfortable with that approach, potentially. Though, if we go with that approach, I would also argue that the approach of using NPM for PHP tooling just because of composer.json limitations (see #1090) does not make sense - we might as well have composer.json files per plugin in that case, which is just as common as phpunit.xml.dist files per plugin.

I realize I now mentioned specific examples here, though that was mostly to clarify my overarching perspective. Let's take a step back and define which kind of developer workflows we want to encourage and optimize for, holistically, rather than discussing individual tooling concerns without having defined the bigger picture.

@westonruter
Copy link
Member

An example is that some of us suggest developing from a specific plugin's directory, while others suggest developing from the repository root. Those are entirely different approaches we need to discuss more. Potentially we can later find a tooling approach that works for both. What are the benefits of one or the other? Do we want to optimize for one or the other?

If we do facilitate developing with the working directory being the plugin directory, this doesn't mean that the ability to develop from the root would have to be sacrificed. I think both should be facilitated. But IMO, it makes more sense to optimize for developing with the standalone plugin as the working directory (e.g. running phpcs from there automatically locates the correct phpcs.xml.dist).

@thelovekesh
Copy link
Member Author

thelovekesh commented Apr 5, 2024

An example is that some of us suggest developing from a specific plugin's directory, while others suggest developing from the repository root. Those are entirely different approaches we need to discuss more. Potentially we can later find a tooling approach that works for both. What are the benefits of one or the other? Do we want to optimize for one or the other?

I'm in favor of making it work both ways, as we can't mandate developers to work from a specific root or plugin directory; it's entirely their choice. Adopting the NPM script runner can address this issue promptly, ensuring equal functionality in both scenarios. By standardizing our scripts in NPM instead of Composer, we can achieve this goal. While I acknowledge that multiple scripts might result in a bloated package.json file, we can explore Joe's suggestion of consolidating commands and passing arguments to target specific plugin(s), thereby maintaining simplicity with a single command.

Example:

# A single command can be used to handle all use cases IMO.
npm run lint:php # Run linting for PL plugin.
npm run lint:php -- --phpcs-args # Pass arguments to tool executable.
npm run lint:php --plugin=foo -- --phpcs-args # Pass both NPM and tool args.
npm run lint:php --plugin=foo # Run linting for specific plugin.
npm run lint:php --plugin=foo,bar # Run linting for two plugins at a time.
npm run lint:php --all # Run linting on all files.

Though, if we go with that approach, I would also argue that the approach of using NPM for PHP tooling just because of composer.json limitations (see #1090) does not make sense - we might as well have composer.json files per plugin in that case, which is just as common as phpunit.xml.dist files per plugin.

While we can do so, is it truly necessary? Considering that we already have an executable binary for each tool in ./vendor/bin by default, and NPM can execute them from any directory out of the box. The exception lies with Jetpack, where multiple use cases exist, and composer autoloading per plugin is one of them, which cannot be accomplished with a single composer config. Moreover, given the considerable weight of the Jetpack repo, configuring per plugin aids in use cases like sparse git checkout, where users may prefer to clone only what is necessary.

EDIT: Seems like I just deleted the previous comment history which was an edit in above commands example.

@thelovekesh
Copy link
Member Author

But IMO, it makes more sense to optimize for developing with the standalone plugin as the working directory (e.g. running phpcs from there automatically locates the correct phpcs.xml.dist).

@westonruter We can accomplish this in both scenarios by utilizing NPM as a script runner. You can refer to the examples provided above. Alternatively, those who prefer not to use an NPM run the command can execute binaries directly, such as ../../vendor/bin/phpcs. Another option is to use phpcs directly if ./vendor/bin is added to the PATH by their IDE/bashrc or themselves 😎.

@thelovekesh
Copy link
Member Author

@felixarntz Just checking if we have any updates here to unblock #1065.

@felixarntz
Copy link
Member

@thelovekesh I think we need to further discuss what we actually want here. Kind of following up on #1012 (comment), I think we should take a step back from further developing towards a solution we haven't even defined what it should look like.

The major overarching question to answer here is: Is our encouraged development workflow from the repository's root directory, or from the individual plugin directories? Making a decision on that will guide our current and future engineering decisions.

While ideally we would be able to cater equally for both, we're already seeing in several instances that that is either not possible or at least requiring complicated workarounds that lead to further discussions because they may seem hacky. Additionally, catering for both, in part because of those complexities, will require more time and resource investment and thus distract us from what we actually should be doing - work on performance features for the end users.

Let's discuss the tradeoffs between the two development approaches. After considering these, we should make a decision on what is our encouraged and formally supported workflow. From there, we can implement accordingly.

@felixarntz
Copy link
Member

Catching up after my long leave, any updates in regards to this issue? I saw #1262 got merged, which is fair, though it seems that #1262 (comment) still needs to be addressed.

Anything else that happened related to this topic but outside this issue?

Curious for any updates @joemcgill @westonruter @thelovekesh

@westonruter
Copy link
Member

@felixarntz Welcome back 🎉

A couple other PRs that are related to this issue:

These make it much easier to run the tests for a single standalone plugin.

Also related is:

This helper trait is in the shared /tests directory and is reused by multiple plugins. This would work regardless of whether there is a single phpunit.xml for the entire plugin suite or individual phpunit.xml configs for each standalone plugin. Nevertheless, there doesn't seem to be a strong need to have separate phpunit.xml configs at the moment. So I think we can close this issue and reconsider down the road.

though it seems that #1262 (comment) still needs to be addressed

The Enhanced Responsive Images (auto-sizes) plugin now bootstraps Image Prioritizer and Optimization Detective when it runs its tests since when that plugin is active it has additional integrations (#1322). The same is true for Embed Optimizer which bootstraps Optimization Detective (#1302). And of course Image Prioritizer has to bootstrap Optimization Detective since there is a plugin dependency. Other than these cases, we don't have other situations where different combinations of plugins are tested. But it seems these could be added in /tests as the need arises. We'd have to re-think the --testsuite arg we use in bootstrapping.

I think we can close this issue as completed.

@westonruter
Copy link
Member

The Enhanced Responsive Images (auto-sizes) plugin now bootstraps Image Prioritizer and Optimization Detective when it runs its tests since when that plugin is active it has additional integrations (#1322). The same is true for Embed Optimizer which bootstraps Optimization Detective (#1302). And of course Image Prioritizer has to bootstrap Optimization Detective since there is a plugin dependency. Other than these cases, we don't have other situations where different combinations of plugins are tested. But it seems these could be added in /tests as the need arises. We'd have to re-think the --testsuite arg we use in bootstrapping.

And yet, see #1433 (comment).

@felixarntz
Copy link
Member

Left a reply in #1433 (comment). I feel like generic tests for a "random" combination of our plugins active wouldn't make to much sense from a PHPUnit perspective. IMO this would be mostly about ensuring there's no fatal error from the mere presence of those plugins together.

For more specific scenarios of testing a plugin with its dependency, I think those are still first and foremost testing of that plugin and therefore make more sense in the plugin's own tests directory, maybe as a second test suite with the dependency plugin active.

On another note, +1 to closing this issue as completed.

@thelovekesh
Copy link
Member Author

For more specific scenarios of testing a plugin with its dependency, I think those are still first and foremost testing of that plugin and therefore make more sense in the plugin's own tests directory, maybe as a second test suite with the dependency plugin active.

Absolutely! Ideally, if plugin Foo is designed to work with plugin Bar, it should have two test suites:

  1. Its own unit test suite.
  2. An integration test suite for plugin Foo with Bar.

With our current setup, it would be challenging to test things this way because we only have a single bootstrap and PHPUnit configuration file. Having an independent PHPUnit config and bootstrap file for each plugin, located in its own test directory, is something to consider in such cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Infrastructure Issues for the overall performance plugin infrastructure
Projects
None yet
Development

No branches or pull requests

5 participants