diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/jobqueue')
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' ) ) ); + } + +} |