Squashed 'libraries/action-scheduler/' content from commit a95f351

git-subtree-dir: libraries/action-scheduler
git-subtree-split: a95f351058eada5e5281faa22e5a40865542e839
This commit is contained in:
2026-03-16 13:15:04 +01:00
commit d435f549fe
174 changed files with 32087 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
<?php
namespace Action_Scheduler\Tests\DataStores;
use ActionScheduler_Action;
use ActionScheduler_Callbacks;
use ActionScheduler_IntervalSchedule;
use ActionScheduler_Mocker;
use ActionScheduler_SimpleSchedule;
use ActionScheduler_Store;
use ActionScheduler_UnitTestCase;
use InvalidArgumentException;
/**
* Abstract store test class.
*
* Many tests for the WP Post store or the custom tables store can be shared. This abstract class contains tests that
* apply to both stores without having to duplicate code.
*/
abstract class AbstractStoreTest extends ActionScheduler_UnitTestCase {
/**
* Get data store for tests.
*
* @return ActionScheduler_Store
*/
abstract protected function get_store();
public function test_get_status() {
$time = as_get_datetime_object( '-10 minutes' );
$schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
$store = $this->get_store();
$action_id = $store->save_action( $action );
$this->assertEquals( ActionScheduler_Store::STATUS_PENDING, $store->get_status( $action_id ) );
$store->mark_complete( $action_id );
$this->assertEquals( ActionScheduler_Store::STATUS_COMPLETE, $store->get_status( $action_id ) );
$store->mark_failure( $action_id );
$this->assertEquals( ActionScheduler_Store::STATUS_FAILED, $store->get_status( $action_id ) );
}
// Start tests for \ActionScheduler_Store::query_actions().
// phpcs:ignore Squiz.Commenting.FunctionComment.WrongStyle
public function test_query_actions_query_type_arg_invalid_option() {
$this->expectException( InvalidArgumentException::class );
$this->get_store()->query_actions( array( 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ), 'invalid' );
}
public function test_query_actions_query_type_arg_valid_options() {
$store = $this->get_store();
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) );
$action_id_1 = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule ) );
$action_id_2 = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule ) );
$this->assertEquals( array( $action_id_1, $action_id_2 ), $store->query_actions( array( 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ) ) );
$this->assertEquals( 2, $store->query_actions( array( 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ), 'count' ) );
}
public function test_query_actions_by_single_status() {
$store = $this->get_store();
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) );
$this->assertEquals( 0, $store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_PENDING ), 'count' ) );
$action_id_1 = $store->save_action( new ActionScheduler_Action( 'my_hook_1', array( 1 ), $schedule ) );
$action_id_2 = $store->save_action( new ActionScheduler_Action( 'my_hook_2', array( 1 ), $schedule ) );
$action_id_3 = $store->save_action( new ActionScheduler_Action( 'my_hook_3', array( 1 ), $schedule ) );
$store->mark_complete( $action_id_3 );
$this->assertEquals( 2, $store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_PENDING ), 'count' ) );
$this->assertEquals( 1, $store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_COMPLETE ), 'count' ) );
}
public function test_query_actions_by_array_status() {
$store = $this->get_store();
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) );
$this->assertEquals(
0,
$store->query_actions(
array(
'status' => array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_RUNNING ),
),
'count'
)
);
$action_id_1 = $store->save_action( new ActionScheduler_Action( 'my_hook_1', array( 1 ), $schedule ) );
$action_id_2 = $store->save_action( new ActionScheduler_Action( 'my_hook_2', array( 1 ), $schedule ) );
$action_id_3 = $store->save_action( new ActionScheduler_Action( 'my_hook_3', array( 1 ), $schedule ) );
$store->mark_failure( $action_id_3 );
$this->assertEquals(
3,
$store->query_actions(
array(
'status' => array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_FAILED ),
),
'count'
)
);
$this->assertEquals(
2,
$store->query_actions(
array(
'status' => array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_COMPLETE ),
),
'count'
)
);
}
// phpcs:ignore Squiz.PHP.CommentedOutCode.Found
// End tests for \ActionScheduler_Store::query_actions().
/**
* The `has_pending_actions_due` method should return a boolean value depending on whether there are
* pending actions.
*
* @return void
*/
public function test_has_pending_actions_due() {
$store = $this->get_store();
$runner = ActionScheduler_Mocker::get_queue_runner( $store );
for ( $i = - 3; $i <= 3; $i ++ ) {
// Some past actions, some future actions.
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
$store->save_action( $action );
}
$this->assertTrue( $store->has_pending_actions_due() );
$runner->run();
$this->assertFalse( $store->has_pending_actions_due() );
}
/**
* The `has_pending_actions_due` method should return false when all pending actions are in the future.
*
* @return void
*/
public function test_has_pending_actions_due_only_future_actions() {
$store = $this->get_store();
for ( $i = 1; $i <= 3; $i ++ ) {
// Only future actions.
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
$store->save_action( $action );
}
$this->assertFalse( $store->has_pending_actions_due() );
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Class ActionScheduler_DBStoreMigrator_Test
* @group tables
*/
class ActionScheduler_DBStoreMigrator_Test extends ActionScheduler_UnitTestCase {
public function test_create_action_with_last_attempt_date() {
$scheduled_date = as_get_datetime_object( strtotime( '-24 hours' ) );
$last_attempt_date = as_get_datetime_object( strtotime( '-23 hours' ) );
$action = new ActionScheduler_FinishedAction( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), new ActionScheduler_SimpleSchedule( $scheduled_date ) );
$store = new ActionScheduler_DBStoreMigrator();
$action_id = $store->save_action( $action, null, $last_attempt_date );
$action_date = $store->get_date( $action_id );
$this->assertEquals( $last_attempt_date->format( 'U' ), $action_date->format( 'U' ) );
$action_id = $store->save_action( $action, $scheduled_date, $last_attempt_date );
$action_date = $store->get_date( $action_id );
$this->assertEquals( $last_attempt_date->format( 'U' ), $action_date->format( 'U' ) );
}
}

View File

