Page MenuHomePhorge

No OneTemporary

Size
12 KB
Referenced Files
None
Subscribers
None
diff --git a/includes/specialpage/QueryPage.php b/includes/specialpage/QueryPage.php
index 6463000f308..606676b6109 100644
--- a/includes/specialpage/QueryPage.php
+++ b/includes/specialpage/QueryPage.php
@@ -126,8 +126,8 @@ abstract class QueryPage extends SpecialPage {
* Get a list of query page classes and their associated special pages,
* for periodic updates.
*
- * DO NOT CHANGE THIS LIST without testing that
- * maintenance/updateSpecialPages.php still works.
+ * When changing this list, you should ensure that maintenance/updateSpecialPages.php still works
+ * including when test data exists.
*
* @return array[] List of [ string $class, string $specialPageName, ?int $limit (optional) ].
* Limit defaults to $wgQueryCacheLimit if not given.
diff --git a/maintenance/updateSpecialPages.php b/maintenance/updateSpecialPages.php
index 45f4b9728ca..2e7586f71c4 100644
--- a/maintenance/updateSpecialPages.php
+++ b/maintenance/updateSpecialPages.php
@@ -94,18 +94,7 @@ class UpdateSpecialPages extends Maintenance {
$this->output( "FAILED: database error\n" );
} else {
$this->output( "got $num rows in " );
-
- $elapsed = $t2 - $t1;
- $hours = intval( $elapsed / 3600 );
- $minutes = intval( (int)$elapsed % 3600 / 60 );
- $seconds = $elapsed - $hours * 3600 - $minutes * 60;
- if ( $hours ) {
- $this->output( $hours . 'h ' );
- }
- if ( $minutes ) {
- $this->output( $minutes . 'm ' );
- }
- $this->output( sprintf( "%.2fs\n", $seconds ) );
+ $this->outputElapsedTime( $t2 - $t1 );
}
# Reopen any connections that have closed
$this->reopenAndWaitForReplicas();
@@ -137,6 +126,9 @@ class UpdateSpecialPages extends Maintenance {
$lbFactory = $this->getServiceContainer()->getDBLoadBalancerFactory();
$lb = $lbFactory->getMainLB();
if ( !$lb->pingAll() ) {
+ // We don't want the tests to sleep for 10 seconds, so mark this as ignored because there is no reason to
+ // test it.
+ // @codeCoverageIgnoreStart
$this->output( "\n" );
do {
$this->error( "Connection failed, reconnecting in 10 seconds..." );
@@ -144,6 +136,7 @@ class UpdateSpecialPages extends Maintenance {
$this->waitForReplication();
} while ( !$lb->pingAll() );
$this->output( "Reconnected\n\n" );
+ // @codeCoverageIgnoreEnd
}
$this->waitForReplication();
}
@@ -157,32 +150,43 @@ class UpdateSpecialPages extends Maintenance {
}
if ( !$this->hasOption( 'only' ) || $this->getOption( 'only' ) === $special ) {
+ $this->output( sprintf( '%-30s [callback] ', $special ) );
if ( !is_callable( $call ) ) {
$this->error( "Uncallable function $call!" );
continue;
}
- $this->output( sprintf( '%-30s [callback] ', $special ) );
$t1 = microtime( true );
$call( $dbw );
$t2 = microtime( true );
$this->output( "completed in " );
- $elapsed = $t2 - $t1;
- $hours = intval( $elapsed / 3600 );
- $minutes = intval( (int)$elapsed % 3600 / 60 );
- $seconds = $elapsed - $hours * 3600 - $minutes * 60;
- if ( $hours ) {
- $this->output( $hours . 'h ' );
- }
- if ( $minutes ) {
- $this->output( $minutes . 'm ' );
- }
- $this->output( sprintf( "%.2fs\n", $seconds ) );
+ $this->outputElapsedTime( $t2 - $t1 );
+
# Wait for the replica DB to catch up
$this->reopenAndWaitForReplicas();
}
}
}
+
+ /**
+ * Outputs the time that was elapsed to update the cache update for
+ * a script.
+ *
+ * @param float $elapsed
+ * @return void
+ */
+ private function outputElapsedTime( float $elapsed ) {
+ $hours = intval( $elapsed / 3600 );
+ $minutes = intval( (int)$elapsed % 3600 / 60 );
+ $seconds = $elapsed - $hours * 3600 - $minutes * 60;
+ if ( $hours ) {
+ $this->output( $hours . 'h ' );
+ }
+ if ( $minutes ) {
+ $this->output( $minutes . 'm ' );
+ }
+ $this->output( sprintf( "%.2fs\n", $seconds ) );
+ }
}
// @codeCoverageIgnoreStart
diff --git a/tests/phpunit/maintenance/UpdateSpecialPagesTest.php b/tests/phpunit/maintenance/UpdateSpecialPagesTest.php
new file mode 100644
index 00000000000..8d9a110fea1
--- /dev/null
+++ b/tests/phpunit/maintenance/UpdateSpecialPagesTest.php
@@ -0,0 +1,230 @@
+<?php
+
+namespace MediaWiki\Tests\Maintenance;
+
+use MediaWiki\MainConfigNames;
+use MediaWiki\SpecialPage\QueryPage;
+use MediaWiki\SpecialPage\SpecialPage;
+use MediaWiki\SpecialPage\SpecialPageFactory;
+use MediaWiki\Tests\User\TempUser\TempUserTestTrait;
+use UpdateSpecialPages;
+use Wikimedia\TestingAccessWrapper;
+
+class MockSpecialPageForUpdateSpecialPagesTest extends SpecialPage {
+
+}
+
+/**
+ * @covers \UpdateSpecialPages
+ * @group Database
+ * @group Maintenance
+ */
+class UpdateSpecialPagesTest extends MaintenanceBaseTestCase {
+ use TempUserTestTrait;
+
+ public function getMaintenanceClass() {
+ return UpdateSpecialPages::class;
+ }
+
+ public function testExecuteForList() {
+ $this->maintenance->setOption( 'list', 1 );
+ $this->maintenance->execute();
+
+ $actualOutput = $this->getActualOutputForAssertion();
+ $this->assertStringContainsString( 'Statistics [callback]', $actualOutput );
+ $this->assertStringContainsString( 'Uncategorizedcategories [QueryPage]', $actualOutput );
+ $this->assertStringContainsString( 'BrokenRedirects [QueryPage]', $actualOutput );
+ }
+
+ public function testExecuteWhenAllQueryPagesDisabled() {
+ $this->overrideConfigValue(
+ MainConfigNames::DisableQueryPageUpdate,
+ array_column( QueryPage::getPages(), 1 )
+ );
+ $this->maintenance->execute();
+
+ $actualOutput = $this->getActualOutputForAssertion();
+ $this->assertMatchesRegularExpression(
+ '/Statistics[\s\S]*\[callback] completed in/', $actualOutput
+ );
+ $this->assertMatchesRegularExpression(
+ '/Uncategorizedcategories[\s\S]*\[QueryPage] disabled/', $actualOutput
+ );
+ $this->assertMatchesRegularExpression(
+ '/BrokenRedirects[\s\S]*\[QueryPage] disabled/', $actualOutput
+ );
+ }
+
+ public function testExecuteForAllUpdates() {
+ $this->maintenance->execute();
+
+ $actualOutput = $this->getActualOutputForAssertion();
+ $this->assertMatchesRegularExpression(
+ '/Statistics[\s\S]*\[callback] completed in/', $actualOutput
+ );
+ $this->assertMatchesRegularExpression(
+ '/Uncategorizedcategories[\s\S]*\[QueryPage] got 0 rows in/', $actualOutput
+ );
+ $this->assertMatchesRegularExpression(
+ '/BrokenRedirects[\s\S]*\[QueryPage] got 0 rows in/', $actualOutput
+ );
+ $this->assertMatchesRegularExpression(
+ '/MIMEsearch[\s\S]*\[QueryPage] cheap, skipped/', $actualOutput
+ );
+ $this->assertStringNotContainsString( 'No such special page', $actualOutput );
+ $this->assertStringNotContainsString( 'is not an instance of QueryPage', $actualOutput );
+ }
+
+ /**
+ * Installs a mock SpecialPageFactory that mocks ::getPage to return the specified
+ * value if the special page is "Ancientpages" (chosen at random) and otherwise
+ * use the real service.
+ *
+ * We cannot easily mock the return value of QueryPages::getPages as it uses a static variable
+ * in the method to cache the calls, so cannot be reset between tests.
+ *
+ * @param SpecialPage|null $ancientPagesReturnValue
+ * @return void
+ */
+ private function installMockSpecialPageFactory( ?SpecialPage $ancientPagesReturnValue ) {
+ $realSpecialPageFactory = $this->getServiceContainer()->getSpecialPageFactory();
+ $mockSpecialPageFactory = $this->createMock( SpecialPageFactory::class );
+ $mockSpecialPageFactory->method( 'getPage' )
+ ->willReturnCallback( static function ( $name ) use ( $realSpecialPageFactory, $ancientPagesReturnValue ) {
+ if ( $name === 'Ancientpages' ) {
+ return $ancientPagesReturnValue;
+ } else {
+ return $realSpecialPageFactory->getPage( $name );
+ }
+ } );
+ $this->setService( 'SpecialPageFactory', $mockSpecialPageFactory );
+ }
+
+ public function testExecuteWhenListIncludesMissingSpecialPage() {
+ $this->installMockSpecialPageFactory( null );
+ $this->maintenance->execute();
+
+ $actualOutput = $this->getActualOutputForAssertion();
+ $this->assertStringContainsString( "No such special page: Ancientpages", $actualOutput );
+ }
+
+ public function testExecuteWhenListIncludesSpecialPageThatDoesNotExtendQueryPage() {
+ $this->installMockSpecialPageFactory( new MockSpecialPageForUpdateSpecialPagesTest() );
+
+ $this->expectOutputRegex(
+ '/MockSpecialPageForUpdateSpecialPagesTest is not an instance of QueryPage/'
+ );
+ $this->expectCallToFatalError();
+ $this->maintenance->execute();
+ }
+
+ public function testExecuteWhenQueryPageIsCheapButDataExisted() {
+ $mockQueryPage = $this->createMock( QueryPage::class );
+ $mockQueryPage->method( 'getCachedTimestamp' )
+ ->willReturn( '20240506070809' );
+ $mockQueryPage->method( 'isExpensive' )
+ ->willReturn( false );
+ $mockQueryPage->expects( $this->once() )
+ ->method( 'deleteAllCachedData' );
+
+ $this->installMockSpecialPageFactory( $mockQueryPage );
+
+ $this->maintenance->execute();
+
+ $actualOutput = $this->getActualOutputForAssertion();
+ $this->assertMatchesRegularExpression(
+ '/Ancientpages[\s\S]*\[QueryPage] cheap, but deleted cached data/', $actualOutput
+ );
+ }
+
+ public function testExecuteWhenQueryPageIsExpensiveAndQueryFails() {
+ $mockQueryPage = $this->createMock( QueryPage::class );
+ $mockQueryPage->method( 'isExpensive' )
+ ->willReturn( true );
+ $mockQueryPage->method( 'getName' )
+ ->willReturn( 'Ancientpages' );
+ $mockQueryPage->expects( $this->once() )
+ ->method( 'recache' )
+ ->willReturn( false );
+
+ $this->installMockSpecialPageFactory( $mockQueryPage );
+
+ $this->maintenance->setOption( 'only', 'Ancientpages' );
+ $this->maintenance->execute();
+
+ $actualOutput = $this->getActualOutputForAssertion();
+ $this->assertMatchesRegularExpression(
+ '/Ancientpages[\s\S]*\[QueryPage] FAILED: database error/', $actualOutput
+ );
+ $this->assertStringNotContainsString( 'Mostimages', $actualOutput );
+ }
+
+ public function testExecuteWhenQueryPageIsExpensiveAndQuerySucceeds() {
+ $mockQueryPage = $this->createMock( QueryPage::class );
+ $mockQueryPage->method( 'isExpensive' )
+ ->willReturn( true );
+ $mockQueryPage->expects( $this->once() )
+ ->method( 'recache' )
+ ->willReturn( 123456 );
+
+ $this->installMockSpecialPageFactory( $mockQueryPage );
+
+ $this->maintenance->execute();
+
+ $actualOutput = $this->getActualOutputForAssertion();
+ $this->assertMatchesRegularExpression(
+ '/Ancientpages[\s\S]*\[QueryPage] got 123456 rows in \d*.\d\ds/', $actualOutput
+ );
+ }
+
+ public function testExecuteWhenSpecialPageCacheUpdateCallbackIsNotCallable() {
+ $this->overrideConfigValue(
+ MainConfigNames::SpecialPageCacheUpdates,
+ [ 'Statistics' => 'nonExistingTestFunction' ]
+ );
+
+ $this->maintenance->execute();
+
+ $actualOutput = $this->getActualOutputForAssertion();
+ $this->assertMatchesRegularExpression(
+ '/Statistics[\s\S]*\[callback] Uncallable function nonExistingTestFunction!/', $actualOutput
+ );
+ }
+
+ public function testExecuteWhenSpecialPageCacheUpdate() {
+ $callbackCalled = false;
+ $this->overrideConfigValue(
+ MainConfigNames::SpecialPageCacheUpdates,
+ [
+ 'Statistics' => static function () use ( &$callbackCalled ) {
+ $callbackCalled = true;
+ },
+ ]
+ );
+
+ $this->maintenance->execute();
+
+ $actualOutput = $this->getActualOutputForAssertion();
+ $this->assertMatchesRegularExpression(
+ '/Statistics[\s\S]*\[callback] completed in \d*.\d\ds/', $actualOutput
+ );
+ $this->assertTrue( $callbackCalled, 'Callback not called to update special page cache' );
+ }
+
+ /** @dataProvider provideOutputElapsedTime */
+ public function testOutputElapsedTime( float $elapsedTime, string $expectedOutputString ) {
+ /** @var TestingAccessWrapper $maintenance */
+ $maintenance = $this->maintenance;
+ $maintenance->outputElapsedTime( $elapsedTime );
+ $this->expectOutputString( $expectedOutputString );
+ }
+
+ public static function provideOutputElapsedTime(): array {
+ return [
+ 'Elapsed time is 0' => [ 0, "0.00s\n" ],
+ 'Elapsed time is 11.3 seconds' => [ 11.3, "11.30s\n" ],
+ 'Elapsed time is 3 minutes and 5 seconds' => [ 3 * 60 + 5, "3m 5.00s\n" ],
+ 'Elapsed time is 3 hours, 5 minutes, and 7.334 seconds' => [ 3 * 3600 + 5 * 60 + 7.33, "3h 5m 7.33s\n" ],
+ ];
+ }
+}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Jul 5, 5:32 AM (11 h, 54 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
227502
Default Alt Text
(12 KB)

Event Timeline