summaryrefslogtreecommitdiff
path: root/www/wiki/tests/phpunit/includes/jobqueue
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/tests/phpunit/includes/jobqueue')
-rw-r--r--www/wiki/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php63
-rw-r--r--www/wiki/tests/phpunit/includes/jobqueue/JobQueueTest.php393
-rw-r--r--www/wiki/tests/phpunit/includes/jobqueue/JobTest.php133
-rw-r--r--www/wiki/tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php113
-rw-r--r--www/wiki/tests/phpunit/includes/jobqueue/jobs/CategoryMembershipChangeJobTest.php87
-rw-r--r--www/wiki/tests/phpunit/includes/jobqueue/jobs/ClearUserWatchlistJobTest.php79
6 files changed, 868 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php b/www/wiki/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php
new file mode 100644
index 00000000..bf8603dd
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @covers JobQueueMemory
+ *
+ * @group JobQueue
+ *
+ * @license GNU GPL v2+
+ * @author Thiemo Kreuz
+ */
+class JobQueueMemoryTest extends PHPUnit\Framework\TestCase {
+
+ use MediaWikiCoversValidator;
+
+ /**
+ * @return JobQueueMemory
+ */
+ private function newJobQueue() {
+ return JobQueue::factory( [
+ 'class' => JobQueueMemory::class,
+ 'wiki' => wfWikiID(),
+ 'type' => 'null',
+ ] );
+ }
+
+ private function newJobSpecification() {
+ return new JobSpecification(
+ 'null',
+ [ 'customParameter' => null ],
+ [],
+ Title::newFromText( 'Custom title' )
+ );
+ }
+
+ public function testGetAllQueuedJobs() {
+ $queue = $this->newJobQueue();
+ $this->assertCount( 0, $queue->getAllQueuedJobs() );
+
+ $queue->push( $this->newJobSpecification() );
+ $this->assertCount( 1, $queue->getAllQueuedJobs() );
+ }
+
+ public function testGetAllAcquiredJobs() {
+ $queue = $this->newJobQueue();
+ $this->assertCount( 0, $queue->getAllAcquiredJobs() );
+
+ $queue->push( $this->newJobSpecification() );
+ $this->assertCount( 0, $queue->getAllAcquiredJobs() );
+
+ $queue->pop();
+ $this->assertCount( 1, $queue->getAllAcquiredJobs() );
+ }
+
+ public function testJobFromSpecInternal() {
+ $queue = $this->newJobQueue();
+ $job = $queue->jobFromSpecInternal( $this->newJobSpecification() );
+ $this->assertInstanceOf( Job::class, $job );
+ $this->assertSame( 'null', $job->getType() );
+ $this->assertArrayHasKey( 'customParameter', $job->getParams() );
+ $this->assertSame( 'Custom title', $job->getTitle()->getText() );
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/jobqueue/JobQueueTest.php b/www/wiki/tests/phpunit/includes/jobqueue/JobQueueTest.php
new file mode 100644
index 00000000..64dde778
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/jobqueue/JobQueueTest.php
@@ -0,0 +1,393 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @group JobQueue
+ * @group medium
+ * @group Database
+ */
+class JobQueueTest extends MediaWikiTestCase {
+ protected $key;
+ protected $queueRand, $queueRandTTL, $queueFifo, $queueFifoTTL;
+
+ function __construct( $name = null, array $data = [], $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed[] = 'job';
+ }
+
+ protected function setUp() {
+ global $wgJobTypeConf;
+ parent::setUp();
+
+ if ( $this->getCliArg( 'use-jobqueue' ) ) {
+ $name = $this->getCliArg( 'use-jobqueue' );
+ if ( !isset( $wgJobTypeConf[$name] ) ) {
+ throw new MWException( "No \$wgJobTypeConf entry for '$name'." );
+ }
+ $baseConfig = $wgJobTypeConf[$name];
+ } else {
+ $baseConfig = [ 'class' => JobQueueDBSingle::class ];
+ }
+ $baseConfig['type'] = 'null';
+ $baseConfig['wiki'] = wfWikiID();
+ $variants = [
+ 'queueRand' => [ 'order' => 'random', 'claimTTL' => 0 ],
+ 'queueRandTTL' => [ 'order' => 'random', 'claimTTL' => 10 ],
+ 'queueTimestamp' => [ 'order' => 'timestamp', 'claimTTL' => 0 ],
+ 'queueTimestampTTL' => [ 'order' => 'timestamp', 'claimTTL' => 10 ],
+ 'queueFifo' => [ 'order' => 'fifo', 'claimTTL' => 0 ],
+ 'queueFifoTTL' => [ 'order' => 'fifo', 'claimTTL' => 10 ],
+ ];
+ foreach ( $variants as $q => $settings ) {
+ try {
+ $this->$q = JobQueue::factory( $settings + $baseConfig );
+ } catch ( MWException $e ) {
+ // unsupported?
+ // @todo What if it was another error?
+ };
+ }
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+ foreach (
+ [
+ 'queueRand', 'queueRandTTL', 'queueTimestamp', 'queueTimestampTTL',
+ 'queueFifo', 'queueFifoTTL'
+ ] as $q
+ ) {
+ if ( $this->$q ) {
+ $this->$q->delete();
+ }
+ $this->$q = null;
+ }
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ * @covers JobQueue::getWiki
+ */
+ public function testGetWiki( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+ $this->assertEquals( wfWikiID(), $queue->getWiki(), "Proper wiki ID ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ * @covers JobQueue::getType
+ */
+ public function testGetType( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+ $this->assertEquals( 'null', $queue->getType(), "Proper job type ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ * @covers JobQueue
+ */
+ public function testBasicOperations( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
+
+ $this->assertNull( $queue->push( $this->newJob() ), "Push worked ($desc)" );
+ $this->assertNull( $queue->batchPush( [ $this->newJob() ] ), "Push worked ($desc)" );
+
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 2, $queue->getSize(), "Queue size is correct ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+ $jobs = iterator_to_array( $queue->getAllQueuedJobs() );
+ $this->assertEquals( 2, count( $jobs ), "Queue iterator size is correct ($desc)" );
+
+ $job1 = $queue->pop();
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 1, $queue->getSize(), "Queue size is correct ($desc)" );
+
+ $queue->flushCaches();
+ if ( $recycles ) {
+ $this->assertEquals( 1, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ $job2 = $queue->pop();
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ if ( $recycles ) {
+ $this->assertEquals( 2, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ $queue->ack( $job1 );
+
+ $queue->flushCaches();
+ if ( $recycles ) {
+ $this->assertEquals( 1, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ $queue->ack( $job2 );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+
+ $this->assertNull( $queue->batchPush( [ $this->newJob(), $this->newJob() ] ),
+ "Push worked ($desc)" );
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->delete();
+ $queue->flushCaches();
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ * @covers JobQueue
+ */
+ public function testBasicDeduplication( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
+
+ $this->assertNull(
+ $queue->batchPush(
+ [ $this->newDedupedJob(), $this->newDedupedJob(), $this->newDedupedJob() ]
+ ),
+ "Push worked ($desc)" );
+
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 1, $queue->getSize(), "Queue size is correct ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+
+ $this->assertNull(
+ $queue->batchPush(
+ [ $this->newDedupedJob(), $this->newDedupedJob(), $this->newDedupedJob() ]
+ ),
+ "Push worked ($desc)"
+ );
+
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 1, $queue->getSize(), "Queue size is correct ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+
+ $job1 = $queue->pop();
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ if ( $recycles ) {
+ $this->assertEquals( 1, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ $queue->ack( $job1 );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ * @covers JobQueue
+ */
+ public function testDeduplicationWhileClaimed( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $job = $this->newDedupedJob();
+ $queue->push( $job );
+
+ // De-duplication does not apply to already-claimed jobs
+ $j = $queue->pop();
+ $queue->push( $job );
+ $queue->ack( $j );
+
+ $j = $queue->pop();
+ // Make sure ack() of the twin did not delete the sibling data
+ $this->assertType( NullJob::class, $j );
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ * @covers JobQueue
+ */
+ public function testRootDeduplication( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
+
+ $id = wfRandomString( 32 );
+ $root1 = Job::newRootJobParams( "nulljobspam:$id" ); // task ID/timestamp
+ for ( $i = 0; $i < 5; ++$i ) {
+ $this->assertNull( $queue->push( $this->newJob( 0, $root1 ) ), "Push worked ($desc)" );
+ }
+ $queue->deduplicateRootJob( $this->newJob( 0, $root1 ) );
+
+ $root2 = $root1;
+ # Add a second to UNIX epoch and format back to TS_MW
+ $root2_ts = strtotime( $root2['rootJobTimestamp'] );
+ $root2_ts++;
+ $root2['rootJobTimestamp'] = wfTimestamp( TS_MW, $root2_ts );
+
+ $this->assertNotEquals( $root1['rootJobTimestamp'], $root2['rootJobTimestamp'],
+ "Root job signatures have different timestamps." );
+ for ( $i = 0; $i < 5; ++$i ) {
+ $this->assertNull( $queue->push( $this->newJob( 0, $root2 ) ), "Push worked ($desc)" );
+ }
+ $queue->deduplicateRootJob( $this->newJob( 0, $root2 ) );
+
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 10, $queue->getSize(), "Queue size is correct ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+
+ $dupcount = 0;
+ $jobs = [];
+ do {
+ $job = $queue->pop();
+ if ( $job ) {
+ $jobs[] = $job;
+ $queue->ack( $job );
+ }
+ if ( $job instanceof DuplicateJob ) {
+ ++$dupcount;
+ }
+ } while ( $job );
+
+ $this->assertEquals( 10, count( $jobs ), "Correct number of jobs popped ($desc)" );
+ $this->assertEquals( 5, $dupcount, "Correct number of duplicate jobs popped ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_fifoQueueLists
+ * @covers JobQueue
+ */
+ public function testJobOrder( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
+
+ for ( $i = 0; $i < 10; ++$i ) {
+ $this->assertNull( $queue->push( $this->newJob( $i ) ), "Push worked ($desc)" );
+ }
+
+ for ( $i = 0; $i < 10; ++$i ) {
+ $job = $queue->pop();
+ $this->assertTrue( $job instanceof Job, "Jobs popped from queue ($desc)" );
+ $params = $job->getParams();
+ $this->assertEquals( $i, $params['i'], "Job popped from queue is FIFO ($desc)" );
+ $queue->ack( $job );
+ }
+
+ $this->assertFalse( $queue->pop(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+ }
+
+ /**
+ * @covers JobQueue
+ */
+ public function testQueueAggregateTable() {
+ $queue = $this->queueFifo;
+ if ( !$queue || !method_exists( $queue, 'getServerQueuesWithJobs' ) ) {
+ $this->markTestSkipped();
+ }
+
+ $this->assertNotContains(
+ [ $queue->getType(), $queue->getWiki() ],
+ $queue->getServerQueuesWithJobs(),
+ "Null queue not in listing"
+ );
+
+ $queue->push( $this->newJob( 0 ) );
+
+ $this->assertContains(
+ [ $queue->getType(), $queue->getWiki() ],
+ $queue->getServerQueuesWithJobs(),
+ "Null queue in listing"
+ );
+ }
+
+ public static function provider_queueLists() {
+ return [
+ [ 'queueRand', false, 'Random queue without ack()' ],
+ [ 'queueRandTTL', true, 'Random queue with ack()' ],
+ [ 'queueTimestamp', false, 'Time ordered queue without ack()' ],
+ [ 'queueTimestampTTL', true, 'Time ordered queue with ack()' ],
+ [ 'queueFifo', false, 'FIFO ordered queue without ack()' ],
+ [ 'queueFifoTTL', true, 'FIFO ordered queue with ack()' ]
+ ];
+ }
+
+ public static function provider_fifoQueueLists() {
+ return [
+ [ 'queueFifo', false, 'Ordered queue without ack()' ],
+ [ 'queueFifoTTL', true, 'Ordered queue with ack()' ]
+ ];
+ }
+
+ function newJob( $i = 0, $rootJob = [] ) {
+ return new NullJob( Title::newMainPage(),
+ [ 'lives' => 0, 'usleep' => 0, 'removeDuplicates' => 0, 'i' => $i ] + $rootJob );
+ }
+
+ function newDedupedJob( $i = 0, $rootJob = [] ) {
+ return new NullJob( Title::newMainPage(),
+ [ 'lives' => 0, 'usleep' => 0, 'removeDuplicates' => 1, 'i' => $i ] + $rootJob );
+ }
+}
+
+class JobQueueDBSingle extends JobQueueDB {
+ protected function getDB( $index ) {
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ // Override to not use CONN_TRX_AUTOCOMMIT so that we see the same temporary `job` table
+ return $lb->getConnection( $index, [], $this->wiki );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/jobqueue/JobTest.php b/www/wiki/tests/phpunit/includes/jobqueue/JobTest.php
new file mode 100644
index 00000000..0cab7024
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/jobqueue/JobTest.php
@@ -0,0 +1,133 @@
+<?php
+
+/**
+ * @author Addshore
+ */
+class JobTest extends MediaWikiTestCase {
+
+ /**
+ * @dataProvider provideTestToString
+ *
+ * @param Job $job
+ * @param string $expected
+ *
+ * @covers Job::toString
+ */
+ public function testToString( $job, $expected ) {
+ $this->assertEquals( $expected, $job->toString() );
+ }
+
+ public function provideTestToString() {
+ $mockToStringObj = $this->getMockBuilder( stdClass::class )
+ ->setMethods( [ '__toString' ] )->getMock();
+ $mockToStringObj->expects( $this->any() )
+ ->method( '__toString' )
+ ->will( $this->returnValue( '{STRING_OBJ_VAL}' ) );
+
+ $requestId = 'requestId=' . WebRequest::getRequestId();
+
+ return [
+ [
+ $this->getMockJob( false ),
+ 'someCommand ' . $requestId
+ ],
+ [
+ $this->getMockJob( [ 'key' => 'val' ] ),
+ 'someCommand key=val ' . $requestId
+ ],
+ [
+ $this->getMockJob( [ 'key' => [ 'inkey' => 'inval' ] ] ),
+ 'someCommand key={"inkey":"inval"} ' . $requestId
+ ],
+ [
+ $this->getMockJob( [ 'val1' ] ),
+ 'someCommand 0=val1 ' . $requestId
+ ],
+ [
+ $this->getMockJob( [ 'val1', 'val2' ] ),
+ 'someCommand 0=val1 1=val2 ' . $requestId
+ ],
+ [
+ $this->getMockJob( [ new stdClass() ] ),
+ 'someCommand 0=object(stdClass) ' . $requestId
+ ],
+ [
+ $this->getMockJob( [ $mockToStringObj ] ),
+ 'someCommand 0={STRING_OBJ_VAL} ' . $requestId
+ ],
+ [
+ $this->getMockJob( [
+ "pages" => [
+ "932737" => [
+ 0,
+ "Robert_James_Waller"
+ ]
+ ],
+ "rootJobSignature" => "45868e99bba89064e4483743ebb9b682ef95c1a7",
+ "rootJobTimestamp" => "20160309110158",
+ "masterPos" => [
+ "file" => "db1023-bin.001288",
+ "pos" => "308257743",
+ "asOfTime" => 1457521464.3814
+ ],
+ "triggeredRecursive" => true
+ ] ),
+ 'someCommand pages={"932737":[0,"Robert_James_Waller"]} ' .
+ 'rootJobSignature=45868e99bba89064e4483743ebb9b682ef95c1a7 ' .
+ 'rootJobTimestamp=20160309110158 masterPos=' .
+ '{"file":"db1023-bin.001288","pos":"308257743","asOfTime":' .
+ // Embed dynamically because TestSetup sets serialize_precision=17
+ // which, in PHP 7.1 and 7.2, produces 1457521464.3814001 instead
+ json_encode( 1457521464.3814 ) . '} ' . 'triggeredRecursive=1 ' .
+ $requestId
+ ],
+ ];
+ }
+
+ public function getMockJob( $params ) {
+ $mock = $this->getMockForAbstractClass(
+ Job::class,
+ [ 'someCommand', new Title(), $params ],
+ 'SomeJob'
+ );
+ return $mock;
+ }
+
+ /**
+ * @dataProvider provideTestJobFactory
+ *
+ * @param mixed $handler
+ *
+ * @covers Job::factory
+ */
+ public function testJobFactory( $handler ) {
+ $this->mergeMwGlobalArrayValue( 'wgJobClasses', [ 'testdummy' => $handler ] );
+
+ $job = Job::factory( 'testdummy', Title::newMainPage(), [] );
+ $this->assertInstanceOf( NullJob::class, $job );
+
+ $job2 = Job::factory( 'testdummy', Title::newMainPage(), [] );
+ $this->assertInstanceOf( NullJob::class, $job2 );
+ $this->assertNotSame( $job, $job2, 'should not reuse instance' );
+ }
+
+ public function provideTestJobFactory() {
+ return [
+ 'class name' => [ 'NullJob' ],
+ 'closure' => [ function ( Title $title, array $params ) {
+ return new NullJob( $title, $params );
+ } ],
+ 'function' => [ [ $this, 'newNullJob' ] ],
+ 'static function' => [ self::class . '::staticNullJob' ]
+ ];
+ }
+
+ public function newNullJob( Title $title, array $params ) {
+ return new NullJob( $title, $params );
+ }
+
+ public static function staticNullJob( Title $title, array $params ) {
+ return new NullJob( $title, $params );
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php b/www/wiki/tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php
new file mode 100644
index 00000000..f874f6de
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * @group JobQueue
+ * @group medium
+ * @group Database
+ */
+class RefreshLinksPartitionTest extends MediaWikiTestCase {
+ public function __construct( $name = null, array $data = [], $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed[] = 'page';
+ $this->tablesUsed[] = 'revision';
+ $this->tablesUsed[] = 'pagelinks';
+ }
+
+ /**
+ * @dataProvider provider_backlinks
+ * @covers BacklinkJobUtils::partitionBacklinkJob
+ */
+ public function testRefreshLinks( $ns, $dbKey, $pages ) {
+ $title = Title::makeTitle( $ns, $dbKey );
+
+ foreach ( $pages as $page ) {
+ list( $bns, $bdbkey ) = $page;
+ $bpage = WikiPage::factory( Title::makeTitle( $bns, $bdbkey ) );
+ $content = ContentHandler::makeContent( "[[{$title->getPrefixedText()}]]", $bpage->getTitle() );
+ $bpage->doEditContent( $content, "test" );
+ }
+
+ $title->getBacklinkCache()->clear();
+ $this->assertEquals(
+ 20,
+ $title->getBacklinkCache()->getNumLinks( 'pagelinks' ),
+ 'Correct number of backlinks'
+ );
+
+ $job = new RefreshLinksJob( $title, [ 'recursive' => true, 'table' => 'pagelinks' ]
+ + Job::newRootJobParams( "refreshlinks:pagelinks:{$title->getPrefixedText()}" ) );
+ $extraParams = $job->getRootJobParams();
+ $jobs = BacklinkJobUtils::partitionBacklinkJob( $job, 9, 1, [ 'params' => $extraParams ] );
+
+ $this->assertEquals( 10, count( $jobs ), 'Correct number of sub-jobs' );
+ $this->assertEquals( $pages[0], current( $jobs[0]->params['pages'] ),
+ 'First job is leaf job with proper title' );
+ $this->assertEquals( $pages[8], current( $jobs[8]->params['pages'] ),
+ 'Last leaf job is leaf job with proper title' );
+ $this->assertEquals( true, isset( $jobs[9]->params['recursive'] ),
+ 'Last job is recursive sub-job' );
+ $this->assertEquals( true, $jobs[9]->params['recursive'],
+ 'Last job is recursive sub-job' );
+ $this->assertEquals( true, is_array( $jobs[9]->params['range'] ),
+ 'Last job is recursive sub-job' );
+ $this->assertEquals( $title->getPrefixedText(), $jobs[0]->getTitle()->getPrefixedText(),
+ 'Base job title retainend in leaf job' );
+ $this->assertEquals( $title->getPrefixedText(), $jobs[9]->getTitle()->getPrefixedText(),
+ 'Base job title retainend recursive sub-job' );
+ $this->assertEquals( $extraParams['rootJobSignature'], $jobs[0]->params['rootJobSignature'],
+ 'Leaf job has root params' );
+ $this->assertEquals( $extraParams['rootJobSignature'], $jobs[9]->params['rootJobSignature'],
+ 'Recursive sub-job has root params' );
+
+ $jobs2 = BacklinkJobUtils::partitionBacklinkJob(
+ $jobs[9],
+ 9,
+ 1,
+ [ 'params' => $extraParams ]
+ );
+
+ $this->assertEquals( 10, count( $jobs2 ), 'Correct number of sub-jobs' );
+ $this->assertEquals( $pages[9], current( $jobs2[0]->params['pages'] ),
+ 'First job is leaf job with proper title' );
+ $this->assertEquals( $pages[17], current( $jobs2[8]->params['pages'] ),
+ 'Last leaf job is leaf job with proper title' );
+ $this->assertEquals( true, isset( $jobs2[9]->params['recursive'] ),
+ 'Last job is recursive sub-job' );
+ $this->assertEquals( true, $jobs2[9]->params['recursive'],
+ 'Last job is recursive sub-job' );
+ $this->assertEquals( true, is_array( $jobs2[9]->params['range'] ),
+ 'Last job is recursive sub-job' );
+ $this->assertEquals( $extraParams['rootJobSignature'], $jobs2[0]->params['rootJobSignature'],
+ 'Leaf job has root params' );
+ $this->assertEquals( $extraParams['rootJobSignature'], $jobs2[9]->params['rootJobSignature'],
+ 'Recursive sub-job has root params' );
+
+ $jobs3 = BacklinkJobUtils::partitionBacklinkJob(
+ $jobs2[9],
+ 9,
+ 1,
+ [ 'params' => $extraParams ]
+ );
+
+ $this->assertEquals( 2, count( $jobs3 ), 'Correct number of sub-jobs' );
+ $this->assertEquals( $pages[18], current( $jobs3[0]->params['pages'] ),
+ 'First job is leaf job with proper title' );
+ $this->assertEquals( $extraParams['rootJobSignature'], $jobs3[0]->params['rootJobSignature'],
+ 'Leaf job has root params' );
+ $this->assertEquals( $pages[19], current( $jobs3[1]->params['pages'] ),
+ 'Last job is leaf job with proper title' );
+ $this->assertEquals( $extraParams['rootJobSignature'], $jobs3[1]->params['rootJobSignature'],
+ 'Last leaf job has root params' );
+ }
+
+ public static function provider_backlinks() {
+ $pages = [];
+ for ( $i = 0; $i < 20; ++$i ) {
+ $pages[] = [ 0, "Page-$i" ];
+ }
+ return [
+ [ 10, 'Bang', $pages ]
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/jobqueue/jobs/CategoryMembershipChangeJobTest.php b/www/wiki/tests/phpunit/includes/jobqueue/jobs/CategoryMembershipChangeJobTest.php
new file mode 100644
index 00000000..5960a16b
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/jobqueue/jobs/CategoryMembershipChangeJobTest.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @covers CategoryMembershipChangeJob
+ *
+ * @group JobQueue
+ * @group Database
+ *
+ * @license GNU GPL v2+
+ * @author Addshore
+ */
+class CategoryMembershipChangeJobTest extends MediaWikiTestCase {
+
+ const TITLE_STRING = 'UTCatChangeJobPage';
+
+ /**
+ * @var Title
+ */
+ private $title;
+
+ public function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( 'wgRCWatchCategoryMembership', true );
+ $this->setContentLang( 'qqx' );
+ }
+
+ public function addDBDataOnce() {
+ parent::addDBDataOnce();
+ $insertResult = $this->insertPage( self::TITLE_STRING, 'UT Content' );
+ $this->title = $insertResult['title'];
+ }
+
+ private function runJobs() {
+ JobQueueGroup::destroySingletons();
+ $jobs = new RunJobs;
+ $jobs->loadParamsAndArgs( null, [ 'quiet' => true ], null );
+ $jobs->execute();
+ }
+
+ /**
+ * @param string $text new page text
+ *
+ * @return int|null
+ */
+ private function editPageText( $text ) {
+ $page = WikiPage::factory( $this->title );
+ $editResult = $page->doEditContent(
+ ContentHandler::makeContent( $text, $this->title ),
+ __METHOD__
+ );
+ /** @var Revision $revision */
+ $revision = $editResult->value['revision'];
+ $this->runJobs();
+
+ return $revision->getId();
+ }
+
+ /**
+ * @param int $revId
+ *
+ * @return RecentChange|null
+ */
+ private function getCategorizeRecentChangeForRevId( $revId ) {
+ return RecentChange::newFromConds(
+ [
+ 'rc_type' => RC_CATEGORIZE,
+ 'rc_this_oldid' => $revId,
+ ],
+ __METHOD__
+ );
+ }
+
+ public function testRun_normalCategoryAddedAndRemoved() {
+ $addedRevId = $this->editPageText( '[[Category:Normal]]' );
+ $removedRevId = $this->editPageText( 'Blank' );
+
+ $this->assertEquals(
+ '(recentchanges-page-added-to-category: ' . self::TITLE_STRING . ')',
+ $this->getCategorizeRecentChangeForRevId( $addedRevId )->getAttribute( 'rc_comment' )
+ );
+ $this->assertEquals(
+ '(recentchanges-page-removed-from-category: ' . self::TITLE_STRING . ')',
+ $this->getCategorizeRecentChangeForRevId( $removedRevId )->getAttribute( 'rc_comment' )
+ );
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/jobqueue/jobs/ClearUserWatchlistJobTest.php b/www/wiki/tests/phpunit/includes/jobqueue/jobs/ClearUserWatchlistJobTest.php
new file mode 100644
index 00000000..6ae7d605
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/jobqueue/jobs/ClearUserWatchlistJobTest.php
@@ -0,0 +1,79 @@
+<?php
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @covers ClearUserWatchlistJob
+ *
+ * @group JobQueue
+ * @group Database
+ *
+ * @license GNU GPL v2+
+ * @author Addshore
+ */
+class ClearUserWatchlistJobTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ parent::setUp();
+ self::$users['ClearUserWatchlistJobTestUser']
+ = new TestUser( 'ClearUserWatchlistJobTestUser' );
+ $this->runJobs();
+ JobQueueGroup::destroySingletons();
+ }
+
+ private function getUser() {
+ return self::$users['ClearUserWatchlistJobTestUser']->getUser();
+ }
+
+ private function runJobs( $jobLimit = 9999 ) {
+ $runJobs = new RunJobs;
+ $runJobs->loadParamsAndArgs( null, [ 'quiet' => true, 'maxjobs' => $jobLimit ] );
+ $runJobs->execute();
+ }
+
+ private function getWatchedItemStore() {
+ return MediaWikiServices::getInstance()->getWatchedItemStore();
+ }
+
+ public function testRun() {
+ $user = $this->getUser();
+ $watchedItemStore = $this->getWatchedItemStore();
+
+ $watchedItemStore->addWatch( $user, new TitleValue( 0, 'A' ) );
+ $watchedItemStore->addWatch( $user, new TitleValue( 1, 'A' ) );
+ $watchedItemStore->addWatch( $user, new TitleValue( 0, 'B' ) );
+ $watchedItemStore->addWatch( $user, new TitleValue( 1, 'B' ) );
+
+ $maxId = $watchedItemStore->getMaxId();
+
+ $watchedItemStore->addWatch( $user, new TitleValue( 0, 'C' ) );
+ $watchedItemStore->addWatch( $user, new TitleValue( 1, 'C' ) );
+
+ $this->setMwGlobals( 'wgUpdateRowsPerQuery', 2 );
+
+ JobQueueGroup::singleton()->push(
+ new ClearUserWatchlistJob(
+ null,
+ [
+ 'userId' => $user->getId(),
+ 'maxWatchlistId' => $maxId,
+ ]
+ )
+ );
+
+ $this->assertEquals( 1, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] );
+ $this->assertEquals( 6, $watchedItemStore->countWatchedItems( $user ) );
+ $this->runJobs( 1 );
+ $this->assertEquals( 1, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] );
+ $this->assertEquals( 4, $watchedItemStore->countWatchedItems( $user ) );
+ $this->runJobs( 1 );
+ $this->assertEquals( 1, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] );
+ $this->assertEquals( 2, $watchedItemStore->countWatchedItems( $user ) );
+ $this->runJobs( 1 );
+ $this->assertEquals( 0, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] );
+ $this->assertEquals( 2, $watchedItemStore->countWatchedItems( $user ) );
+
+ $this->assertTrue( $watchedItemStore->isWatched( $user, new TitleValue( 0, 'C' ) ) );
+ $this->assertTrue( $watchedItemStore->isWatched( $user, new TitleValue( 1, 'C' ) ) );
+ }
+
+}