@@ -0,0 +1,814 @@
<?php
use Action_Scheduler\Tests\DataStores\AbstractStoreTest;
/**
* Class ActionScheduler_DBStore_Test
* @group tables
*/
class ActionScheduler_DBStore_Test extends AbstractStoreTest {
/**
* Saved instance of wpdb to restore in cases where we replace the global instance with a mock.
*
* @see $this->test_db_supports_skip_locked()
*
* @var \wpdb
*/
private $original_wpdb;
public function setUp(): void {
global $wpdb;
// Delete all actions before each test.
$wpdb->query( "DELETE FROM {$wpdb->actionscheduler_actions}" );
parent::setUp();
}
/**
* Restore the original wpdb global instance for tests that have replaced it.
*
* @return void
*/
public function tearDown(): void {
global $wpdb;
if ( null !== $this->original_wpdb ) {
$wpdb = $this->original_wpdb;
$this->original_wpdb = null;
}
parent::tearDown();
}
/**
* Get data store for tests.
*
* @return ActionScheduler_DBStore
*/
protected function get_store() {
return new ActionScheduler_DBStore();
}
public function test_create_action() {
$time = as_get_datetime_object();
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
$store = new ActionScheduler_DBStore();
$action_id = $store->save_action( $action );
$this->assertNotEmpty( $action_id );
}
public function test_create_action_with_scheduled_date() {
$time = as_get_datetime_object( strtotime( '-1 week' ) );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), new ActionScheduler_SimpleSchedule( $time ) );
$store = new ActionScheduler_DBStore();
$action_id = $store->save_action( $action, $time );
$action_date = $store->get_date( $action_id );
$this->assertEquals( $time->format( 'U' ), $action_date->format( 'U' ) );
}
public function test_retrieve_action() {
$time = as_get_datetime_object();
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' );
$store = new ActionScheduler_DBStore();
$action_id = $store->save_action( $action );
$retrieved = $store->fetch_action( $action_id );
$this->assertEquals( $action->get_hook(), $retrieved->get_hook() );
$this->assertEqualSets( $action->get_args(), $retrieved->get_args() );
$this->assertEquals( $action->get_schedule()->get_date()->format( 'U' ), $retrieved->get_schedule()->get_date()->format( 'U' ) );
$this->assertEquals( $action->get_group(), $retrieved->get_group() );
}
public function test_cancel_action() {
$time = as_get_datetime_object();
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' );
$store = new ActionScheduler_DBStore();
$action_id = $store->save_action( $action );
$store->cancel_action( $action_id );
$fetched = $store->fetch_action( $action_id );
$this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched );
}
public function test_cancel_actions_by_hook() {
$store = new ActionScheduler_DBStore();
$actions = array();
$hook = 'by_hook_test';
for ( $day = 1; $day <= 3; $day++ ) {
$delta = sprintf( '+%d day', $day );
$time = as_get_datetime_object( $delta );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( $hook, array(), $schedule, 'my_group' );
$actions[] = $store->save_action( $action );
}
$store->cancel_actions_by_hook( $hook );
foreach ( $actions as $action_id ) {
$fetched = $store->fetch_action( $action_id );
$this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched );
}
}
public function test_cancel_actions_by_group() {
$store = new ActionScheduler_DBStore();
$actions = array();
$group = 'by_group_test';
for ( $day = 1; $day <= 3; $day++ ) {
$delta = sprintf( '+%d day', $day );
$time = as_get_datetime_object( $delta );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, $group );
$actions[] = $store->save_action( $action );
}
$store->cancel_actions_by_group( $group );
foreach ( $actions as $action_id ) {
$fetched = $store->fetch_action( $action_id );
$this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched );
}
}
public function test_claim_actions() {
$created_actions = array();
$store = new ActionScheduler_DBStore();
for ( $i = 3; $i > - 3; $i -- ) {
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
$created_actions[] = $store->save_action( $action );
}
$claim = $store->stake_claim();
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 3, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions, 3, 3 ), $claim->get_actions() );
}
public function test_claim_actions_order() {
$store = new ActionScheduler_DBStore();
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
$created_actions = array(
$store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'my_group' ) ),
$store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'my_group' ) ),
);
$claim = $store->stake_claim();
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
// Verify uniqueness of action IDs.
$this->assertEquals( 2, count( array_unique( $created_actions ) ) );
// Verify the count and order of the actions.
$claimed_actions = $claim->get_actions();
$this->assertCount( 2, $claimed_actions );
$this->assertEquals( $created_actions, $claimed_actions );
// Verify the reversed order doesn't pass.
$reversed_actions = array_reverse( $created_actions );
$this->assertNotEquals( $reversed_actions, $claimed_actions );
}
public function test_claim_actions_by_hooks() {
$created_actions_by_hook = array();
$created_actions = array();
$store = new ActionScheduler_DBStore();
$unique_hook_one = 'my_unique_hook_one';
$unique_hook_two = 'my_unique_hook_two';
$unique_hooks = array(
$unique_hook_one,
$unique_hook_two,
);
for ( $i = 3; $i > - 3; $i -- ) {
foreach ( $unique_hooks as $unique_hook ) {
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( $unique_hook, array( $i ), $schedule, 'my_group' );
$action_id = $store->save_action( $action );
$created_actions[] = $action_id;
$created_actions_by_hook[ $unique_hook ][] = $action_id;
}
}
$claim = $store->stake_claim( 10, null, $unique_hooks );
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 6, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions, 6, 6 ), $claim->get_actions() );
$store->release_claim( $claim );
$claim = $store->stake_claim( 10, null, array( $unique_hook_one ) );
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 3, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_one ], 3, 3 ), $claim->get_actions() );
$store->release_claim( $claim );
$claim = $store->stake_claim( 10, null, array( $unique_hook_two ) );
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 3, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_two ], 3, 3 ), $claim->get_actions() );
}
public function test_claim_actions_by_group() {
$created_actions = array();
$store = new ActionScheduler_DBStore();
$unique_group_one = 'my_unique_group_one';
$unique_group_two = 'my_unique_group_two';
$unique_groups = array(
$unique_group_one,
$unique_group_two,
);
for ( $i = 3; $i > - 3; $i -- ) {
foreach ( $unique_groups as $unique_group ) {
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, $unique_group );
$created_actions[ $unique_group ][] = $store->save_action( $action );
}
}
$claim = $store->stake_claim( 10, null, array(), $unique_group_one );
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 3, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions[ $unique_group_one ], 3, 3 ), $claim->get_actions() );
$store->release_claim( $claim );
$claim = $store->stake_claim( 10, null, array(), $unique_group_two );
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 3, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions[ $unique_group_two ], 3, 3 ), $claim->get_actions() );
}
/**
* The DBStore allows one or more groups to be excluded from a claim.
*/
public function test_claim_actions_with_group_exclusions() {
$created_actions = array();
$store = new ActionScheduler_DBStore();
$groups = array( 'foo', 'bar', 'baz' );
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
// Create 6 actions (with 2 in each test group).
foreach ( $groups as $group_slug ) {
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, $group_slug );
$created_actions[ $group_slug ] = array(
$store->save_action( $action ),
$store->save_action( $action ),
);
}
// If we exclude group 'foo' (representing 2 actions) the remaining 4 actions from groups 'bar' and 'baz' should still be claimed.
$store->set_claim_filter( 'exclude-groups', 'foo' );
$claim = $store->stake_claim();
$this->assertEquals(
array_merge( $created_actions['bar'], $created_actions['baz'] ),
$claim->get_actions(),
'A single group can successfully be excluded from claims.'
);
$store->release_claim( $claim );
// If we exclude groups 'bar' and 'baz' (representing 4 actions) the remaining 2 actions from group 'foo' should still be claimed.
$store->set_claim_filter( 'exclude-groups', array( 'bar', 'baz' ) );
$claim = $store->stake_claim();
$this->assertEquals(
$created_actions['foo'],
$claim->get_actions(),
'Multiple groups can successfully be excluded from claims.'
);
$store->release_claim( $claim );
// If we include group 'foo' (representing 2 actions) after excluding all groups, the inclusion should 'win'.
$store->set_claim_filter( 'exclude-groups', array( 'foo', 'bar', 'baz' ) );
$claim = $store->stake_claim( 10, null, array(), 'foo' );
$this->assertEquals(
$created_actions['foo'],
$claim->get_actions(),
'Including a specific group takes precedence over group exclusions.'
);
$store->release_claim( $claim );
}
public function test_claim_actions_by_hook_and_group() {
$created_actions_by_hook = array();
$created_actions = array();
$store = new ActionScheduler_DBStore();
$unique_hook_one = 'my_other_unique_hook_one';
$unique_hook_two = 'my_other_unique_hook_two';
$unique_hooks = array(
$unique_hook_one,
$unique_hook_two,
);
$unique_group_one = 'my_other_other_unique_group_one';
$unique_group_two = 'my_other_unique_group_two';
$unique_groups = array(
$unique_group_one,
$unique_group_two,
);
for ( $i = 3; $i > - 3; $i -- ) {
foreach ( $unique_hooks as $unique_hook ) {
foreach ( $unique_groups as $unique_group ) {
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( $unique_hook, array( $i ), $schedule, $unique_group );
$action_id = $store->save_action( $action );
$created_actions[ $unique_group ][] = $action_id;
$created_actions_by_hook[ $unique_hook ][ $unique_group ][] = $action_id;
}
}
}
/** Test Both Hooks with Each Group */
$claim = $store->stake_claim( 10, null, $unique_hooks, $unique_group_one );
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 6, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions[ $unique_group_one ], 6, 6 ), $claim->get_actions() );
$store->release_claim( $claim );
$claim = $store->stake_claim( 10, null, $unique_hooks, $unique_group_two );
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 6, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions[ $unique_group_two ], 6, 6 ), $claim->get_actions() );
$store->release_claim( $claim );
/** Test Just One Hook with Group One */
$claim = $store->stake_claim( 10, null, array( $unique_hook_one ), $unique_group_one );
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 3, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_one ][ $unique_group_one ], 3, 3 ), $claim->get_actions() );
$store->release_claim( $claim );
$claim = $store->stake_claim( 24, null, array( $unique_hook_two ), $unique_group_one );
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 3, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_two ][ $unique_group_one ], 3, 3 ), $claim->get_actions() );
$store->release_claim( $claim );
/** Test Just One Hook with Group Two */
$claim = $store->stake_claim( 10, null, array( $unique_hook_one ), $unique_group_two );
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 3, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_one ][ $unique_group_two ], 3, 3 ), $claim->get_actions() );
$store->release_claim( $claim );
$claim = $store->stake_claim( 24, null, array( $unique_hook_two ), $unique_group_two );
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 3, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_two ][ $unique_group_two ], 3, 3 ), $claim->get_actions() );
}
/**
* Confirm that priorities are respected when claiming actions.
*
* @return void
*/
public function test_claim_actions_respecting_priority() {
$store = new ActionScheduler_DBStore();
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-2 hours' ) );
$routine_action_1 = $store->save_action( new ActionScheduler_Action( 'routine_past_due', array(), $schedule, '' ) );
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
$action = new ActionScheduler_Action( 'high_priority_past_due', array(), $schedule, '' );
$action->set_priority( 5 );
$priority_action = $store->save_action( $action );
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-4 hours' ) );
$routine_action_2 = $store->save_action( new ActionScheduler_Action( 'routine_past_due', array(), $schedule, '' ) );
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '+1 hour' ) );
$action = new ActionScheduler_Action( 'high_priority_future', array(), $schedule, '' );
$action->set_priority( 2 );
$priority_future_action = $store->save_action( $action );
$claim = $store->stake_claim();
$this->assertEquals(
array( $priority_action, $routine_action_2, $routine_action_1 ),
$claim->get_actions(),
'High priority actions take precedence over older but lower priority actions.'
);
}
/**
* The query used to claim actions explicitly ignores future pending actions, but it
* is still possible under unusual conditions (such as if MySQL runs out of temporary
* storage space) for such actions to be returned.
*
* When this happens, we still expect the store to filter them out, otherwise there is
* a risk that actions will be unexpectedly processed ahead of time.
*
* @see https://github.com/woocommerce/action-scheduler/issues/634
*/
public function test_claim_filters_out_unexpected_future_actions() {
$group = __METHOD__;
$store = new ActionScheduler_DBStore();
// Create 4 actions: 2 that are already due (-3hrs and -1hrs) and 2 that are not yet due (+1hr and +3hrs).
for ( $i = -3; $i <= 3; $i += 2 ) {
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( $i . ' hours' ) );
$action_ids[] = $store->save_action( new ActionScheduler_Action( 'test_' . $i, array(), $schedule, $group ) );
}
// This callback is used to simulate the unusual conditions whereby MySQL might unexpectedly return future
// actions, contrary to the conditions used by the store object when staking its claim.
$simulate_unexpected_db_behavior = function( $sql ) use ( $action_ids ) {
global $wpdb;
// Look out for the claim update query, ignore all others.
if (
0 !== strpos( $sql, "UPDATE $wpdb->actionscheduler_actions" )
|| ! preg_match( "/claim_id = 0 AND scheduled_date_gmt <= '([0-9:\-\s]{19})'/", $sql, $matches )
|| count( $matches ) !== 2
) {
return $sql;
}
// Now modify the query, forcing it to also return the future actions we created.
return str_replace( $matches[1], as_get_datetime_object( '+4 hours' )->format( 'Y-m-d H:i:s' ), $sql );
};
add_filter( 'query', $simulate_unexpected_db_behavior );
$claim = $store->stake_claim( 10, null, array(), $group );
$claimed_actions = $claim->get_actions();
$this->assertCount( 2, $claimed_actions );
// Cleanup.
remove_filter( 'query', $simulate_unexpected_db_behavior );
$store->release_claim( $claim );
}
public function test_duplicate_claim() {
$created_actions = array();
$store = new ActionScheduler_DBStore();
for ( $i = 0; $i > - 3; $i -- ) {
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
$created_actions[] = $store->save_action( $action );
}
$claim1 = $store->stake_claim();
$claim2 = $store->stake_claim();
$this->assertCount( 3, $claim1->get_actions() );
$this->assertCount( 0, $claim2->get_actions() );
}
public function test_release_claim() {
$created_actions = array();
$store = new ActionScheduler_DBStore();
for ( $i = 0; $i > - 3; $i -- ) {
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
$created_actions[] = $store->save_action( $action );
}
$claim1 = $store->stake_claim();
$store->release_claim( $claim1 );
$this->assertCount( 0, $store->find_actions_by_claim_id( $claim1->get_id() ) );
$claim2 = $store->stake_claim();
$this->assertCount( 3, $claim2->get_actions() );
$store->release_claim( $claim2 );
$this->assertCount( 0, $store->find_actions_by_claim_id( $claim1->get_id() ) );
}
public function test_search() {
$created_actions = array();
$store = new ActionScheduler_DBStore();
for ( $i = - 3; $i <= 3; $i ++ ) {
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
$created_actions[] = $store->save_action( $action );
}
$next_no_args = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK );
$this->assertEquals( $created_actions[0], $next_no_args );
$next_with_args = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'args' => array( 1 ) ) );
$this->assertEquals( $created_actions[4], $next_with_args );
$non_existent = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'args' => array( 17 ) ) );
$this->assertNull( $non_existent );
}
public function test_search_by_group() {
$store = new ActionScheduler_DBStore();
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) );
$abc = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'abc' ) );
$def = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'def' ) );
$ghi = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'ghi' ) );
$this->assertEquals( $abc, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'abc' ) ) );
$this->assertEquals( $def, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'def' ) ) );
$this->assertEquals( $ghi, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'ghi' ) ) );
}
public function test_get_run_date() {
$time = as_get_datetime_object( '-10 minutes' );
$schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
$store = new ActionScheduler_DBStore();
$action_id = $store->save_action( $action );
$this->assertEquals( $time->format( 'U' ), $store->get_date( $action_id )->format( 'U' ) );
$action = $store->fetch_action( $action_id );
$action->execute();
$now = as_get_datetime_object();
$store->mark_complete( $action_id );
$this->assertEquals( $now->format( 'U' ), $store->get_date( $action_id )->format( 'U' ) );
$next = $action->get_schedule()->get_next( $now );
$new_action_id = $store->save_action( $action, $next );
$this->assertEquals( (int) ( $now->format( 'U' ) ) + HOUR_IN_SECONDS, $store->get_date( $new_action_id )->format( 'U' ) );
}
/**
* Test creating a unique action.
*/
public function test_create_action_unique() {
$time = as_get_datetime_object();
$hook = md5( wp_rand() );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$store = new ActionScheduler_DBStore();
$action = new ActionScheduler_Action( $hook, array(), $schedule );
$action_id = $store->save_action( $action );
$this->assertNotEquals( 0, $action_id );
$action_from_db = $store->fetch_action( $action_id );
$this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) );
$action = new ActionScheduler_Action( $hook, array(), $schedule );
$action_id_duplicate = $store->save_unique_action( $action );
$this->assertEquals( 0, $action_id_duplicate );
}
/**
* Test saving unique actions across different groups. Different groups should be saved, same groups shouldn't.
*/
public function test_create_action_unique_with_different_groups() {
$time = as_get_datetime_object();
$hook = md5( wp_rand() );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$store = new ActionScheduler_DBStore();
$action = new ActionScheduler_Action( $hook, array(), $schedule, 'group1' );
$action_id = $store->save_action( $action );
$action_from_db = $store->fetch_action( $action_id );
$this->assertNotEquals( 0, $action_id );
$this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) );
$action2 = new ActionScheduler_Action( $hook, array(), $schedule, 'group2' );
$action_id_group2 = $store->save_unique_action( $action2 );
$this->assertNotEquals( 0, $action_id_group2 );
$action_2_from_db = $store->fetch_action( $action_id_group2 );
$this->assertTrue( is_a( $action_2_from_db, ActionScheduler_Action::class ) );
$action3 = new ActionScheduler_Action( $hook, array(), $schedule, 'group2' );
$action_id_group2_double = $store->save_unique_action( $action3 );
$this->assertEquals( 0, $action_id_group2_double );
}
/**
* Test saving a unique action first, and then successfully scheduling a non-unique action.
*/
public function test_create_action_unique_and_then_non_unique() {
$time = as_get_datetime_object();
$hook = md5( wp_rand() );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$store = new ActionScheduler_DBStore();
$action = new ActionScheduler_Action( $hook, array(), $schedule );
$action_id = $store->save_unique_action( $action );
$this->assertNotEquals( 0, $action_id );
$action_from_db = $store->fetch_action( $action_id );
$this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) );
// Non unique action is scheduled even if the previous one was unique.
$action = new ActionScheduler_Action( $hook, array(), $schedule );
$action_id_duplicate = $store->save_action( $action );
$this->assertNotEquals( 0, $action_id_duplicate );
$action_from_db_duplicate = $store->fetch_action( $action_id_duplicate );
$this->assertTrue( is_a( $action_from_db_duplicate, ActionScheduler_Action::class ) );
}
/**
* Test asserting that action when an action is created with empty args, it matches with actions created with args for uniqueness.
*/
public function test_create_action_unique_with_empty_array() {
$time = as_get_datetime_object();
$hook = md5( wp_rand() );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$store = new ActionScheduler_DBStore();
$action = new ActionScheduler_Action( $hook, array( 'foo' => 'bar' ), $schedule );
$action_id = $store->save_unique_action( $action );
$this->assertNotEquals( 0, $action_id );
$action_from_db = $store->fetch_action( $action_id );
$this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) );
$action_with_empty_args = new ActionScheduler_Action( $hook, array(), $schedule );
$action_id_duplicate = $store->save_unique_action( $action_with_empty_args );
$this->assertEquals( 0, $action_id_duplicate );
}
/**
* Uniqueness does not check for args, so actions with different args can't be scheduled when unique is true.
*/
public function test_create_action_unique_with_different_args_still_fail() {
$time = as_get_datetime_object();
$hook = md5( wp_rand() );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$store = new ActionScheduler_DBStore();
$action = new ActionScheduler_Action( $hook, array( 'foo' => 'bar' ), $schedule );
$action_id = $store->save_unique_action( $action );
$this->assertNotEquals( 0, $action_id );
$action_from_db = $store->fetch_action( $action_id );
$this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) );
$action_with_diff_args = new ActionScheduler_Action( $hook, array( 'foo' => 'bazz' ), $schedule );
$action_id_duplicate = $store->save_unique_action( $action_with_diff_args );
$this->assertEquals( 0, $action_id_duplicate );
}
/**
* When a set of claimed actions are processed, they should be executed in the expected order (by priority,
* then by least number of attempts, then by scheduled date, then finally by action ID).
*
* @return void
*/
public function test_actions_are_processed_in_correct_order() {
global $wpdb;
$now = time();
$actual_order = array();
// When `foo` actions are processed, record the sequence number they supply.
$watcher = function ( $number ) use ( &$actual_order ) {
$actual_order[] = $number;
};
as_schedule_single_action( $now - 10, 'foo', array( 4 ), '', false, 10 );
as_schedule_single_action( $now - 20, 'foo', array( 3 ), '', false, 10 );
as_schedule_single_action( $now - 5, 'foo', array( 2 ), '', false, 5 );
as_schedule_single_action( $now - 20, 'foo', array( 1 ), '', false, 5 );
$reattempted = as_schedule_single_action( $now - 40, 'foo', array( 7 ), '', false, 20 );
as_schedule_single_action( $now - 40, 'foo', array( 5 ), '', false, 20 );
as_schedule_single_action( $now - 40, 'foo', array( 6 ), '', false, 20 );
// Modify the `attempt` count on one of our test actions, to change expectations about its execution order.
$wpdb->update(
$wpdb->actionscheduler_actions,
array( 'attempts' => 5 ),
array( 'action_id' => $reattempted )
);
add_action( 'foo', $watcher );
ActionScheduler_Mocker::get_queue_runner( ActionScheduler::store() )->run();
remove_action( 'foo', $watcher );
$this->assertEquals( range( 1, 7 ), $actual_order, 'When a claim is processed, individual actions execute in the expected order.' );
}
/**
* When a set of claimed actions are processed, they should be executed in the expected order (by priority,
* then by least number of attempts, then by scheduled date, then finally by action ID). This should be true
* even if actions are scheduled from within other scheduled actions.
*
* This test is a variation of `test_actions_are_processed_in_correct_order`, see discussion in
* https://github.com/woocommerce/action-scheduler/issues/951 to see why this specific nuance is tested.
*
* @return void
*/
public function test_child_actions_are_processed_in_correct_order() {
$time = time() - 10;
$actual_order = array();
$watcher = function ( $number ) use ( &$actual_order ) {
$actual_order[] = $number;
};
$parent_action = function () use ( $time ) {
// We generate 20 test actions because this is optimal for reproducing the conditions in the
// linked bug report. With fewer actions, the error condition is less likely to surface.
for ( $i = 1; $i <= 20; $i++ ) {
as_schedule_single_action( $time, 'foo', array( $i ) );
}
};
add_action( 'foo', $watcher );
add_action( 'parent', $parent_action );
as_schedule_single_action( $time, 'parent' );
ActionScheduler_Mocker::get_queue_runner( ActionScheduler::store() )->run();
remove_action( 'foo', $watcher );
add_action( 'parent', $parent_action );
$this->assertEquals( range( 1, 20 ), $actual_order, 'Once claimed, scheduled actions are executed in the expected order, including if "child actions" are scheduled from within another action.' );
}
/**
* @param bool $expected_result
* @param string $db_server_info
*
* @return void
*
* @dataProvider db_supports_skip_locked_provider
*/
public function test_db_supports_skip_locked( bool $expected_result, string $db_server_info ) {
global $wpdb;
// Stash the original since we're overwriting it with a partial mock. Self::tear_down() will restore this.
$this->original_wpdb = $wpdb;
$wpdb = $this->getMockBuilder( get_class( $wpdb ) )
->setMethods( [ 'db_server_info' ] )
->disableOriginalConstructor()
->getMock();
$wpdb->method( 'db_server_info' )->willReturn( $db_server_info );
$reflection = new \ReflectionClass( ActionScheduler_DBStore::class );
$method = $reflection->getMethod( 'db_supports_skip_locked' );
$method->setAccessible( true );
$db_store = new ActionScheduler_DBStore();
$this->assertSame( $expected_result, $method->invoke( $db_store ) );
}
/**
* Data Provider for ::test_db_supports_skip_locked().
*
* @return array[]
*/
public static function db_supports_skip_locked_provider(): array {
// PHP <= 8.0.15 didn't strip the 5.5.5- prefix for MariaDB.
$maria_db_prefix = PHP_VERSION_ID < 80016 ? '5.5.5-' : '';
return array(
'MySQL 5.6.1 does not support skip locked' => array(
false,
'5.6.1'
),
'MySQL 8.0.0 does not support skip locked' => array(
false,
'8.0.0'
),
'MySQL 8.0.1 does support skip locked' => array(
true,
'8.0.1'
),
'MySQL 8.4.4 does support skip locked' => array(
true,
'8.4.4'
),
'MariaDB 10.5.0 does not support skip locked' => array(
false,
$maria_db_prefix . '10.5.0-MariaDB'
),
'MariaDB 10.6.0 does support skip locked' => array(
true,
$maria_db_prefix . '10.6.0-MariaDB'
),
'MariaDB 11.5.0 does support skip locked' => array(
true,
$maria_db_prefix . '11.5.0-MariaDB'
),
);
}
}

