diff --git a/includes/Checker/Abstract_Check_Runner.php b/includes/Checker/Abstract_Check_Runner.php new file mode 100644 index 000000000..d3d44c6ce --- /dev/null +++ b/includes/Checker/Abstract_Check_Runner.php @@ -0,0 +1,160 @@ +requires_universal_preparations( $this->get_checks_to_run() ) ) { + $preparation = new Universal_Runtime_Preparation( $this->get_checks_instance()->context() ); + return $preparation->prepare(); + } + + return function() {}; + } + + /** + * Runs the checks against the plugin. + * + * @since n.e.x.t + * + * @return Check_Result An object containing all check results. + */ + public function run() { + $checks = $this->get_checks_to_run(); + $preparations = $this->get_shared_preparations( $checks ); + $cleanups = array(); + + foreach ( $preparations as $preparation ) { + $instance = new $preparation['class']( ...$preparation['args'] ); + $cleanups[] = $instance->prepare(); + } + + $results = $this->get_checks_instance()->run_checks( $checks ); + + if ( ! empty( $cleanups ) ) { + foreach ( $cleanups as $cleanup ) { + $cleanup(); + } + } + + return $results; + } + + /** + * Determines if any of the checks requires the universal runtime preparation. + * + * @since n.e.x.t + * + * @param array $checks An array of check instances to run. + * @return bool Returns true if one or more checks requires the universal runtime preparation. + */ + protected function requires_universal_preparations( array $checks ) { + foreach ( $checks as $check ) { + if ( $check instanceof Runtime_Check ) { + return true; + } + } + + return false; + } + + /** + * Returns all shared preparations used by the checks to run. + * + * @since n.e.x.t + * + * @param array $checks An array of Check instances to run. + * @return array An array of Preparations to run where each item is an array with keys `class` and `args`. + */ + private function get_shared_preparations( array $checks ) { + $shared_preparations = array(); + + foreach ( $checks as $check ) { + if ( ! $check instanceof With_Shared_Preparations ) { + continue; + } + + $preparations = $check->get_shared_preparations(); + + foreach ( $preparations as $class => $args ) { + $key = $class . '::' . md5( json_encode( $args ) ); + + if ( ! isset( $shared_preparations[ $key ] ) ) { + $shared_preparations[ $key ] = array( + 'class' => $class, + 'args' => $args, + ); + } + } + } + + return array_values( $shared_preparations ); + } + + /** + * Returns the Check instances to run. + * + * @since n.e.x.t + * + * @return array An array map of check slugs to Check instances. + */ + protected function get_checks_to_run() { + $check_slugs = $this->get_check_slugs_to_run(); + $all_checks = $this->get_checks_instance()->get_checks(); + + if ( empty( $check_slugs ) ) { + return $all_checks; + } + + return array_intersect_key( $all_checks, array_flip( $check_slugs ) ); + } +} diff --git a/includes/Checker/CLI_Runner.php b/includes/Checker/CLI_Runner.php new file mode 100644 index 000000000..d240ebf2c --- /dev/null +++ b/includes/Checker/CLI_Runner.php @@ -0,0 +1,90 @@ + count( $_SERVER['argv'] ) ) { + return false; + } + + if ( + 'wp' === $_SERVER['argv'][0] && + 'plugin' === $_SERVER['argv'][1] && + 'check' === $_SERVER['argv'][2] + ) { + return true; + } + + return false; + } + + /** + * Retruns an instance of the Checks class. + * + * @since n.e.x.t + * + * @return Checks + * + * @throws Exception Thrown if the plugin main file cannot be found based on the CLI input. + */ + protected function get_checks_instance() { + if ( ! isset( $this->checks ) ) { + // Get the plugin name from the command line arguments. + $plugin_slug = isset( $_SERVER['argv'][3] ) ? $_SERVER['argv'][3] : ''; + $plugin_file = Plugin_Request_Utility::get_plugin_basename_from_input( $plugin_slug ); + + $this->checks = new Checks( WP_PLUGIN_DIR . '/' . $plugin_file ); + } + + return $this->checks; + } + + /** + * Returns an array of Check instances to run. + * + * @since n.e.x.t + * + * @return array An array of Check instances to run. + */ + protected function get_check_slugs_to_run() { + $checks = array(); + + foreach ( $_SERVER['argv'] as $value ) { + if ( false !== strpos( $value, '--checks=' ) ) { + $checks = explode( ',', str_replace( '--checks=', '', $value ) ); + break; + } + } + + return $checks; + } +} diff --git a/includes/Checker/Checks.php b/includes/Checker/Checks.php index 7219269df..75f112bb4 100644 --- a/includes/Checker/Checks.php +++ b/includes/Checker/Checks.php @@ -17,6 +17,14 @@ */ class Checks { + /** + * Array of all available Checks. + * + * @since n.e.x.t + * @var array + */ + protected $checks; + /** * Context for the plugin to check. * @@ -36,6 +44,17 @@ public function __construct( $plugin_main_file ) { $this->check_context = new Check_Context( $plugin_main_file ); } + /** + * Returns the Check Context. + * + * @since n.e.x.t + * + * @return Check_Context The plugin context that is being checked. + */ + public function context() { + return $this->check_context; + } + /** * Runs checks against the plugin. * @@ -105,19 +124,25 @@ protected function run_check_with_result( Check $check, Check_Result $result ) { * * @since n.e.x.t * - * @return array List of plugin check class instances implementing the Check interface. + * @return array An array map of check slugs to Check instances. */ public function get_checks() { - // TODO: Add checks once implemented. - $checks = array(); - - /** - * Filters the available plugin check classes. - * - * @since n.e.x.t - * - * @param array $checks List of plugin check class instances implementing the Check interface. - */ - return apply_filters( 'wp_plugin_check_checks', $checks ); + if ( ! isset( $this->checks ) ) { + // TODO: Add checks once implemented. + $checks = array( + 'i18n_usage' => new Checks\I18n_Usage_Check(), + ); + + /** + * Filters the available plugin check classes. + * + * @since n.e.x.t + * + * @param array $checks An array map of check slugs to Check instances. + */ + $this->checks = apply_filters( 'wp_plugin_check_checks', $checks ); + } + + return $this->checks; } } diff --git a/includes/Utilities/Plugin_Request_Utility.php b/includes/Utilities/Plugin_Request_Utility.php new file mode 100644 index 000000000..e06162ba4 --- /dev/null +++ b/includes/Utilities/Plugin_Request_Utility.php @@ -0,0 +1,63 @@ + $plugin_data ) { + if ( strpos( $plugin_basename, $plugin_slug . '/' ) === 0 ) { + return $plugin_basename; + } + } + + throw new Exception( + sprintf( + 'Invalid plugin slug: Plugin with slug %s is not installed.', + $plugin_slug + ) + ); + } +} diff --git a/tests/Checker/CLI_Runner_Tests.php b/tests/Checker/CLI_Runner_Tests.php new file mode 100644 index 000000000..278f3f211 --- /dev/null +++ b/tests/Checker/CLI_Runner_Tests.php @@ -0,0 +1,157 @@ +assertTrue( $runner->is_plugin_check() ); + } + + public function test_is_plugin_check_returns_false() { + $_SERVER['argv'] = array(); + + $runner = new CLI_Runner(); + + $this->assertFalse( $runner->is_plugin_check() ); + } + + public function test_prepare_with_runtime_check() { + $_SERVER['argv'] = array( + 'wp', + 'plugin', + 'check', + 'plugin-check', + '--checks=runtime-check', + ); + + add_filter( + 'wp_plugin_check_checks', + function( $checks ) { + return array( + 'runtime-check' => new WordPress\Plugin_Check\Test_Data\Runtime_Check(), + ); + } + ); + + $runner = new CLI_Runner(); + $cleanup = $runner->prepare(); + + $this->assertIsCallable( $cleanup ); + + // Assert the Universal_Runtume_Preparation was run. + $this->assertTrue( has_filter( 'option_active_plugins' ) ); + $this->assertTrue( has_filter( 'default_option_active_plugins' ) ); + $this->assertTrue( has_filter( 'stylesheet' ) ); + $this->assertTrue( has_filter( 'template' ) ); + $this->assertTrue( has_filter( 'pre_option_template' ) ); + $this->assertTrue( has_filter( 'pre_option_stylesheet' ) ); + $this->assertTrue( has_filter( 'pre_option_current_theme' ) ); + $this->assertTrue( has_filter( 'pre_option_template_root' ) ); + $this->assertTrue( has_filter( 'pre_option_stylesheet_root' ) ); + } + + public function test_prepare_with_static_check() { + $_SERVER['argv'] = array( + 'wp', + 'plugin', + 'check', + 'plugin-check', + '--checks=empty-check', + ); + + add_filter( + 'wp_plugin_check_checks', + function( $checks ) { + return array( + 'empty-check' => new WordPress\Plugin_Check\Test_Data\Empty_Check(), + ); + } + ); + + $runner = new CLI_Runner(); + $cleanup = $runner->prepare(); + + $this->assertIsCallable( $cleanup ); + + // Assert the Universal_Runtume_Preparation was not run. + $this->assertFalse( has_filter( 'option_active_plugins' ) ); + $this->assertFalse( has_filter( 'default_option_active_plugins' ) ); + $this->assertFalse( has_filter( 'stylesheet' ) ); + $this->assertFalse( has_filter( 'template' ) ); + $this->assertFalse( has_filter( 'pre_option_template' ) ); + $this->assertFalse( has_filter( 'pre_option_stylesheet' ) ); + $this->assertFalse( has_filter( 'pre_option_current_theme' ) ); + $this->assertFalse( has_filter( 'pre_option_template_root' ) ); + $this->assertFalse( has_filter( 'pre_option_stylesheet_root' ) ); + } + + public function test_run() { + $_SERVER['argv'] = array( + 'wp', + 'plugin', + 'check', + 'plugin-check', + '--checks=empty-check', + ); + + add_filter( + 'wp_plugin_check_checks', + function( $checks ) { + return array( + 'empty-check' => new WordPress\Plugin_Check\Test_Data\Empty_Check(), + ); + } + ); + + $runner = new CLI_Runner(); + $runner->prepare(); + $results = $runner->run(); + + $this->assertInstanceOf( Check_Result::class, $results ); + $this->assertEmpty( $results->get_warnings() ); + $this->assertEmpty( $results->get_errors() ); + } + + public function test_run_with_errors() { + $_SERVER['argv'] = array( + 'wp', + 'plugin', + 'check', + 'plugin-check', + '--checks=error-check', + ); + + add_filter( + 'wp_plugin_check_checks', + function( $checks ) { + return array( + 'error-check' => new WordPress\Plugin_Check\Test_Data\Error_Check(), + ); + } + ); + + $runner = new CLI_Runner(); + $runner->prepare(); + $results = $runner->run(); + + $this->assertInstanceOf( Check_Result::class, $results ); + $this->assertEmpty( $results->get_warnings() ); + $this->assertNotEmpty( $results->get_errors() ); + } +} diff --git a/tests/Utilities/Plugin_Request_Utility_Tests.php b/tests/Utilities/Plugin_Request_Utility_Tests.php new file mode 100644 index 000000000..47b677f57 --- /dev/null +++ b/tests/Utilities/Plugin_Request_Utility_Tests.php @@ -0,0 +1,31 @@ +assertSame( plugin_basename( WP_PLUGIN_CHECK_MAIN_FILE ), $plugin ); + } + + public function test_get_plugin_basename_from_input_with_empty_input() { + $this->expectException( 'Exception' ); + $this->expectExceptionMessage( 'Invalid plugin slug: Plugin slug must not be empty.' ); + + Plugin_Request_Utility::get_plugin_basename_from_input( '' ); + } + + public function test_get_plugin_basename_from_input_with_invalid_input() { + $this->expectException( 'Exception' ); + $this->expectExceptionMessage( 'Invalid plugin slug: Plugin with slug invalid is not installed.' ); + + Plugin_Request_Utility::get_plugin_basename_from_input( 'invalid' ); + } +} diff --git a/tests/testdata/Checks/Runtime_Check.php b/tests/testdata/Checks/Runtime_Check.php new file mode 100644 index 000000000..95c22b039 --- /dev/null +++ b/tests/testdata/Checks/Runtime_Check.php @@ -0,0 +1,12 @@ +