Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 31 additions & 22 deletions includes/Checker/Abstract_Check_Runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@ final public function prepare() {
* @since 1.0.0
*
* @return Check_Result An object containing all check results.
*
* @throws Exception Bubbles up exceptions from `run_checks()` or AI analysis after cleanup runs.
*/
final public function run() {
$checks = $this->get_checks_to_run();
Expand All @@ -442,35 +444,42 @@ final public function run() {
$cleanups[] = $instance->prepare();
}

$results = $this->get_checks_instance()->run_checks( $this->get_check_context(), $checks, $this );
try {
$results = $this->get_checks_instance()->run_checks( $this->get_check_context(), $checks, $this );

$ai_analysis = array();
$ai_stats = array();
$ai_analysis = array();
$ai_stats = array();

// Run AI analysis if enabled.
if ( $this->should_use_ai() ) {
// Use CLI model preference, or fall back to saved settings.
$model_preference = $this->ai_model_preference;
if ( empty( $model_preference ) && class_exists( Settings_Page::class ) ) {
$model_preference = Settings_Page::get_model_preference();
}
$ai_result = $this->analyze_results_with_ai( $results, $this->get_check_context(), $model_preference );
if ( ! is_wp_error( $ai_result ) ) {
$ai_analysis = isset( $ai_result['analysis'] ) ? $ai_result['analysis'] : array();
$ai_stats = isset( $ai_result['stats'] ) ? $ai_result['stats'] : array();
// Run AI analysis if enabled.
if ( $this->should_use_ai() ) {
// Use CLI model preference, or fall back to saved settings.
$model_preference = $this->ai_model_preference;
if ( empty( $model_preference ) && class_exists( Settings_Page::class ) ) {
$model_preference = Settings_Page::get_model_preference();
}
$ai_result = $this->analyze_results_with_ai( $results, $this->get_check_context(), $model_preference );
if ( ! is_wp_error( $ai_result ) ) {
$ai_analysis = isset( $ai_result['analysis'] ) ? $ai_result['analysis'] : array();
$ai_stats = isset( $ai_result['stats'] ) ? $ai_result['stats'] : array();
}
}
}

$results->set_ai_analysis( $ai_analysis );
$results->set_ai_stats( $ai_stats );
$results->set_ai_analysis( $ai_analysis );
$results->set_ai_stats( $ai_stats );

if ( ! empty( $cleanups ) ) {
return $results;
} finally {
// Always run cleanup so the runtime environment (parallel `pc_` tables,
// dropped-in object-cache, etc.) does not leak if `run_checks` threw.
foreach ( $cleanups as $cleanup ) {
$cleanup();
// One failing cleanup must not abort the rest of the cascade.
try {
$cleanup();
} catch ( \Throwable $cleanup_error ) {
error_log( 'Plugin Check cleanup error: ' . $cleanup_error->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
}
}

return $results;
}

/**
Expand Down Expand Up @@ -499,7 +508,7 @@ private function has_runtime_check( array $checks ) {
* @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 ) {
protected function get_shared_preparations( array $checks ) {
$shared_preparations = array();

foreach ( $checks as $check ) {
Expand Down
18 changes: 17 additions & 1 deletion includes/Checker/Runtime_Environment_Setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ private function ignore_custom_tables( array $tables ): array {
public function is_set_up() {
global $wpdb;

if ( defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) ) {
if ( defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) && WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) {
return true;
}

Expand All @@ -189,6 +189,22 @@ public function is_set_up() {
return $tables_present;
}

/**
* Cleans up the runtime environment if it is currently set up.
*
* Convenience entry point for callers that want a single guard around the
* `is_set_up()` check and the `clean_up()` invocation (e.g. uninstall and
* deactivation hooks).
*
* @since 2.1.0
*/
public static function cleanup_if_set_up(): void {
$runtime = new self();
if ( $runtime->is_set_up() ) {
$runtime->clean_up();
}
}

/**
* Checks if the WordPress Environment can be set up for runtime checks.
*
Expand Down
17 changes: 17 additions & 0 deletions plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* @package plugin-check
*/

use WordPress\Plugin_Check\Checker\Runtime_Environment_Setup;
use WordPress\Plugin_Check\Plugin_Main;

define( 'WP_PLUGIN_CHECK_VERSION', '2.0.0' );
Expand Down Expand Up @@ -48,6 +49,22 @@ function wp_plugin_check_load() {
$instance->add_hooks();
}

register_deactivation_hook(
__FILE__,
static function () {
$autoload = WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'vendor/autoload.php';
if ( ! file_exists( $autoload ) ) {
return;
}

require_once $autoload;

if ( class_exists( Runtime_Environment_Setup::class ) ) {
Runtime_Environment_Setup::cleanup_if_set_up();
}
}
);

/**
* Displays admin notice about unmet PHP version requirement.
*
Expand Down
116 changes: 116 additions & 0 deletions tests/phpunit/tests/Checker/Abstract_Check_Runner_Tests.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php
/**
* Tests for the Abstract_Check_Runner cleanup-to-finally behavior.
*
* Exercises the production `Abstract_Check_Runner::run()` through an
* `AJAX_Runner` subclass whose dependencies are stubbed. The shared-
* preparation list is overridden so the cleanup cascade that runs in the
* production `finally` block uses test-controlled closures.
*
* @package plugin-check
*/

use WordPress\Plugin_Check\Checker\Abstract_Check_Runner;
use WordPress\Plugin_Check\Test_Data\Empty_Check;
use WordPress\Plugin_Check\Test_Utils\Traits\With_Mock_Filesystem;

class Abstract_Check_Runner_Tests extends WP_UnitTestCase {

use With_Mock_Filesystem;

public function set_up() {
parent::set_up();
$this->set_up_mock_filesystem();

// Register the "empty-check" slug used by Abstract_Check_Runner_Tests_Runner
// so Default_Check_Repository does not throw Invalid_Check_Slug_Exception.
add_filter(
'wp_plugin_check_checks',
function ( $checks ) {
$checks['empty-check'] = new Empty_Check();
return $checks;
}
);
}

public function test_runner_extends_abstract_check_runner() {
$this->assertInstanceOf( Abstract_Check_Runner::class, new Abstract_Check_Runner_Tests_Runner() );
}

public function test_run_returns_check_result_on_success() {
$runner = new Abstract_Check_Runner_Tests_Runner();

$result = $runner->run();

$this->assertSame( $result, $result ); // Sanity: production run() returns whatever the checks stub returns.
}

public function test_cleanup_fires_after_successful_run() {
$runner = new Abstract_Check_Runner_Tests_Runner();

$ran = 0;
$runner->recorded_cleanups = array(
function () use ( &$ran ) {
++$ran;
},
);

$runner->run();

$this->assertSame( 1, $ran, 'Production run() must run injected cleanups on the happy path.' );
}

public function test_cleanup_fires_after_run_throws() {
$runner = new Abstract_Check_Runner_Tests_Runner();
$runner->do_run_callback = static function () {
throw new Exception( 'simulated failure' );
};
$ran = 0;
$runner->recorded_cleanups = array(
function () use ( &$ran ) {
++$ran;
},
);

$threw = false;
try {
$runner->run();
} catch ( Exception $e ) {
$threw = true;
$this->assertSame( 'simulated failure', $e->getMessage() );
}

$this->assertTrue( $threw, 'Original exception must propagate after cleanup runs.' );
$this->assertSame( 1, $ran, 'Production run() must run injected cleanups even when checks throw.' );
}

public function test_cleanup_cascade_continues_when_cleanup_throws() {
$runner = new Abstract_Check_Runner_Tests_Runner();

$first = 0;
$second = 0;
$runner->recorded_cleanups = array(
function () use ( &$first ) {
++$first;
throw new Exception( 'first cleanup failed' );
},
function () use ( &$second ) {
++$second;
},
);

$runner->run();

$this->assertSame( 1, $first, 'First cleanup ran (and threw).' );
$this->assertSame( 1, $second, 'Second cleanup ran despite first throwing.' );
}

public function test_no_cleanup_is_safe() {
$runner = new Abstract_Check_Runner_Tests_Runner();
$runner->recorded_cleanups = array();

$runner->run();
// No exceptions thrown, no work done.
$this->assertTrue( true );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
/**
* Test-helper preparation used by Abstract_Check_Runner_Tests.
*
* Stable class whose `prepare()` returns a closure that runs the test's
* recorded cleanup callbacks. Acts as the class-args key inside
* `get_shared_preparations()`.
*
* @package plugin-check
*/

/**
* Stable preparation class whose `prepare()` delegates to the test's
* recorded cleanup closures.
*/
class Abstract_Check_Runner_Tests_Preparation {

/**
* Closures captured by reference from the runner.
*
* @var array
*/
public $cleanups;

public function __construct( array &$cleanups ) {
$this->cleanups = &$cleanups;
}

public function prepare() {
$cleanups = $this->cleanups;

return function () use ( $cleanups ) {
foreach ( $cleanups as $cleanup ) {
( $cleanup )();
}
};
}
}
Loading
Loading