View File

@@ -0,0 +1,277 @@
<?php
use Action_Scheduler\Migration\Config;
use ActionScheduler_NullAction as NullAction;
use ActionScheduler_wpCommentLogger as CommentLogger;
use ActionScheduler_wpPostStore as PostStore;
/**
* Class ActionScheduler_HybridStore_Test
* @group tables
*/
class ActionScheduler_HybridStore_Test extends ActionScheduler_UnitTestCase {
/** @var int */
private $demarkation_id = 1000;
public function setUp(): void {
parent::setUp();
if ( ! taxonomy_exists( PostStore::GROUP_TAXONOMY ) ) {
// register the post type and taxonomy necessary for the store to work.
$store = new PostStore();
$store->init();
}
update_option( ActionScheduler_HybridStore::DEMARKATION_OPTION, $this->demarkation_id );
$hybrid = new ActionScheduler_HybridStore();
$hybrid->set_autoincrement( '', ActionScheduler_StoreSchema::ACTIONS_TABLE );
}
public function tearDown(): void {
parent::tearDown();
// reset the autoincrement index.
/** @var \wpdb $wpdb */
global $wpdb;
$wpdb->query( "TRUNCATE TABLE {$wpdb->actionscheduler_actions}" );
$wpdb->query( "TRUNCATE TABLE {$wpdb->actionscheduler_logs}" );
delete_option( ActionScheduler_HybridStore::DEMARKATION_OPTION );
}
public function test_actions_are_migrated_on_find() {
$source_store = new PostStore();
$destination_store = new ActionScheduler_DBStore();
$source_logger = new CommentLogger();
$destination_logger = new ActionScheduler_DBLogger();
$config = new Config();
$config->set_source_store( $source_store );
$config->set_source_logger( $source_logger );
$config->set_destination_store( $destination_store );
$config->set_destination_logger( $destination_logger );
$hybrid_store = new ActionScheduler_HybridStore( $config );
$time = as_get_datetime_object( '10 minutes ago' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
$source_id = $source_store->save_action( $action );
$found = $hybrid_store->find_action( __FUNCTION__, array() );
$this->assertNotEquals( $source_id, $found );
$this->assertGreaterThanOrEqual( $this->demarkation_id, $found );
$found_in_source = $source_store->fetch_action( $source_id );
$this->assertInstanceOf( NullAction::class, $found_in_source );
}
public function test_actions_are_migrated_on_query() {
$source_store = new PostStore();
$destination_store = new ActionScheduler_DBStore();
$source_logger = new CommentLogger();
$destination_logger = new ActionScheduler_DBLogger();
$config = new Config();
$config->set_source_store( $source_store );
$config->set_source_logger( $source_logger );
$config->set_destination_store( $destination_store );
$config->set_destination_logger( $destination_logger );
$hybrid_store = new ActionScheduler_HybridStore( $config );
$source_actions = array();
$destination_actions = array();
for ( $i = 0; $i < 10; $i++ ) {
// create in instance in the source store.
$time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
$source_actions[] = $source_store->save_action( $action );
// create an instance in the destination store.
$time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
$destination_actions[] = $destination_store->save_action( $action );
}
$found = $hybrid_store->query_actions(
array(
'hook' => __FUNCTION__,
'per_page' => 6,
)
);
$this->assertCount( 6, $found );
foreach ( $found as $key => $action_id ) {
$this->assertNotContains( $action_id, $source_actions );
$this->assertGreaterThanOrEqual( $this->demarkation_id, $action_id );
if ( 0 === $key % 2 ) { // it should have been in the source store.
$this->assertNotContains( $action_id, $destination_actions );
} else { // it should have already been in the destination store.
$this->assertContains( $action_id, $destination_actions );
}
}
// six of the original 10 should have migrated to the new store,
// even though only three were retrieve in the final query.
$found_in_source = $source_store->query_actions(
array(
'hook' => __FUNCTION__,
'per_page' => 10,
)
);
$this->assertCount( 4, $found_in_source );
}
public function test_actions_are_migrated_on_claim() {
$source_store = new PostStore();
$destination_store = new ActionScheduler_DBStore();
$source_logger = new CommentLogger();
$destination_logger = new ActionScheduler_DBLogger();
$config = new Config();
$config->set_source_store( $source_store );
$config->set_source_logger( $source_logger );
$config->set_destination_store( $destination_store );
$config->set_destination_logger( $destination_logger );
$hybrid_store = new ActionScheduler_HybridStore( $config );
$source_actions = array();
$destination_actions = array();
for ( $i = 0; $i < 10; $i++ ) {
// create in instance in the source store.
$time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes ago' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
$source_actions[] = $source_store->save_action( $action );
// create an instance in the destination store.
$time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes ago' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
$destination_actions[] = $destination_store->save_action( $action );
}
$claim = $hybrid_store->stake_claim( 6 );
$claimed_actions = $claim->get_actions();
$this->assertCount( 6, $claimed_actions );
$this->assertCount( 3, array_intersect( $destination_actions, $claimed_actions ) );
// six of the original 10 should have migrated to the new store,
// even though only three were retrieve in the final claim.
$found_in_source = $source_store->query_actions(
array(
'hook' => __FUNCTION__,
'per_page' => 10,
)
);
$this->assertCount( 4, $found_in_source );
$this->assertEquals( 0, $source_store->get_claim_count() );
$this->assertEquals( 1, $destination_store->get_claim_count() );
$this->assertEquals( 1, $hybrid_store->get_claim_count() );
}
public function test_fetch_respects_demarkation() {
$source_store = new PostStore();
$destination_store = new ActionScheduler_DBStore();
$source_logger = new CommentLogger();
$destination_logger = new ActionScheduler_DBLogger();
$config = new Config();
$config->set_source_store( $source_store );
$config->set_source_logger( $source_logger );
$config->set_destination_store( $destination_store );
$config->set_destination_logger( $destination_logger );
$hybrid_store = new ActionScheduler_HybridStore( $config );
$source_actions = array();
$destination_actions = array();
for ( $i = 0; $i < 2; $i++ ) {
// create in instance in the source store.
$time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes ago' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
$source_actions[] = $source_store->save_action( $action );
// create an instance in the destination store.
$time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes ago' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
$destination_actions[] = $destination_store->save_action( $action );
}
foreach ( $source_actions as $action_id ) {
$action = $hybrid_store->fetch_action( $action_id );
$this->assertInstanceOf( ActionScheduler_Action::class, $action );
$this->assertNotInstanceOf( NullAction::class, $action );
}
foreach ( $destination_actions as $action_id ) {
$action = $hybrid_store->fetch_action( $action_id );
$this->assertInstanceOf( ActionScheduler_Action::class, $action );
$this->assertNotInstanceOf( NullAction::class, $action );
}
}
public function test_mark_complete_respects_demarkation() {
$source_store = new PostStore();
$destination_store = new ActionScheduler_DBStore();
$source_logger = new CommentLogger();
$destination_logger = new ActionScheduler_DBLogger();
$config = new Config();
$config->set_source_store( $source_store );
$config->set_source_logger( $source_logger );
$config->set_destination_store( $destination_store );
$config->set_destination_logger( $destination_logger );
$hybrid_store = new ActionScheduler_HybridStore( $config );
$source_actions = array();
$destination_actions = array();
for ( $i = 0; $i < 2; $i++ ) {
// create in instance in the source store.
$time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes ago' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
$source_actions[] = $source_store->save_action( $action );
// create an instance in the destination store.
$time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes ago' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
$destination_actions[] = $destination_store->save_action( $action );
}
foreach ( $source_actions as $action_id ) {
$hybrid_store->mark_complete( $action_id );
$action = $hybrid_store->fetch_action( $action_id );
$this->assertInstanceOf( ActionScheduler_FinishedAction::class, $action );
}
foreach ( $destination_actions as $action_id ) {
$hybrid_store->mark_complete( $action_id );
$action = $hybrid_store->fetch_action( $action_id );
$this->assertInstanceOf( ActionScheduler_FinishedAction::class, $action );
}
}
}

View File

@@ -0,0 +1,474 @@
<?php
use Action_Scheduler\Tests\DataStores\AbstractStoreTest;
/**
* Class ActionScheduler_wpPostStore_Test
* @group stores
*/
class ActionScheduler_wpPostStore_Test extends AbstractStoreTest {
/**
* Get data store for tests.
*
* @return ActionScheduler_wpPostStore
*/
protected function get_store() {
return new ActionScheduler_wpPostStore();
}
public function test_create_action() {
$time = as_get_datetime_object();
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
$store = new ActionScheduler_wpPostStore();
$action_id = $store->save_action( $action );
$this->assertNotEmpty( $action_id );
}
public function test_create_action_with_scheduled_date() {
$time = as_get_datetime_object( strtotime( '-1 week' ) );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), new ActionScheduler_SimpleSchedule( $time ) );
$store = new ActionScheduler_wpPostStore();
$action_id = $store->save_action( $action, $time );
$action_date = $store->get_date( $action_id );
$this->assertEquals( $time->getTimestamp(), $action_date->getTimestamp() );
}
public function test_retrieve_action() {
$time = as_get_datetime_object();
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' );
$store = new ActionScheduler_wpPostStore();
$action_id = $store->save_action( $action );
$retrieved = $store->fetch_action( $action_id );
$this->assertEquals( $action->get_hook(), $retrieved->get_hook() );
$this->assertEqualSets( $action->get_args(), $retrieved->get_args() );
$this->assertEquals( $action->get_schedule()->get_date()->getTimestamp(), $retrieved->get_schedule()->get_date()->getTimestamp() );
$this->assertEquals( $action->get_group(), $retrieved->get_group() );
}
/**
* @dataProvider provide_bad_args
*
* @param string $content Post content.
*/
public function test_action_bad_args( $content ) {
$store = new ActionScheduler_wpPostStore();
$post_id = wp_insert_post(
array(
'post_type' => ActionScheduler_wpPostStore::POST_TYPE,
'post_status' => ActionScheduler_Store::STATUS_PENDING,
'post_content' => $content,
)
);
$fetched = $store->fetch_action( $post_id );
$this->assertInstanceOf( 'ActionScheduler_NullSchedule', $fetched->get_schedule() );
}
public function provide_bad_args() {
return array(
array( '{"bad_json":true}}' ),
);
}
public function test_cancel_action() {
$time = as_get_datetime_object();
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' );
$store = new ActionScheduler_wpPostStore();
$action_id = $store->save_action( $action );
$store->cancel_action( $action_id );
$fetched = $store->fetch_action( $action_id );
$this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched );
}
public function test_cancel_actions_by_hook() {
$store = new ActionScheduler_wpPostStore();
$actions = array();
$hook = 'by_hook_test';
for ( $day = 1; $day <= 3; $day++ ) {
$delta = sprintf( '+%d day', $day );
$time = as_get_datetime_object( $delta );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( $hook, array(), $schedule, 'my_group' );
$actions[] = $store->save_action( $action );
}
$store->cancel_actions_by_hook( $hook );
foreach ( $actions as $action_id ) {
$fetched = $store->fetch_action( $action_id );
$this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched );
}
}
public function test_cancel_actions_by_group() {
$store = new ActionScheduler_wpPostStore();
$actions = array();
$group = 'by_group_test';
for ( $day = 1; $day <= 3; $day++ ) {
$delta = sprintf( '+%d day', $day );
$time = as_get_datetime_object( $delta );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, $group );
$actions[] = $store->save_action( $action );
}
$store->cancel_actions_by_group( $group );
foreach ( $actions as $action_id ) {
$fetched = $store->fetch_action( $action_id );
$this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched );
}
}
public function test_claim_actions() {
$created_actions = array();
$store = new ActionScheduler_wpPostStore();
for ( $i = 3; $i > -3; $i-- ) {
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
$created_actions[] = $store->save_action( $action );
}
$claim = $store->stake_claim();
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
$this->assertCount( 3, $claim->get_actions() );
$this->assertEqualSets( array_slice( $created_actions, 3, 3 ), $claim->get_actions() );
}
public function test_claim_actions_order() {
$store = new ActionScheduler_wpPostStore();
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
$created_actions = array(
$store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'my_group' ) ),
$store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'my_group' ) ),
);
$claim = $store->stake_claim();
$this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
// Verify uniqueness of action IDs.
$this->assertEquals( 2, count( array_unique( $created_actions ) ) );
// Verify the count and order of the actions.
$claimed_actions = $claim->get_actions();
$this->assertCount( 2, $claimed_actions );
$this->assertEquals( $created_actions, $claimed_actions );
// Verify the reversed order doesn't pass.
$reversed_actions = array_reverse( $created_actions );
$this->assertNotEquals( $reversed_actions, $claimed_actions );
}
public function test_duplicate_claim() {
$created_actions = array();
$store = new ActionScheduler_wpPostStore();
for ( $i = 0; $i > -3; $i-- ) {
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
$created_actions[] = $store->save_action( $action );
}
$claim1 = $store->stake_claim();
$claim2 = $store->stake_claim();
$this->assertCount( 3, $claim1->get_actions() );
$this->assertCount( 0, $claim2->get_actions() );
}
public function test_release_claim() {
$created_actions = array();
$store = new ActionScheduler_wpPostStore();
for ( $i = 0; $i > -3; $i-- ) {
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
$created_actions[] = $store->save_action( $action );
}
$claim1 = $store->stake_claim();
$store->release_claim( $claim1 );
$claim2 = $store->stake_claim();
$this->assertCount( 3, $claim2->get_actions() );
}
public function test_search() {
$created_actions = array();
$store = new ActionScheduler_wpPostStore();
for ( $i = -3; $i <= 3; $i++ ) {
$time = as_get_datetime_object( $i . ' hours' );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
$created_actions[] = $store->save_action( $action );
}
$next_no_args = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK );
$this->assertEquals( $created_actions[0], $next_no_args );
$next_with_args = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'args' => array( 1 ) ) );
$this->assertEquals( $created_actions[4], $next_with_args );
$non_existent = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'args' => array( 17 ) ) );
$this->assertNull( $non_existent );
}
public function test_search_by_group() {
$store = new ActionScheduler_wpPostStore();
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) );
$abc = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'abc' ) );
$def = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'def' ) );
$ghi = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'ghi' ) );
$this->assertEquals( $abc, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'abc' ) ) );
$this->assertEquals( $def, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'def' ) ) );
$this->assertEquals( $ghi, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'ghi' ) ) );
}
public function test_post_author() {
$current_user = get_current_user_id();
$time = as_get_datetime_object();
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
$store = new ActionScheduler_wpPostStore();
$action_id = $store->save_action( $action );
$post = get_post( $action_id );
$this->assertEquals( 0, $post->post_author );
$new_user = $this->factory->user->create_object(
array(
'user_login' => __FUNCTION__,
'user_pass' => md5( wp_rand() ),
)
);
wp_set_current_user( $new_user );
$schedule = new ActionScheduler_SimpleSchedule( $time );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
$action_id = $store->save_action( $action );
$post = get_post( $action_id );
$this->assertEquals( 0, $post->post_author );
wp_set_current_user( $current_user );
}
/**
* @issue 13
*/
public function test_post_status_for_recurring_action() {
$time = as_get_datetime_object( '10 minutes' );
$schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
$store = new ActionScheduler_wpPostStore();
$action_id = $store->save_action( $action );
$action = $store->fetch_action( $action_id );
$action->execute();
$store->mark_complete( $action_id );
$next = $action->get_schedule()->get_next( as_get_datetime_object() );
$new_action_id = $store->save_action( $action, $next );
$this->assertEquals( 'publish', get_post_status( $action_id ) );
$this->assertEquals( 'pending', get_post_status( $new_action_id ) );
}
public function test_get_run_date() {
$time = as_get_datetime_object( '-10 minutes' );
$schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS );
$action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
$store = new ActionScheduler_wpPostStore();
$action_id = $store->save_action( $action );
$this->assertEquals( $store->get_date( $action_id )->getTimestamp(), $time->getTimestamp() );
$action = $store->fetch_action( $action_id );
$action->execute();
$now = as_get_datetime_object();
$store->mark_complete( $action_id );
$this->assertEquals( $store->get_date( $action_id )->getTimestamp(), $now->getTimestamp(), '', 1 ); // allow timestamp to be 1 second off for older versions of PHP.
$next = $action->get_schedule()->get_next( $now );
$new_action_id = $store->save_action( $action, $next );
$this->assertEquals( (int) ( $now->getTimestamp() ) + HOUR_IN_SECONDS, $store->get_date( $new_action_id )->getTimestamp() );
}
public function test_claim_actions_by_hooks() {
$hook1 = __FUNCTION__ . '_hook_1';
$hook2 = __FUNCTION__ . '_hook_2';
$store = new ActionScheduler_wpPostStore();
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
$action1 = $store->save_action( new ActionScheduler_Action( $hook1, array(), $schedule ) );
$action2 = $store->save_action( new ActionScheduler_Action( $hook2, array(), $schedule ) );
// Claiming no hooks should include all actions.
$claim = $store->stake_claim( 10 );
$this->assertEquals( 2, count( $claim->get_actions() ) );
$this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
$this->assertTrue( in_array( $action2, $claim->get_actions(), true ) );
$store->release_claim( $claim );
// Claiming a hook should claim only actions with that hook.
$claim = $store->stake_claim( 10, null, array( $hook1 ) );
$this->assertEquals( 1, count( $claim->get_actions() ) );
$this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
$store->release_claim( $claim );
// Claiming two hooks should claim actions with either of those hooks.
$claim = $store->stake_claim( 10, null, array( $hook1, $hook2 ) );
$this->assertEquals( 2, count( $claim->get_actions() ) );
$this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
$this->assertTrue( in_array( $action2, $claim->get_actions(), true ) );
$store->release_claim( $claim );
// Claiming two hooks should claim actions with either of those hooks.
$claim = $store->stake_claim( 10, null, array( __METHOD__ . '_hook_3' ) );
$this->assertEquals( 0, count( $claim->get_actions() ) );
$this->assertFalse( in_array( $action1, $claim->get_actions(), true ) );
$this->assertFalse( in_array( $action2, $claim->get_actions(), true ) );
$store->release_claim( $claim );
}
/**
* @issue 121
*/
public function test_claim_actions_by_group() {
$group1 = md5( wp_rand() );
$store = new ActionScheduler_wpPostStore();
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
$action1 = $store->save_action( new ActionScheduler_Action( __METHOD__, array(), $schedule, $group1 ) );
$action2 = $store->save_action( new ActionScheduler_Action( __METHOD__, array(), $schedule ) );
// Claiming no group should include all actions.
$claim = $store->stake_claim( 10 );
$this->assertEquals( 2, count( $claim->get_actions() ) );
$this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
$this->assertTrue( in_array( $action2, $claim->get_actions(), true ) );
$store->release_claim( $claim );
// Claiming a group should claim only actions in that group.
$claim = $store->stake_claim( 10, null, array(), $group1 );
$this->assertEquals( 1, count( $claim->get_actions() ) );
$this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
$store->release_claim( $claim );
}
public function test_claim_actions_by_hook_and_group() {
$hook1 = __FUNCTION__ . '_hook_1';
$hook2 = __FUNCTION__ . '_hook_2';
$hook3 = __FUNCTION__ . '_hook_3';
$group1 = 'group_' . md5( wp_rand() );
$group2 = 'group_' . md5( wp_rand() );
$store = new ActionScheduler_wpPostStore();
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
$action1 = $store->save_action( new ActionScheduler_Action( $hook1, array(), $schedule, $group1 ) );
$action2 = $store->save_action( new ActionScheduler_Action( $hook2, array(), $schedule ) );
$action3 = $store->save_action( new ActionScheduler_Action( $hook3, array(), $schedule, $group2 ) );
// Claiming no hooks or group should include all actions.
$claim = $store->stake_claim( 10 );
$this->assertEquals( 3, count( $claim->get_actions() ) );
$this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
$this->assertTrue( in_array( $action2, $claim->get_actions(), true ) );
$store->release_claim( $claim );
// Claiming a group and hook should claim only actions in that group.
$claim = $store->stake_claim( 10, null, array( $hook1 ), $group1 );
$this->assertEquals( 1, count( $claim->get_actions() ) );
$this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
$store->release_claim( $claim );
// Claiming a group and hook should claim only actions with that hook in that group.
$claim = $store->stake_claim( 10, null, array( $hook2 ), $group1 );
$this->assertEquals( 0, count( $claim->get_actions() ) );
$this->assertFalse( in_array( $action1, $claim->get_actions(), true ) );
$this->assertFalse( in_array( $action2, $claim->get_actions(), true ) );
$store->release_claim( $claim );
// Claiming a group and hook should claim only actions with that hook in that group.
$claim = $store->stake_claim( 10, null, array( $hook1, $hook2 ), $group2 );
$this->assertEquals( 0, count( $claim->get_actions() ) );
$this->assertFalse( in_array( $action1, $claim->get_actions(), true ) );
$this->assertFalse( in_array( $action2, $claim->get_actions(), true ) );
$store->release_claim( $claim );
}
/**
* The query used to claim actions explicitly ignores future pending actions, but it
* is still possible under unusual conditions (such as if MySQL runs out of temporary
* storage space) for such actions to be returned.
*
* When this happens, we still expect the store to filter them out, otherwise there is
* a risk that actions will be unexpectedly processed ahead of time.
*
* @see https://github.com/woocommerce/action-scheduler/issues/634
*/
public function test_claim_filters_out_unexpected_future_actions() {
$group = __METHOD__;
$store = new ActionScheduler_wpPostStore();
// Create 4 actions: 2 that are already due (-3hrs and -1hrs) and 2 that are not yet due (+1hr and +3hrs).
for ( $i = -3; $i <= 3; $i += 2 ) {
$schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( $i . ' hours' ) );
$action_ids[] = $store->save_action( new ActionScheduler_Action( 'test_' . $i, array(), $schedule, $group ) );
}
// This callback is used to simulate the unusual conditions whereby MySQL might unexpectedly return future
// actions, contrary to the conditions used by the store object when staking its claim.
$simulate_unexpected_db_behavior = function( $sql ) use ( $action_ids ) {
global $wpdb;
$post_type = ActionScheduler_wpPostStore::POST_TYPE;
$pending = ActionScheduler_wpPostStore::STATUS_PENDING;
// Look out for the claim update query, ignore all others.
if (
0 !== strpos( $sql, "UPDATE $wpdb->posts" )
|| 0 !== strpos( $sql, "WHERE post_type = '$post_type' AND post_status = '$pending' AND post_password = ''" )
|| ! preg_match( "/AND post_date_gmt <= '([0-9:\-\s]{19})'/", $sql, $matches )
|| count( $matches ) !== 2
) {
return $sql;
}
// Now modify the query, forcing it to also return the future actions we created.
return str_replace( $matches[1], as_get_datetime_object( '+4 hours' )->format( 'Y-m-d H:i:s' ), $sql );
};
add_filter( 'query', $simulate_unexpected_db_behavior );
$claim = $store->stake_claim( 10, null, array(), $group );
$claimed_actions = $claim->get_actions();
$this->assertCount( 2, $claimed_actions );
// Cleanup.
remove_filter( 'query', $simulate_unexpected_db_behavior );
$store->release_claim( $claim );
}
}