diff --git a/cli.php b/cli.php index 104ad8d47..3d7a40dbd 100644 --- a/cli.php +++ b/cli.php @@ -28,6 +28,31 @@ require_once __DIR__ . '/vendor/autoload.php'; } +/* + * Load the consumer-supplied bootstrap file (if configured) before the command is registered. + * This mirrors the behaviour of the `object-cache.php` drop-in so that integrations have one + * consistent extension point across admin and WP-CLI paths. + * + * The constant is expected to hold an absolute filesystem path. When the constant is defined + * but the file is missing, a WP-CLI warning is emitted and execution continues. + */ +if ( defined( 'WP_PLUGIN_CHECK_BOOTSTRAP_FILE' ) ) { + $plugin_check_bootstrap_file = WP_PLUGIN_CHECK_BOOTSTRAP_FILE; + if ( is_string( $plugin_check_bootstrap_file ) && '' !== $plugin_check_bootstrap_file ) { + if ( is_file( $plugin_check_bootstrap_file ) ) { + require_once $plugin_check_bootstrap_file; + } else { + WP_CLI::warning( + sprintf( + 'Plugin Check: WP_PLUGIN_CHECK_BOOTSTRAP_FILE points to "%s", but the file does not exist.', + $plugin_check_bootstrap_file + ) + ); + } + } + unset( $plugin_check_bootstrap_file ); +} + if ( ! isset( $context ) ) { $context = new Plugin_Context( __DIR__ . '/plugin.php' ); } diff --git a/drop-ins/object-cache.copy.php b/drop-ins/object-cache.copy.php index 5973d5d2e..15daf9fed 100644 --- a/drop-ins/object-cache.copy.php +++ b/drop-ins/object-cache.copy.php @@ -34,6 +34,32 @@ function plugin_check_initialize_runner() { require_once $plugin_dir . 'vendor/autoload.php'; + /* + * Load the consumer-supplied bootstrap file (if configured) before the runner is initialized. + * The bootstrap file is the earliest point at which an integration can register listeners for + * plugin-check actions/filters, since this drop-in runs before mu-plugins. + * + * The constant is expected to hold an absolute filesystem path. When the constant is defined + * but the file is missing, a PHP warning is emitted so that misconfiguration is visible; PCP + * keeps running regardless. + */ + if ( defined( 'WP_PLUGIN_CHECK_BOOTSTRAP_FILE' ) ) { + $plugin_check_bootstrap_file = WP_PLUGIN_CHECK_BOOTSTRAP_FILE; + if ( is_string( $plugin_check_bootstrap_file ) && '' !== $plugin_check_bootstrap_file ) { + if ( is_file( $plugin_check_bootstrap_file ) ) { + require_once $plugin_check_bootstrap_file; + } else { + trigger_error( + sprintf( + 'Plugin Check: WP_PLUGIN_CHECK_BOOTSTRAP_FILE points to "%s", but the file does not exist.', + $plugin_check_bootstrap_file + ), + E_USER_WARNING + ); + } + } + } + if ( class_exists( 'WordPress\Plugin_Check\Utilities\Plugin_Request_Utility' ) ) { // Initialize the Check Runner class based on the request. WordPress\Plugin_Check\Utilities\Plugin_Request_Utility::initialize_runner(); diff --git a/includes/Checker/Check_Result.php b/includes/Checker/Check_Result.php index b2f43e033..fe856aa74 100644 --- a/includes/Checker/Check_Result.php +++ b/includes/Checker/Check_Result.php @@ -97,6 +97,8 @@ public function plugin() { /** * Adds an error or warning to the respective stack. * + * @SuppressWarnings(PHPMD.NPathComplexity) + * * @since 1.0.0 * * @param bool $error Whether it is an error message. @@ -130,9 +132,35 @@ public function add_message( $error, $message, $args = array() ) { array_intersect_key( $args, $defaults ) ); - $file = str_replace( $this->plugin()->path( '/' ), '', $data['file'] ); - $line = $data['line']; - $column = $data['column']; + // Normalize the file path before the filter so consumers see the same value as the stored entry. + $data['file'] = str_replace( $this->plugin()->path(), '', $data['file'] ); + + /** + * Filters a single check result entry before it is recorded. + * + * Return `null` (or any non-array value) to suppress the entry entirely. + * Return a modified array to record the modified entry instead. + * The `$is_error` argument continues to drive whether the entry is stored + * as an error or a warning regardless of changes made to the filtered array — + * promotion / demotion is intentionally out of scope here. + * + * @since 2.0.0 + * + * @param array|null $data Entry data with keys + * `message`, `code`, `file`, `line`, `column`, `link`, `docs`, `severity`. + * Return `null` to drop the entry. + * @param Check_Result $result The check result the entry will be added to. + * @param bool $is_error True if the entry is being recorded as an error, false if as a warning. + */ + $data = apply_filters( 'wp_plugin_check_check_result', $data, $this, $error ); + + if ( ! is_array( $data ) ) { + return; + } + + $file = isset( $data['file'] ) ? (string) $data['file'] : ''; + $line = isset( $data['line'] ) ? (int) $data['line'] : 0; + $column = isset( $data['column'] ) ? (int) $data['column'] : 0; unset( $data['line'], $data['column'], $data['file'] ); if ( $error ) { diff --git a/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php b/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php index d3bde6a40..031dbc492 100644 --- a/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php +++ b/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php @@ -59,6 +59,7 @@ abstract protected function get_args( Check_Result $result ); * Amends the given result by running the check on the associated plugin. * * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * * @since 1.0.0 * @@ -92,6 +93,22 @@ final public function run( Check_Result $result ) { $args = $this->get_args( $result ); + /** + * Filters the PHPCS arguments for a check before it runs. + * + * Lets integrations override the PHPCS standard, runtime-set values, + * extensions, installed_paths, sniffs and other arguments returned by + * the check's `get_args()` implementation — without having to subclass + * and swap the check via `wp_plugin_check_checks`. + * + * @since 2.0.0 + * + * @param array $args PHPCS arguments returned by `get_args()`. + * @param Abstract_PHP_CodeSniffer_Check $check The check instance. + * @param Check_Result $result The check result. + */ + $args = apply_filters( 'wp_plugin_check_phpcs_args', $args, $this, $result ); + // Reset PHP_CodeSniffer config. $this->reset_php_codesniffer_config(); diff --git a/includes/Checker/Runtime_Environment_Setup.php b/includes/Checker/Runtime_Environment_Setup.php index f01169364..d8f2b05c3 100644 --- a/includes/Checker/Runtime_Environment_Setup.php +++ b/includes/Checker/Runtime_Environment_Setup.php @@ -24,73 +24,103 @@ final class Runtime_Environment_Setup { * * @global wpdb $wpdb WordPress database abstraction object. * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function set_up() { global $wpdb, $wp_filesystem; - require_once ABSPATH . '/wp-admin/includes/upgrade.php'; - - // Get the existing site URL. - $site_url = get_option( 'siteurl' ); - - // Get the existing active plugins. - $active_plugins = get_option( 'active_plugins' ); - - // Get the existing active theme. - $active_theme = get_option( 'stylesheet' ); - - // Get the existing permalink structure. - $permalink_structure = get_option( 'permalink_structure' ); - - // Set the new prefix. - $prefix_cleanup = $this->amend_db_base_prefix(); - - // Create and populate the test database tables if they do not exist. - if ( $wpdb->posts !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->posts ) ) ) { - /* - * Set the same permalink structure *before* install finishes, - * so that wp_install_maybe_enable_pretty_permalinks() does not flush rewrite rules. - * - * See https://github.com/WordPress/plugin-check/issues/330 - */ - add_action( - 'populate_options', - static function () use ( $permalink_structure ) { - /* - * If pretty permalinks are not used, temporarily enable them by setting a permalink structure, to - * avoid flushing rewrite rules in wp_install_maybe_enable_pretty_permalinks(). - * Afterwards, on the 'wp_install' action, set the original (empty) permalink structure. - */ - if ( ! $permalink_structure ) { - add_action( - 'wp_install', - static function () use ( $permalink_structure ) { - update_option( 'permalink_structure', $permalink_structure ); - } - ); - $permalink_structure = '/%postname%/'; + /** + * Fires before the runtime environment is set up. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all setup steps. + * } + */ + do_action( 'wp_plugin_check_before_runtime_setup', array( 'early_exit' => false ) ); + + try { + require_once ABSPATH . '/wp-admin/includes/upgrade.php'; + + // Get the existing site URL. + $site_url = get_option( 'siteurl' ); + + // Get the existing active plugins. + $active_plugins = get_option( 'active_plugins' ); + + // Get the existing active theme. + $active_theme = get_option( 'stylesheet' ); + + // Get the existing permalink structure. + $permalink_structure = get_option( 'permalink_structure' ); + + // Set the new prefix. + $prefix_cleanup = $this->amend_db_base_prefix(); + + // Create and populate the test database tables if they do not exist. + if ( $wpdb->posts !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->posts ) ) ) { + /* + * Set the same permalink structure *before* install finishes, + * so that wp_install_maybe_enable_pretty_permalinks() does not flush rewrite rules. + * + * See https://github.com/WordPress/plugin-check/issues/330 + */ + add_action( + 'populate_options', + static function () use ( $permalink_structure ) { + /* + * If pretty permalinks are not used, temporarily enable them by setting a permalink structure, to + * avoid flushing rewrite rules in wp_install_maybe_enable_pretty_permalinks(). + * Afterwards, on the 'wp_install' action, set the original (empty) permalink structure. + */ + if ( ! $permalink_structure ) { + add_action( + 'wp_install', + static function () use ( $permalink_structure ) { + update_option( 'permalink_structure', $permalink_structure ); + } + ); + $permalink_structure = '/%postname%/'; + } + add_option( 'permalink_structure', $permalink_structure ); } - add_option( 'permalink_structure', $permalink_structure ); - } - ); + ); - $this->install_wordpress( $site_url, $active_theme, $active_plugins ); - } + $this->install_wordpress( $site_url, $active_theme, $active_plugins ); + } - // Restore the old prefix. - $prefix_cleanup(); + // Restore the old prefix. + $prefix_cleanup(); - // Return early if the plugin check object cache already exists. - if ( defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) && WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) { - return; - } + // Return early if the plugin check object cache already exists. + if ( defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) && WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) { + return; + } - // Create the object-cache.php file. - if ( $wp_filesystem || WP_Filesystem() ) { - // Do not replace the object-cache.php file if it already exists. - if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) { - $wp_filesystem->copy( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php', WP_CONTENT_DIR . '/object-cache.php' ); + // Create the object-cache.php file. + if ( $wp_filesystem || WP_Filesystem() ) { + // Do not replace the object-cache.php file if it already exists. + if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) { + $wp_filesystem->copy( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php', WP_CONTENT_DIR . '/object-cache.php' ); + } } + } finally { + /** + * Fires after the runtime environment is set up, including when it exits early. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all setup steps. + * } + */ + do_action( 'wp_plugin_check_after_runtime_setup', array( 'early_exit' => false ) ); } } @@ -105,38 +135,66 @@ static function () use ( $permalink_structure ) { public function clean_up() { global $wpdb, $wp_filesystem; - require_once ABSPATH . '/wp-admin/includes/upgrade.php'; + /** + * Fires before the runtime environment is cleaned up. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all cleanup steps. + * } + */ + do_action( 'wp_plugin_check_before_runtime_cleanup', array( 'early_exit' => false ) ); - $prefix_cleanup = $this->amend_db_base_prefix(); - $tables = $wpdb->tables(); + try { + require_once ABSPATH . '/wp-admin/includes/upgrade.php'; - $tables = $this->ignore_custom_tables( $tables ); + $prefix_cleanup = $this->amend_db_base_prefix(); + $tables = $wpdb->tables(); - foreach ( $tables as $table ) { - $wpdb->query( "DROP TABLE IF EXISTS `$table`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - } + $tables = $this->ignore_custom_tables( $tables ); - // Restore the old prefix. - $prefix_cleanup(); + foreach ( $tables as $table ) { + $wpdb->query( "DROP TABLE IF EXISTS `$table`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } - // Return early if the plugin check object cache does not exist. - if ( ! defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) || ! WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) { - return; - } + // Restore the old prefix. + $prefix_cleanup(); - // Remove the object-cache.php file. - if ( $wp_filesystem || WP_Filesystem() ) { - if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) { + // Return early if the plugin check object cache does not exist. + if ( ! defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) || ! WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) { return; } - // Check the drop-in file matches the copy. - $original_content = $wp_filesystem->get_contents( WP_CONTENT_DIR . '/object-cache.php' ); - $copy_content = $wp_filesystem->get_contents( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php' ); + // Remove the object-cache.php file. + if ( $wp_filesystem || WP_Filesystem() ) { + if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) { + return; + } + + // Check the drop-in file matches the copy. + $original_content = $wp_filesystem->get_contents( WP_CONTENT_DIR . '/object-cache.php' ); + $copy_content = $wp_filesystem->get_contents( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php' ); - if ( $original_content && $original_content === $copy_content ) { - $wp_filesystem->delete( WP_CONTENT_DIR . '/object-cache.php' ); + if ( $original_content && $original_content === $copy_content ) { + $wp_filesystem->delete( WP_CONTENT_DIR . '/object-cache.php' ); + } } + } finally { + /** + * Fires after the runtime environment is cleaned up, including when it exits early. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all cleanup steps. + * } + */ + do_action( 'wp_plugin_check_after_runtime_cleanup', array( 'early_exit' => false ) ); } } diff --git a/tests/behat/features/plugin-check-bootstrap-file.feature b/tests/behat/features/plugin-check-bootstrap-file.feature new file mode 100644 index 000000000..dbc1e37db --- /dev/null +++ b/tests/behat/features/plugin-check-bootstrap-file.feature @@ -0,0 +1,120 @@ +Feature: Test that WP_PLUGIN_CHECK_BOOTSTRAP_FILE is loaded in the WP-CLI path. + + Scenario: Bootstrap file is required when the constant points to an existing file + Given a WP install with the Plugin Check plugin + And a wp-content/pcp-bootstrap.php file: + """ + assertEquals( 1, $this->check_result->get_error_count() ); } + + public function test_check_result_filter_receives_data_result_and_is_error_flag() { + $captured = array(); + + add_filter( + 'wp_plugin_check_check_result', + static function ( $data, $result, $is_error ) use ( &$captured ) { + $captured[] = array( + 'data' => $data, + 'result' => $result, + 'is_error' => $is_error, + ); + + return $data; + }, + 10, + 3 + ); + + $this->check_result->add_message( + true, + 'Error message', + array( + 'code' => 'test_error', + 'file' => 'test-plugin/test-plugin.php', + 'line' => 22, + 'column' => 30, + ) + ); + + $this->assertCount( 1, $captured ); + $this->assertIsArray( $captured[0]['data'] ); + $this->assertSame( 'Error message', $captured[0]['data']['message'] ); + $this->assertSame( 'test_error', $captured[0]['data']['code'] ); + // File path is normalised before the filter fires. + $this->assertSame( 'test-plugin.php', $captured[0]['data']['file'] ); + $this->assertSame( 22, $captured[0]['data']['line'] ); + $this->assertSame( 30, $captured[0]['data']['column'] ); + $this->assertSame( $this->check_result, $captured[0]['result'] ); + $this->assertTrue( $captured[0]['is_error'] ); + } + + public function test_check_result_filter_suppresses_entry_when_returning_null() { + add_filter( + 'wp_plugin_check_check_result', + static function ( $data ) { + return ( 'noisy_warning' === ( $data['code'] ?? '' ) ) ? null : $data; + } + ); + + $this->check_result->add_message( + false, + 'Noise.', + array( + 'code' => 'noisy_warning', + 'file' => 'test-plugin/test-plugin.php', + ) + ); + $this->check_result->add_message( + false, + 'Real warning.', + array( + 'code' => 'real_warning', + 'file' => 'test-plugin/test-plugin.php', + ) + ); + + $this->assertEquals( 1, $this->check_result->get_warning_count() ); + $this->assertEquals( 0, $this->check_result->get_error_count() ); + + $warnings = $this->check_result->get_warnings(); + $entries = array(); + foreach ( $warnings as $file_entries ) { + foreach ( $file_entries as $line_entries ) { + foreach ( $line_entries as $column_entries ) { + foreach ( $column_entries as $entry ) { + $entries[] = $entry['code'] ?? ''; + } + } + } + } + $this->assertSame( array( 'real_warning' ), $entries ); + } + + public function test_check_result_filter_can_mutate_entry() { + add_filter( + 'wp_plugin_check_check_result', + static function ( $data ) { + if ( 'mutable_warning' === ( $data['code'] ?? '' ) ) { + $data['message'] = 'Edited by filter.'; + $data['severity'] = 9; + } + + return $data; + } + ); + + $this->check_result->add_message( + false, + 'Original.', + array( + 'code' => 'mutable_warning', + 'file' => 'test-plugin/test-plugin.php', + 'line' => 1, + 'column' => 1, + ) + ); + + $warnings = $this->check_result->get_warnings(); + $entry = $warnings['test-plugin.php'][1][1][0]; + + $this->assertSame( 'Edited by filter.', $entry['message'] ); + $this->assertSame( 9, $entry['severity'] ); + } } diff --git a/tests/phpunit/tests/Checker/Checks/Abstract_PHP_CodeSniffer_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/Abstract_PHP_CodeSniffer_Check_Tests.php new file mode 100644 index 000000000..00f76c4b4 --- /dev/null +++ b/tests/phpunit/tests/Checker/Checks/Abstract_PHP_CodeSniffer_Check_Tests.php @@ -0,0 +1,52 @@ + $args, + 'check' => $check, + 'result' => $result, + ); + + return $args; + }, + 10, + 3 + ); + + $check = new I18n_Usage_Check(); + $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-i18n-usage-without-errors/load.php' ); + $check_result = new Check_Result( $check_context ); + + $check->run( $check_result ); + + $this->assertCount( 1, $captured, 'Filter should fire exactly once per run().' ); + $this->assertIsArray( $captured[0]['args'] ); + $this->assertArrayHasKey( 'standard', $captured[0]['args'] ); + $this->assertInstanceOf( Abstract_PHP_CodeSniffer_Check::class, $captured[0]['check'] ); + $this->assertSame( $check, $captured[0]['check'] ); + $this->assertSame( $check_result, $captured[0]['result'] ); + } + + // End-to-end verification that filter mutations reach PHPCS is covered by + // the Behat scenario `plugin-check-phpcs-args-filter.feature` — it runs a + // real `plugin check` with a sniff added to the `exclude` argument via the + // filter and asserts the sniff's message disappears from STDOUT. +} diff --git a/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php b/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php index 6f78930fb..a45f4ecc0 100644 --- a/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php +++ b/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php @@ -92,6 +92,70 @@ public function test_can_set_up_with_failing_filesystem() { $this->assertFalse( $runtime_setup->can_set_up() ); } + public function test_set_up_fires_setup_environment_hooks() { + $this->set_up_mock_filesystem(); + + $calls = array(); + + add_action( + 'wp_plugin_check_before_runtime_setup', + static function ( $payload ) use ( &$calls ) { + $calls[] = array( 'before', $payload ); + } + ); + + add_action( + 'wp_plugin_check_after_runtime_setup', + static function ( $payload ) use ( &$calls ) { + $calls[] = array( 'after', $payload ); + } + ); + + $runtime_setup = new Runtime_Environment_Setup(); + $runtime_setup->set_up(); + + $this->assertCount( 2, $calls ); + $this->assertSame( 'before', $calls[0][0] ); + $this->assertSame( 'after', $calls[1][0] ); + // Hooks receive the runtime-setup payload array, not the instance. + $this->assertIsArray( $calls[0][1] ); + $this->assertIsArray( $calls[1][1] ); + $this->assertArrayHasKey( 'early_exit', $calls[0][1] ); + $this->assertArrayHasKey( 'early_exit', $calls[1][1] ); + } + + public function test_clean_up_fires_cleanup_environment_hooks() { + $this->set_up_mock_filesystem(); + + $calls = array(); + + add_action( + 'wp_plugin_check_before_runtime_cleanup', + static function ( $payload ) use ( &$calls ) { + $calls[] = array( 'before', $payload ); + } + ); + + add_action( + 'wp_plugin_check_after_runtime_cleanup', + static function ( $payload ) use ( &$calls ) { + $calls[] = array( 'after', $payload ); + } + ); + + $runtime_setup = new Runtime_Environment_Setup(); + $runtime_setup->clean_up(); + + $this->assertCount( 2, $calls ); + $this->assertSame( 'before', $calls[0][0] ); + $this->assertSame( 'after', $calls[1][0] ); + // Hooks receive the runtime-cleanup payload array, not the instance. + $this->assertIsArray( $calls[0][1] ); + $this->assertIsArray( $calls[1][1] ); + $this->assertArrayHasKey( 'early_exit', $calls[0][1] ); + $this->assertArrayHasKey( 'early_exit', $calls[1][1] ); + } + public function test_clean_up() { global $wp_filesystem, $wpdb, $table_prefix; @@ -108,4 +172,41 @@ public function test_clean_up() { $this->assertTrue( 0 <= strpos( $wpdb->last_query, $table_prefix . 'pc_' ) ); $this->assertFalse( $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ); } + + /** + * Ensures the after_setup_environment action fires when set_up() returns early. + * + * This test relies on the drop-in version constant being defined at this point in the run. + * `test_clean_up` defines it earlier in the same process, so set_up() takes the early-return + * branch guarded by `WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION`. The after_* action lives + * in a `finally` block and must still fire. + */ + public function test_set_up_fires_after_action_on_early_return() { + $this->set_up_mock_filesystem(); + + if ( ! defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) ) { + define( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION', 1 ); + } + + $this->assertTrue( + defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) && WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION, + 'Pre-condition: the drop-in version constant must be defined to force the early-return branch.' + ); + + $after_called = false; + add_action( + 'wp_plugin_check_after_runtime_setup', + static function () use ( &$after_called ) { + $after_called = true; + } + ); + + $runtime_setup = new Runtime_Environment_Setup(); + $runtime_setup->set_up(); + + $this->assertTrue( + $after_called, + 'wp_plugin_check_after_runtime_setup must fire even when set_up() returns early.' + ); + } }