wiki.techinc.nl/tests/phpunit/includes/deferred/DeferredUpdatesManagerTest.php
DannyS712 07af42e199 Add DeferredUpdatesManager service to replace DeferredUpdates
This patch doesn't deal with the injection of dependencies
and removal of the global state, but rather moves the code
from DeferredUpdates to the new service essentially as-is,
to simplify review. The changes to inject the various
services needed and make DeferredUpdatesManager a proper
service will be done in follow-ups, to make them easier
to follow.

While almost everything is changed from static to non-static,
DeferredUpdates::$scopeStack remains static as
DeferredUpdatesManager::$scopeStack, just in case multiple
versions of the service are created, to ensure that no
updates are missed.

Bug: T265749
Change-Id: I7f07eddf2fc399b15db4fe9be4c792ef8eb0747b
2023-05-29 03:59:03 +00:00

510 lines
14 KiB
PHP

<?php
use MediaWiki\Deferred\DeferredUpdatesManager;
use MediaWiki\MediaWikiServices;
/**
* Based on DeferredUpdatesTest but calling the corresponding
* service methods instead
*
* @coversDefaultClass \MediaWiki\Deferred\DeferredUpdatesManager
*/
class DeferredUpdatesManagerTest extends MediaWikiIntegrationTestCase {
private function getManager() {
return MediaWikiServices::getInstance()->getDeferredUpdatesManager();
}
/**
* @covers ::addUpdate
* @covers ::doUpdates
* @covers ::attemptUpdate
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testAddAndRun() {
$update = $this->getMockBuilder( DeferrableUpdate::class )
->onlyMethods( [ 'doUpdate' ] )->getMock();
$update->expects( $this->once() )->method( 'doUpdate' );
$manager = $this->getManager();
$manager->addUpdate( $update );
$manager->doUpdates();
}
/**
* @covers ::addUpdate
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testAddMergeable() {
$this->setMwGlobals( 'wgCommandLineMode', false );
$update1 = $this->getMockBuilder( MergeableUpdate::class )
->onlyMethods( [ 'merge', 'doUpdate' ] )->getMock();
$update1->expects( $this->once() )->method( 'merge' );
$update1->expects( $this->never() )->method( 'doUpdate' );
$update2 = $this->getMockBuilder( MergeableUpdate::class )
->onlyMethods( [ 'merge', 'doUpdate' ] )->getMock();
$update2->expects( $this->never() )->method( 'merge' );
$update2->expects( $this->never() )->method( 'doUpdate' );
$manager = $this->getManager();
$manager->addUpdate( $update1 );
$manager->addUpdate( $update2 );
}
/**
* @covers ::addCallableUpdate
* @covers MWCallableUpdate::getOrigin
*/
public function testAddCallableUpdate() {
$this->setMwGlobals( 'wgCommandLineMode', true );
$manager = $this->getManager();
$ran = 0;
$manager->addCallableUpdate( static function () use ( &$ran ) {
$ran++;
} );
$manager->doUpdates();
$this->assertSame( 1, $ran, 'Update ran' );
}
/**
* @covers ::getPendingUpdates
* @covers ::clearPendingUpdates
*/
public function testGetPendingUpdates() {
// Prevent updates from running
$this->setMwGlobals( 'wgCommandLineMode', false );
$pre = DeferredUpdatesManager::PRESEND;
$post = DeferredUpdatesManager::POSTSEND;
$all = DeferredUpdatesManager::ALL;
$update = $this->createMock( DeferrableUpdate::class );
$update->expects( $this->never() )
->method( 'doUpdate' );
$manager = $this->getManager();
$manager->addUpdate( $update, $pre );
$this->assertCount( 1, $manager->getPendingUpdates( $pre ) );
$this->assertSame( [], $manager->getPendingUpdates( $post ) );
$this->assertCount( 1, $manager->getPendingUpdates( $all ) );
$this->assertCount( 1, $manager->getPendingUpdates() );
$manager->clearPendingUpdates();
$this->assertSame( [], $manager->getPendingUpdates() );
$manager->addUpdate( $update, $post );
$this->assertSame( [], $manager->getPendingUpdates( $pre ) );
$this->assertCount( 1, $manager->getPendingUpdates( $post ) );
$this->assertCount( 1, $manager->getPendingUpdates( $all ) );
$this->assertCount( 1, $manager->getPendingUpdates() );
$manager->clearPendingUpdates();
$this->assertSame( [], $manager->getPendingUpdates() );
}
/**
* @covers ::doUpdates
* @covers ::addUpdate
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testDoUpdatesWeb() {
$this->setMwGlobals( 'wgCommandLineMode', false );
$updates = [
'1' => "deferred update 1;\n",
'2' => "deferred update 2;\n",
'2-1' => "deferred update 1 within deferred update 2;\n",
'2-2' => "deferred update 2 within deferred update 2;\n",
'3' => "deferred update 3;\n",
'3-1' => "deferred update 1 within deferred update 3;\n",
'3-2' => "deferred update 2 within deferred update 3;\n",
'3-1-1' => "deferred update 1 within deferred update 1 within deferred update 3;\n",
'3-2-1' => "deferred update 1 within deferred update 2 with deferred update 3;\n",
];
$manager = $this->getManager();
$manager->addCallableUpdate(
static function () use ( $updates ) {
echo $updates['1'];
}
);
$manager->addCallableUpdate(
static function () use ( $updates, $manager ) {
echo $updates['2'];
$manager->addCallableUpdate(
static function () use ( $updates ) {
echo $updates['2-1'];
}
);
$manager->addCallableUpdate(
static function () use ( $updates ) {
echo $updates['2-2'];
}
);
}
);
$manager->addCallableUpdate(
static function () use ( $updates, $manager ) {
echo $updates['3'];
$manager->addCallableUpdate(
static function () use ( $updates, $manager ) {
echo $updates['3-1'];
$manager->addCallableUpdate(
static function () use ( $updates ) {
echo $updates['3-1-1'];
}
);
}
);
$manager->addCallableUpdate(
static function () use ( $updates, $manager ) {
echo $updates['3-2'];
$manager->addCallableUpdate(
static function () use ( $updates ) {
echo $updates['3-2-1'];
}
);
}
);
}
);
$this->assertEquals( 3, $manager->pendingUpdatesCount() );
$this->expectOutputString( implode( '', $updates ) );
$manager->doUpdates();
$x = null;
$y = null;
$manager->addCallableUpdate(
static function () use ( &$x ) {
$x = 'Sherity';
},
DeferredUpdatesManager::PRESEND
);
$manager->addCallableUpdate(
static function () use ( &$y ) {
$y = 'Marychu';
},
DeferredUpdatesManager::POSTSEND
);
$this->assertNull( $x, "Update not run yet" );
$this->assertNull( $y, "Update not run yet" );
$manager->doUpdates( DeferredUpdatesManager::PRESEND );
$this->assertEquals( "Sherity", $x, "PRESEND update ran" );
$this->assertNull( $y, "POSTSEND update not run yet" );
$manager->doUpdates( DeferredUpdatesManager::POSTSEND );
$this->assertEquals( "Marychu", $y, "POSTSEND update ran" );
}
/**
* @covers ::doUpdates
* @covers ::addUpdate
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testDoUpdatesCLI() {
$this->setMwGlobals( 'wgCommandLineMode', true );
$updates = [
'1' => "deferred update 1;\n",
'2' => "deferred update 2;\n",
'2-1' => "deferred update 1 within deferred update 2;\n",
'2-2' => "deferred update 2 within deferred update 2;\n",
'3' => "deferred update 3;\n",
'3-1' => "deferred update 1 within deferred update 3;\n",
'3-2' => "deferred update 2 within deferred update 3;\n",
'3-1-1' => "deferred update 1 within deferred update 1 within deferred update 3;\n",
'3-2-1' => "deferred update 1 within deferred update 2 with deferred update 3;\n",
];
// clear anything
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$lbFactory->commitPrimaryChanges( __METHOD__ );
$manager = $this->getManager();
$manager->addCallableUpdate(
static function () use ( $updates ) {
echo $updates['1'];
}
);
$manager->addCallableUpdate(
static function () use ( $updates, $manager ) {
echo $updates['2'];
$manager->addCallableUpdate(
static function () use ( $updates ) {
echo $updates['2-1'];
}
);
$manager->addCallableUpdate(
static function () use ( $updates ) {
echo $updates['2-2'];
}
);
}
);
$manager->addCallableUpdate(
static function () use ( $updates, $manager ) {
echo $updates['3'];
$manager->addCallableUpdate(
static function () use ( $updates, $manager ) {
echo $updates['3-1'];
$manager->addCallableUpdate(
static function () use ( $updates ) {
echo $updates['3-1-1'];
}
);
}
);
$manager->addCallableUpdate(
static function () use ( $updates, $manager ) {
echo $updates['3-2'];
$manager->addCallableUpdate(
static function () use ( $updates ) {
echo $updates['3-2-1'];
}
);
}
);
}
);
$this->expectOutputString( implode( '', $updates ) );
$manager->doUpdates();
}
/**
* @covers ::doUpdates
* @covers ::addUpdate
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testPresendAddOnPostsendRun() {
$this->setMwGlobals( 'wgCommandLineMode', true );
$x = false;
$y = false;
// clear anything
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$lbFactory->commitPrimaryChanges( __METHOD__ );
$manager = $this->getManager();
$manager->addCallableUpdate(
static function () use ( &$x, &$y, $manager ) {
$x = true;
$manager->addCallableUpdate(
static function () use ( &$y ) {
$y = true;
},
DeferredUpdatesManager::PRESEND
);
},
DeferredUpdatesManager::POSTSEND
);
$manager->doUpdates();
$this->assertTrue( $x, "Outer POSTSEND update ran" );
$this->assertTrue( $y, "Nested PRESEND update ran" );
}
/**
* @covers ::attemptUpdate
*/
public function testRunUpdateTransactionScope() {
$this->setMwGlobals( 'wgCommandLineMode', false );
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$this->assertFalse( $lbFactory->hasTransactionRound(), 'Initial state' );
$manager = $this->getManager();
$ran = 0;
$manager->addCallableUpdate( function () use ( &$ran, $lbFactory ) {
$ran++;
$this->assertTrue( $lbFactory->hasTransactionRound(), 'Has transaction' );
} );
$manager->doUpdates();
$this->assertSame( 1, $ran, 'Update ran' );
$this->assertFalse( $lbFactory->hasTransactionRound(), 'Final state' );
}
/**
* @covers ::attemptUpdate
* @covers TransactionRoundDefiningUpdate::getOrigin
*/
public function testRunOuterScopeUpdate() {
$this->setMwGlobals( 'wgCommandLineMode', false );
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$this->assertFalse( $lbFactory->hasTransactionRound(), 'Initial state' );
$manager = $this->getManager();
$ran = 0;
$manager->addUpdate( new TransactionRoundDefiningUpdate(
function () use ( &$ran, $lbFactory ) {
$ran++;
$this->assertFalse( $lbFactory->hasTransactionRound(), 'No transaction' );
} )
);
$manager->doUpdates();
$this->assertSame( 1, $ran, 'Update ran' );
}
/**
* @covers ::tryOpportunisticExecute
* @covers ::doUpdates
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testTryOpportunisticExecute() {
$calls = [];
$callback1 = static function () use ( &$calls ) {
$calls[] = 1;
};
$callback2 = static function () use ( &$calls ) {
$calls[] = 2;
};
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$lbFactory->beginPrimaryChanges( __METHOD__ );
$manager = $this->getManager();
$manager->addCallableUpdate( $callback1 );
$this->assertEquals( [], $calls );
$manager->tryOpportunisticExecute();
$this->assertEquals( [], $calls );
$dbw = $this->getDb();
$dbw->onTransactionCommitOrIdle( function () use ( &$calls, $callback2, $manager ) {
$manager->addCallableUpdate( $callback2 );
$this->assertEquals( [], $calls );
$calls[] = 'oti';
} );
$this->assertSame( 1, $dbw->trxLevel() );
$this->assertEquals( [], $calls );
$lbFactory->commitPrimaryChanges( __METHOD__ );
$this->assertEquals( [ 'oti' ], $calls );
$manager->tryOpportunisticExecute();
$this->assertEquals( [ 'oti', 1, 2 ], $calls );
}
/**
* @covers ::attemptUpdate
*/
public function testCallbackUpdateRounds() {
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$fname = __METHOD__;
$called = false;
$manager = $this->getManager();
$manager->attemptUpdate(
new MWCallableUpdate(
static function () use ( $lbFactory, $fname, &$called ) {
$lbFactory->flushReplicaSnapshots( $fname );
$lbFactory->commitPrimaryChanges( $fname );
$called = true;
},
$fname
),
$lbFactory
);
$this->assertTrue( $called, "Callback ran" );
}
/**
* @covers ::doUpdates
* @covers DeferredUpdatesScopeStack
* @covers DeferredUpdatesScope
*/
public function testNestedExecution() {
// No immediate execution
$this->setMwGlobals( 'wgCommandLineMode', false );
$res = null;
$resSub = null;
$resSubSub = null;
$resA = null;
$manager = $this->getManager();
$manager->clearPendingUpdates();
$this->assertSame( 0, $manager->pendingUpdatesCount() );
$this->assertSame( 0, $manager->getRecursiveExecutionStackDepth() );
// T249069: TransactionRoundDefiningUpdate => JobRunner => DeferredUpdatesManager::doUpdates()
$manager->addUpdate( new TransactionRoundDefiningUpdate(
function () use ( &$res, &$resSub, &$resSubSub, &$resA, $manager ) {
$res = 1;
$this->assertSame( 0, $manager->pendingUpdatesCount() );
$this->assertSame( 1, $manager->getRecursiveExecutionStackDepth() );
// Add update to subqueue of in-progress top-queue job
$manager->addCallableUpdate( function () use ( &$resSub, &$resSubSub, $manager ) {
$resSub = 'a';
$this->assertSame( 0, $manager->pendingUpdatesCount() );
$this->assertSame( 2, $manager->getRecursiveExecutionStackDepth() );
// Add update to subqueue of in-progress top-queue job (not recursive)
$manager->addCallableUpdate( static function () use ( &$resSubSub ) {
$resSubSub = 'b';
} );
$this->assertSame( 1, $manager->pendingUpdatesCount() );
} );
$this->assertSame( 1, $manager->pendingUpdatesCount() );
$this->assertSame( 1, $manager->getRecursiveExecutionStackDepth() );
if ( $resSub === null && $resA === null && $resSubSub === null ) {
$res = 418;
}
$manager->doUpdates();
}
) );
$this->assertSame( 1, $manager->pendingUpdatesCount() );
$this->assertSame( 0, $manager->getRecursiveExecutionStackDepth() );
$manager->addCallableUpdate( static function () use ( &$resA ) {
$resA = 93;
} );
$this->assertSame( 2, $manager->pendingUpdatesCount() );
$this->assertSame( 0, $manager->getRecursiveExecutionStackDepth() );
$this->assertNull( $resA );
$this->assertNull( $res );
$this->assertNull( $resSub );
$this->assertNull( $resSubSub );
$manager->doUpdates();
$this->assertSame( 0, $manager->pendingUpdatesCount() );
$this->assertSame( 0, $manager->getRecursiveExecutionStackDepth() );
$this->assertSame( 418, $res );
$this->assertSame( 'a', $resSub );
$this->assertSame( 'b', $resSubSub );
$this->assertSame( 93, $resA );
}
}