diff options
Diffstat (limited to 'www/wiki/tests/phpunit/maintenance')
11 files changed, 3478 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/maintenance/BenchmarkerTest.php b/www/wiki/tests/phpunit/maintenance/BenchmarkerTest.php new file mode 100644 index 00000000..c15d789d --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/BenchmarkerTest.php @@ -0,0 +1,142 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use Benchmarker; +use MediaWikiCoversValidator; +use Wikimedia\TestingAccessWrapper; + +/** + * @covers Benchmarker + */ +class BenchmarkerTest extends \PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + public function testBenchSimple() { + $bench = $this->getMockBuilder( Benchmarker::class ) + ->setMethods( [ 'execute', 'output' ] ) + ->getMock(); + $benchProxy = TestingAccessWrapper::newFromObject( $bench ); + $benchProxy->defaultCount = 3; + + $count = 0; + $bench->bench( [ + 'test' => function () use ( &$count ) { + $count++; + } + ] ); + + $this->assertSame( 3, $count ); + } + + public function testBenchSetup() { + $bench = $this->getMockBuilder( Benchmarker::class ) + ->setMethods( [ 'execute', 'output' ] ) + ->getMock(); + $benchProxy = TestingAccessWrapper::newFromObject( $bench ); + $benchProxy->defaultCount = 2; + + $buffer = []; + $bench->bench( [ + 'test' => [ + 'setup' => function () use ( &$buffer ) { + $buffer[] = 'setup'; + }, + 'function' => function () use ( &$buffer ) { + $buffer[] = 'run'; + } + ] + ] ); + + $this->assertSame( [ 'setup', 'run', 'run' ], $buffer ); + } + + public function testBenchVerbose() { + $bench = $this->getMockBuilder( Benchmarker::class ) + ->setMethods( [ 'execute', 'output', 'hasOption', 'verboseRun' ] ) + ->getMock(); + $benchProxy = TestingAccessWrapper::newFromObject( $bench ); + $benchProxy->defaultCount = 1; + + $bench->expects( $this->exactly( 2 ) )->method( 'hasOption' ) + ->will( $this->returnValueMap( [ + [ 'verbose', true ], + [ 'count', false ], + ] ) ); + + $bench->expects( $this->once() )->method( 'verboseRun' ) + ->with( 0 ) + ->willReturn( null ); + + $bench->bench( [ + 'test' => function () { + } + ] ); + } + + public function noop() { + } + + public function testBenchName_method() { + $bench = $this->getMockBuilder( Benchmarker::class ) + ->setMethods( [ 'execute', 'output', 'addResult' ] ) + ->getMock(); + $benchProxy = TestingAccessWrapper::newFromObject( $bench ); + $benchProxy->defaultCount = 1; + + $bench->expects( $this->once() )->method( 'addResult' ) + ->with( $this->callback( function ( $res ) { + return isset( $res['name'] ) && $res['name'] === __CLASS__ . '::noop()'; + } ) ); + + $bench->bench( [ + [ 'function' => [ $this, 'noop' ] ] + ] ); + } + + public function testBenchName_string() { + $bench = $this->getMockBuilder( Benchmarker::class ) + ->setMethods( [ 'execute', 'output', 'addResult' ] ) + ->getMock(); + $benchProxy = TestingAccessWrapper::newFromObject( $bench ); + $benchProxy->defaultCount = 1; + + $bench->expects( $this->once() )->method( 'addResult' ) + ->with( $this->callback( function ( $res ) { + return 'strtolower(A)'; + } ) ); + + $bench->bench( [ [ + 'function' => 'strtolower', + 'args' => [ 'A' ], + ] ] ); + } + + /** + * @covers Benchmarker::verboseRun + */ + public function testVerboseRun() { + $bench = $this->getMockBuilder( Benchmarker::class ) + ->setMethods( [ 'execute', 'output', 'hasOption', 'startBench', 'addResult' ] ) + ->getMock(); + $benchProxy = TestingAccessWrapper::newFromObject( $bench ); + $benchProxy->defaultCount = 1; + + $bench->expects( $this->exactly( 2 ) )->method( 'hasOption' ) + ->will( $this->returnValueMap( [ + [ 'verbose', true ], + [ 'count', false ], + ] ) ); + + $bench->expects( $this->once() )->method( 'output' ) + ->with( $this->callback( function ( $out ) { + return preg_match( '/memory.+ peak/', $out ) === 1; + } ) ); + + $bench->bench( [ + 'test' => function () { + } + ] ); + } +} diff --git a/www/wiki/tests/phpunit/maintenance/DumpTestCase.php b/www/wiki/tests/phpunit/maintenance/DumpTestCase.php new file mode 100644 index 00000000..9b90bfe6 --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/DumpTestCase.php @@ -0,0 +1,417 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use ContentHandler; +use ExecutableFinder; +use MediaWikiLangTestCase; +use Page; +use User; +use XMLReader; +use MWException; + +/** + * Base TestCase for dumps + */ +abstract class DumpTestCase extends MediaWikiLangTestCase { + + /** + * exception to be rethrown once in sound PHPUnit surrounding + * + * As the current MediaWikiTestCase::run is not robust enough to recover + * from thrown exceptions directly, we cannot throw frow within + * self::addDBData, although it would be appropriate. Hence, we catch the + * exception and store it until we are in setUp and may finally rethrow + * the exception without crashing the test suite. + * + * @var Exception|null + */ + protected $exceptionFromAddDBData = null; + + /** + * Holds the XMLReader used for analyzing an XML dump + * + * @var XMLReader|null + */ + protected $xml = null; + + /** @var bool|null Whether the 'gzip' utility is available */ + protected static $hasGzip = null; + + /** + * Skip the test if 'gzip' is not in $PATH. + * + * @return bool + */ + protected function checkHasGzip() { + if ( self::$hasGzip === null ) { + self::$hasGzip = ( ExecutableFinder::findInDefaultPaths( 'gzip' ) !== false ); + } + + if ( !self::$hasGzip ) { + $this->markTestSkipped( "Skip test, requires the gzip utility in PATH" ); + } + + return self::$hasGzip; + } + + /** + * Adds a revision to a page, while returning the resuting revision's id + * + * @param Page $page Page to add the revision to + * @param string $text Revisions text + * @param string $summary Revisions summary + * @param string $model The model ID (defaults to wikitext) + * + * @throws MWException + * @return array + */ + protected function addRevision( Page $page, $text, $summary, $model = CONTENT_MODEL_WIKITEXT ) { + $status = $page->doEditContent( + ContentHandler::makeContent( $text, $page->getTitle(), $model ), + $summary + ); + + if ( $status->isGood() ) { + $value = $status->getValue(); + $revision = $value['revision']; + $revision_id = $revision->getId(); + $text_id = $revision->getTextId(); + + if ( ( $revision_id > 0 ) && ( $text_id > 0 ) ) { + return [ $revision_id, $text_id ]; + } + } + + throw new MWException( "Could not determine revision id (" + . $status->getWikiText( false, false, 'en' ) . ")" ); + } + + /** + * gunzips the given file and stores the result in the original file name + * + * @param string $fname Filename to read the gzipped data from and stored + * the gunzipped data into + */ + protected function gunzip( $fname ) { + $gzipped_contents = file_get_contents( $fname ); + if ( $gzipped_contents === false ) { + $this->fail( "Could not get contents of $fname" ); + } + + $contents = gzdecode( $gzipped_contents ); + + $this->assertEquals( + strlen( $contents ), + file_put_contents( $fname, $contents ), + '# bytes written' + ); + } + + /** + * Default set up function. + * + * Clears $wgUser, and reports errors from addDBData to PHPUnit + */ + protected function setUp() { + parent::setUp(); + + // Check if any Exception is stored for rethrowing from addDBData + // @see self::exceptionFromAddDBData + if ( $this->exceptionFromAddDBData !== null ) { + throw $this->exceptionFromAddDBData; + } + + $this->setMwGlobals( 'wgUser', new User() ); + } + + /** + * Checks for test output consisting only of lines containing ETA announcements + */ + function expectETAOutput() { + // Newer PHPUnits require assertion about the output using PHPUnit's own + // expectOutput[...] functions. However, the PHPUnit shipped prediactes + // do not allow to check /each/ line of the output using /readable/ REs. + // So we ... + + // 1. ... add a dummy output checking to make PHPUnit not complain + // about unchecked test output + $this->expectOutputRegex( '//' ); + + // 2. Do the real output checking on our own. + $lines = explode( "\n", $this->getActualOutput() ); + $this->assertGreaterThan( 1, count( $lines ), "Minimal lines of produced output" ); + $this->assertEquals( '', array_pop( $lines ), "Output ends in LF" ); + $timestamp_re = "[0-9]{4}-[01][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-6][0-9]"; + foreach ( $lines as $line ) { + $this->assertRegExp( + "/$timestamp_re: .* \(ID [0-9]+\) [0-9]* pages .*, [0-9]* revs .*, ETA/", + $line + ); + } + } + + /** + * Step the current XML reader until node end of given name is found. + * + * @param string $name Name of the closing element to look for + * (e.g.: "mediawiki" when looking for </mediawiki>) + * + * @return bool True if the end node could be found. false otherwise. + */ + protected function skipToNodeEnd( $name ) { + while ( $this->xml->read() ) { + if ( $this->xml->nodeType == XMLReader::END_ELEMENT && + $this->xml->name == $name + ) { + return true; + } + } + + return false; + } + + /** + * Step the current XML reader to the first element start after the node + * end of a given name. + * + * @param string $name Name of the closing element to look for + * (e.g.: "mediawiki" when looking for </mediawiki>) + * + * @return bool True if new element after the closing of $name could be + * found. false otherwise. + */ + protected function skipPastNodeEnd( $name ) { + $this->assertTrue( $this->skipToNodeEnd( $name ), + "Skipping to end of $name" ); + while ( $this->xml->read() ) { + if ( $this->xml->nodeType == XMLReader::ELEMENT ) { + return true; + } + } + + return false; + } + + /** + * Opens an XML file to analyze and optionally skips past siteinfo. + * + * @param string $fname Name of file to analyze + * @param bool $skip_siteinfo (optional) If true, step the xml reader + * to the first element after </siteinfo> + */ + protected function assertDumpStart( $fname, $skip_siteinfo = true ) { + $this->xml = new XMLReader(); + $this->assertTrue( $this->xml->open( $fname ), + "Opening temporary file $fname via XMLReader failed" ); + if ( $skip_siteinfo ) { + $this->assertTrue( $this->skipPastNodeEnd( "siteinfo" ), + "Skipping past end of siteinfo" ); + } + } + + /** + * Asserts that the xml reader is at the final closing tag of an xml file and + * closes the reader. + * + * @param string $name (optional) the name of the final tag + * (e.g.: "mediawiki" for </mediawiki>) + */ + protected function assertDumpEnd( $name = "mediawiki" ) { + $this->assertNodeEnd( $name, false ); + if ( $this->xml->read() ) { + $this->skipWhitespace(); + } + $this->assertEquals( $this->xml->nodeType, XMLReader::NONE, + "No proper entity left to parse" ); + $this->xml->close(); + } + + /** + * Steps the xml reader over white space + */ + protected function skipWhitespace() { + $cont = true; + while ( $cont && ( ( $this->xml->nodeType == XMLReader::WHITESPACE ) + || ( $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE ) ) ) { + $cont = $this->xml->read(); + } + } + + /** + * Asserts that the xml reader is at an element of given name, and optionally + * skips past it. + * + * @param string $name The name of the element to check for + * (e.g.: "mediawiki" for <mediawiki>) + * @param bool $skip (optional) if true, skip past the found element + */ + protected function assertNodeStart( $name, $skip = true ) { + $this->assertEquals( $name, $this->xml->name, "Node name" ); + $this->assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" ); + if ( $skip ) { + $this->assertTrue( $this->xml->read(), "Skipping past start tag" ); + } + } + + /** + * Asserts that the xml reader is at an closing element of given name, and optionally + * skips past it. + * + * @param string $name The name of the closing element to check for + * (e.g.: "mediawiki" for </mediawiki>) + * @param bool $skip (optional) if true, skip past the found element + */ + protected function assertNodeEnd( $name, $skip = true ) { + $this->assertEquals( $name, $this->xml->name, "Node name" ); + $this->assertEquals( XMLReader::END_ELEMENT, $this->xml->nodeType, "Node type" ); + if ( $skip ) { + $this->assertTrue( $this->xml->read(), "Skipping past end tag" ); + } + } + + /** + * Asserts that the xml reader is at an element of given tag that contains a given text, + * and skips over the element. + * + * @param string $name The name of the element to check for + * (e.g.: "mediawiki" for <mediawiki>...</mediawiki>) + * @param string|bool $text If string, check if it equals the elements text. + * If false, ignore the element's text + * @param bool $skip_ws (optional) if true, skip past white spaces that trail the + * closing element. + */ + protected function assertTextNode( $name, $text, $skip_ws = true ) { + $this->assertNodeStart( $name ); + + if ( $text !== false ) { + $this->assertEquals( $text, $this->xml->value, "Text of node " . $name ); + } + $this->assertTrue( $this->xml->read(), "Skipping past processed text of " . $name ); + $this->assertNodeEnd( $name ); + + if ( $skip_ws ) { + $this->skipWhitespace(); + } + } + + /** + * Asserts that the xml reader is at the start of a page element and skips over the first + * tags, after checking them. + * + * Besides the opening page element, this function also checks for and skips over the + * title, ns, and id tags. Hence after this function, the xml reader is at the first + * revision of the current page. + * + * @param int $id Id of the page to assert + * @param int $ns Number of namespage to assert + * @param string $name Title of the current page + */ + protected function assertPageStart( $id, $ns, $name ) { + $this->assertNodeStart( "page" ); + $this->skipWhitespace(); + + $this->assertTextNode( "title", $name ); + $this->assertTextNode( "ns", $ns ); + $this->assertTextNode( "id", $id ); + } + + /** + * Asserts that the xml reader is at the page's closing element and skips to the next + * element. + */ + protected function assertPageEnd() { + $this->assertNodeEnd( "page" ); + $this->skipWhitespace(); + } + + /** + * Asserts that the xml reader is at a revision and checks its representation before + * skipping over it. + * + * @param int $id Id of the revision + * @param string $summary Summary of the revision + * @param int $text_id Id of the revision's text + * @param int $text_bytes Number of bytes in the revision's text + * @param string $text_sha1 The base36 SHA-1 of the revision's text + * @param string|bool $text (optional) The revision's string, or false to check for a + * revision stub + * @param int|bool $parentid (optional) id of the parent revision + * @param string $model The expected content model id (default: CONTENT_MODEL_WIKITEXT) + * @param string $format The expected format model id (default: CONTENT_FORMAT_WIKITEXT) + */ + protected function assertRevision( $id, $summary, $text_id, $text_bytes, + $text_sha1, $text = false, $parentid = false, + $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT + ) { + $this->assertNodeStart( "revision" ); + $this->skipWhitespace(); + + $this->assertTextNode( "id", $id ); + if ( $parentid !== false ) { + $this->assertTextNode( "parentid", $parentid ); + } + $this->assertTextNode( "timestamp", false ); + + $this->assertNodeStart( "contributor" ); + $this->skipWhitespace(); + $this->assertTextNode( "ip", false ); + $this->assertNodeEnd( "contributor" ); + $this->skipWhitespace(); + + $this->assertTextNode( "comment", $summary ); + $this->skipWhitespace(); + + $this->assertTextNode( "model", $model ); + $this->skipWhitespace(); + + $this->assertTextNode( "format", $format ); + $this->skipWhitespace(); + + if ( $this->xml->name == "text" ) { + // note: <text> tag may occur here or at the very end. + $text_found = true; + $this->assertText( $id, $text_id, $text_bytes, $text ); + } else { + $text_found = false; + } + + $this->assertTextNode( "sha1", $text_sha1 ); + + if ( !$text_found ) { + $this->assertText( $id, $text_id, $text_bytes, $text ); + } + + $this->assertNodeEnd( "revision" ); + $this->skipWhitespace(); + } + + protected function assertText( $id, $text_id, $text_bytes, $text ) { + $this->assertNodeStart( "text", false ); + if ( $text_bytes !== false ) { + $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes, + "Attribute 'bytes' of revision " . $id ); + } + + if ( $text === false ) { + // Testing for a stub + $this->assertEquals( $this->xml->getAttribute( "id" ), $text_id, + "Text id of revision " . $id ); + $this->assertFalse( $this->xml->hasValue, "Revision has text" ); + $this->assertTrue( $this->xml->read(), "Skipping text start tag" ); + if ( ( $this->xml->nodeType == XMLReader::END_ELEMENT ) + && ( $this->xml->name == "text" ) + ) { + $this->xml->read(); + } + $this->skipWhitespace(); + } else { + // Testing for a real dump + $this->assertTrue( $this->xml->read(), "Skipping text start tag" ); + $this->assertEquals( $text, $this->xml->value, "Text of revision " . $id ); + $this->assertTrue( $this->xml->read(), "Skipping past text" ); + $this->assertNodeEnd( "text" ); + $this->skipWhitespace(); + } + } +} diff --git a/www/wiki/tests/phpunit/maintenance/MaintenanceBaseTestCase.php b/www/wiki/tests/phpunit/maintenance/MaintenanceBaseTestCase.php new file mode 100644 index 00000000..bdcf7e5f --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/MaintenanceBaseTestCase.php @@ -0,0 +1,93 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use Maintenance; +use MediaWikiTestCase; +use Wikimedia\TestingAccessWrapper; + +abstract class MaintenanceBaseTestCase extends MediaWikiTestCase { + + /** + * The main Maintenance instance that is used for testing, wrapped and mockable. + * + * @var Maintenance + */ + protected $maintenance; + + protected function setUp() { + parent::setUp(); + + $this->maintenance = $this->createMaintenance(); + } + + /** + * Do a little stream cleanup to prevent output in case the child class + * hasn't tested the capture buffer. + */ + protected function tearDown() { + if ( $this->maintenance ) { + $this->maintenance->cleanupChanneled(); + } + + // This is smelly, but maintenance scripts usually produce output, so + // we anticipate and ignore with a regex that will catch everything. + // + // If you call $this->expectOutputRegex in your subclass, this guard + // won't be triggered, and your specific pattern will be respected. + if ( !$this->hasExpectationOnOutput() ) { + $this->expectOutputRegex( '/.*/' ); + } + + parent::tearDown(); + } + + /** + * @return string Class name + * + * Subclasses must implement this in order to use the $this->maintenance + * variable. Normally, it will be set like: + * return PopulateDatabaseMaintenance::class; + * + * If you need to change the way your maintenance class is constructed, + * override createMaintenance. + */ + abstract protected function getMaintenanceClass(); + + /** + * Called by setUp to initialize $this->maintenance. + * + * @return object The Maintenance instance to test. + */ + protected function createMaintenance() { + $className = $this->getMaintenanceClass(); + $obj = new $className(); + + // We use TestingAccessWrapper in order to access protected internals + // such as `output()`. + return TestingAccessWrapper::newFromObject( $obj ); + } + + /** + * Asserts the output before and after simulating shutdown + * + * This function simulates shutdown of self::maintenance. + * + * @param string $preShutdownOutput Expected output before simulating shutdown + * @param bool $expectNLAppending Whether or not shutdown simulation is expected + * to add a newline to the output. If false, $preShutdownOutput is the + * expected output after shutdown simulation. Otherwise, + * $preShutdownOutput with an appended newline is the expected output + * after shutdown simulation. + */ + protected function assertOutputPrePostShutdown( $preShutdownOutput, $expectNLAppending ) { + $this->assertEquals( $preShutdownOutput, $this->getActualOutput(), + "Output before shutdown simulation" ); + + $this->maintenance->cleanupChanneled(); + + $postShutdownOutput = $preShutdownOutput . ( $expectNLAppending ? "\n" : "" ); + $this->expectOutputString( $postShutdownOutput ); + } + +} diff --git a/www/wiki/tests/phpunit/maintenance/MaintenanceTest.php b/www/wiki/tests/phpunit/maintenance/MaintenanceTest.php new file mode 100644 index 00000000..141561f0 --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/MaintenanceTest.php @@ -0,0 +1,536 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use Maintenance; +use MediaWiki\MediaWikiServices; +use Wikimedia\TestingAccessWrapper; + +/** + * @covers Maintenance + */ +class MaintenanceTest extends MaintenanceBaseTestCase { + + /** + * @see MaintenanceBaseTestCase::getMaintenanceClass + */ + protected function getMaintenanceClass() { + return Maintenance::class; + } + + /** + * @see MaintenanceBaseTestCase::createMaintenance + * + * Note to extension authors looking for a model to follow: This function + * is normally not needed in a maintenance test, it's only overridden here + * because Maintenance is abstract. + */ + protected function createMaintenance() { + $className = $this->getMaintenanceClass(); + $obj = $this->getMockForAbstractClass( $className ); + + return TestingAccessWrapper::newFromObject( $obj ); + } + + // Although the following tests do not seem to be too consistent (compare for + // example the newlines within the test.*StringString tests, or the + // test.*Intermittent.* tests), the objective of these tests is not to describe + // consistent behavior, but rather currently existing behavior. + + /** + * @dataProvider provideOutputData + */ + function testOutput( $outputs, $expected, $extraNL ) { + foreach ( $outputs as $data ) { + if ( is_array( $data ) ) { + list( $msg, $channel ) = $data; + } else { + $msg = $data; + $channel = null; + } + $this->maintenance->output( $msg, $channel ); + } + $this->assertOutputPrePostShutdown( $expected, $extraNL ); + } + + public function provideOutputData() { + return [ + [ [ "" ], "", false ], + [ [ "foo" ], "foo", false ], + [ [ "foo", "bar" ], "foobar", false ], + [ [ "foo\n" ], "foo\n", false ], + [ [ "foo\n\n" ], "foo\n\n", false ], + [ [ "foo\nbar" ], "foo\nbar", false ], + [ [ "foo\nbar\n" ], "foo\nbar\n", false ], + [ [ "foo\n", "bar\n" ], "foo\nbar\n", false ], + [ [ "", "foo", "", "\n", "ba", "", "r\n" ], "foo\nbar\n", false ], + [ [ "", "foo", "", "\nb", "a", "", "r\n" ], "foo\nbar\n", false ], + [ [ [ "foo", "bazChannel" ] ], "foo", true ], + [ [ [ "foo\n", "bazChannel" ] ], "foo", true ], + + // If this test fails, note that output takes strings with double line + // endings (although output's implementation in this situation calls + // outputChanneled with a string ending in a nl ... which is not allowed + // according to the documentation of outputChanneled) + [ [ [ "foo\n\n", "bazChannel" ] ], "foo\n", true ], + [ [ [ "foo\nbar", "bazChannel" ] ], "foo\nbar", true ], + [ [ [ "foo\nbar\n", "bazChannel" ] ], "foo\nbar", true ], + [ + [ + [ "foo\n", "bazChannel" ], + [ "bar\n", "bazChannel" ], + ], + "foobar", + true + ], + [ + [ + [ "", "bazChannel" ], + [ "foo", "bazChannel" ], + [ "", "bazChannel" ], + [ "\n", "bazChannel" ], + [ "ba", "bazChannel" ], + [ "", "bazChannel" ], + [ "r\n", "bazChannel" ], + ], + "foobar", + true + ], + [ + [ + [ "", "bazChannel" ], + [ "foo", "bazChannel" ], + [ "", "bazChannel" ], + [ "\nb", "bazChannel" ], + [ "a", "bazChannel" ], + [ "", "bazChannel" ], + [ "r\n", "bazChannel" ], + ], + "foo\nbar", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", "bazChannel" ], + [ "qux", "quuxChannel" ], + [ "corge", "bazChannel" ], + ], + "foobar\nqux\ncorge", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar\n", "bazChannel" ], + [ "qux\n", "quuxChannel" ], + [ "corge", "bazChannel" ], + ], + "foobar\nqux\ncorge", + true + ], + [ + [ + [ "foo", null ], + [ "bar", "bazChannel" ], + [ "qux", null ], + [ "quux", "bazChannel" ], + ], + "foobar\nquxquux", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", null ], + [ "qux", "bazChannel" ], + [ "quux", null ], + ], + "foo\nbarqux\nquux", + false + ], + [ + [ + [ "foo", 1 ], + [ "bar", 1.0 ], + ], + "foo\nbar", + true + ], + [ [ "foo", "", "bar" ], "foobar", false ], + [ [ "foo", false, "bar" ], "foobar", false ], + [ + [ + [ "qux", "quuxChannel" ], + "foo", + false, + "bar" + ], + "qux\nfoobar", + false + ], + [ + [ + [ "foo", "bazChannel" ], + [ "", "bazChannel" ], + [ "bar", "bazChannel" ], + ], + "foobar", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ false, "bazChannel" ], + [ "bar", "bazChannel" ], + ], + "foobar", + true + ], + ]; + } + + /** + * @dataProvider provideOutputChanneledData + */ + function testOutputChanneled( $outputs, $expected, $extraNL ) { + foreach ( $outputs as $data ) { + if ( is_array( $data ) ) { + list( $msg, $channel ) = $data; + } else { + $msg = $data; + $channel = null; + } + $this->maintenance->outputChanneled( $msg, $channel ); + } + $this->assertOutputPrePostShutdown( $expected, $extraNL ); + } + + public function provideOutputChanneledData() { + return [ + [ [ "" ], "\n", false ], + [ [ "foo" ], "foo\n", false ], + [ [ "foo", "bar" ], "foo\nbar\n", false ], + [ [ "foo\nbar" ], "foo\nbar\n", false ], + [ [ "", "foo", "", "\nb", "a", "", "r" ], "\nfoo\n\n\nb\na\n\nr\n", false ], + [ [ [ "foo", "bazChannel" ] ], "foo", true ], + [ + [ + [ "foo\nbar", "bazChannel" ] + ], + "foo\nbar", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", "bazChannel" ], + ], + "foobar", + true + ], + [ + [ + [ "", "bazChannel" ], + [ "foo", "bazChannel" ], + [ "", "bazChannel" ], + [ "\nb", "bazChannel" ], + [ "a", "bazChannel" ], + [ "", "bazChannel" ], + [ "r", "bazChannel" ], + ], + "foo\nbar", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", "bazChannel" ], + [ "qux", "quuxChannel" ], + [ "corge", "bazChannel" ], + ], + "foobar\nqux\ncorge", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", "bazChannel" ], + [ "qux", "quuxChannel" ], + [ "corge", "bazChannel" ], + ], + "foobar\nqux\ncorge", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", null ], + [ "qux", null ], + [ "corge", "bazChannel" ], + ], + "foo\nbar\nqux\ncorge", + true + ], + [ + [ + [ "foo", null ], + [ "bar", "bazChannel" ], + [ "qux", null ], + [ "quux", "bazChannel" ], + ], + "foo\nbar\nqux\nquux", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", null ], + [ "qux", "bazChannel" ], + [ "quux", null ], + ], + "foo\nbar\nqux\nquux\n", + false + ], + [ + [ + [ "foo", 1 ], + [ "bar", 1.0 ], + ], + "foo\nbar", + true + ], + [ [ "foo", "", "bar" ], "foo\n\nbar\n", false ], + [ [ "foo", false, "bar" ], "foo\nbar\n", false ], + ]; + } + + function testCleanupChanneledClean() { + $this->maintenance->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "", false ); + } + + function testCleanupChanneledAfterOutput() { + $this->maintenance->output( "foo" ); + $this->maintenance->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testCleanupChanneledAfterOutputWNullChannel() { + $this->maintenance->output( "foo", null ); + $this->maintenance->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testCleanupChanneledAfterOutputWChannel() { + $this->maintenance->output( "foo", "bazChannel" ); + $this->maintenance->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterNLOutput() { + $this->maintenance->output( "foo\n" ); + $this->maintenance->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterNLOutputWNullChannel() { + $this->maintenance->output( "foo\n", null ); + $this->maintenance->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterNLOutputWChannel() { + $this->maintenance->output( "foo\n", "bazChannel" ); + $this->maintenance->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterOutputChanneledWOChannel() { + $this->maintenance->outputChanneled( "foo" ); + $this->maintenance->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterOutputChanneledWNullChannel() { + $this->maintenance->outputChanneled( "foo", null ); + $this->maintenance->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterOutputChanneledWChannel() { + $this->maintenance->outputChanneled( "foo", "bazChannel" ); + $this->maintenance->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutput() { + $m2 = $this->createMaintenance(); + + $this->maintenance->output( "foo" ); + $m2->output( "bar" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWNullChannel() { + $m2 = $this->createMaintenance(); + + $this->maintenance->output( "foo", null ); + $m2->output( "bar", null ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWChannel() { + $m2 = $this->createMaintenance(); + + $this->maintenance->output( "foo", "bazChannel" ); + $m2->output( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foobar\n", true ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWNullChannelNL() { + $m2 = $this->createMaintenance(); + + $this->maintenance->output( "foo\n", null ); + $m2->output( "bar\n", null ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWChannelNL() { + $m2 = $this->createMaintenance(); + + $this->maintenance->output( "foo\n", "bazChannel" ); + $m2->output( "bar\n", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foobar\n", true ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneled() { + $m2 = $this->createMaintenance(); + + $this->maintenance->outputChanneled( "foo" ); + $m2->outputChanneled( "bar" ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneledWNullChannel() { + $m2 = $this->createMaintenance(); + + $this->maintenance->outputChanneled( "foo", null ); + $m2->outputChanneled( "bar", null ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneledWChannel() { + $m2 = $this->createMaintenance(); + + $this->maintenance->outputChanneled( "foo", "bazChannel" ); + $m2->outputChanneled( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foobar\n", true ); + } + + function testMultipleMaintenanceObjectsInteractionCleanupChanneledWChannel() { + $m2 = $this->createMaintenance(); + + $this->maintenance->outputChanneled( "foo", "bazChannel" ); + $m2->outputChanneled( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before first cleanup" ); + $this->maintenance->cleanupChanneled(); + $this->assertEquals( "foobar\n", $this->getActualOutput(), + "Output after first cleanup" ); + $m2->cleanupChanneled(); + $this->assertEquals( "foobar\n\n", $this->getActualOutput(), + "Output after second cleanup" ); + + $m2->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foobar\n\n", false ); + } + + /** + * @covers Maintenance::getConfig + */ + public function testGetConfig() { + $this->assertInstanceOf( 'Config', $this->maintenance->getConfig() ); + $this->assertSame( + MediaWikiServices::getInstance()->getMainConfig(), + $this->maintenance->getConfig() + ); + } + + /** + * @covers Maintenance::setConfig + */ + public function testSetConfig() { + $conf = $this->createMock( 'Config' ); + $this->maintenance->setConfig( $conf ); + $this->assertSame( $conf, $this->maintenance->getConfig() ); + } + + function testParseArgs() { + $m2 = $this->createMaintenance(); + + // Create an option with an argument allowed to be specified multiple times + $m2->addOption( 'multi', 'This option does stuff', false, true, false, true ); + $m2->loadWithArgv( [ '--multi', 'this1', '--multi', 'this2' ] ); + + $this->assertEquals( [ 'this1', 'this2' ], $m2->getOption( 'multi' ) ); + $this->assertEquals( [ [ 'multi', 'this1' ], [ 'multi', 'this2' ] ], + $m2->orderedOptions ); + + $m2->cleanupChanneled(); + + $m2 = $this->createMaintenance(); + + $m2->addOption( 'multi', 'This option does stuff', false, false, false, true ); + $m2->loadWithArgv( [ '--multi', '--multi' ] ); + + $this->assertEquals( [ 1, 1 ], $m2->getOption( 'multi' ) ); + $this->assertEquals( [ [ 'multi', 1 ], [ 'multi', 1 ] ], $m2->orderedOptions ); + + $m2->cleanupChanneled(); + + $m2 = $this->createMaintenance(); + + // Create an option with an argument allowed to be specified multiple times + $m2->addOption( 'multi', 'This option doesn\'t actually support multiple occurrences' ); + $m2->loadWithArgv( [ '--multi=yo' ] ); + + $this->assertEquals( 'yo', $m2->getOption( 'multi' ) ); + $this->assertEquals( [ [ 'multi', 'yo' ] ], $m2->orderedOptions ); + + $m2->cleanupChanneled(); + } +} diff --git a/www/wiki/tests/phpunit/maintenance/backupPrefetchTest.php b/www/wiki/tests/phpunit/maintenance/backupPrefetchTest.php new file mode 100644 index 00000000..8824c7af --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/backupPrefetchTest.php @@ -0,0 +1,279 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use BaseDump; +use MediaWikiTestCase; + +/** + * Tests for BaseDump + * + * @group Dump + * @covers BaseDump + */ +class BaseDumpTest extends MediaWikiTestCase { + + /** + * @var BaseDump The BaseDump instance used within a test. + * + * If set, this BaseDump gets automatically closed in tearDown. + */ + private $dump = null; + + protected function tearDown() { + if ( $this->dump !== null ) { + $this->dump->close(); + } + + // T39458, parent teardown need to be done after closing the + // dump or it might cause some permissions errors. + parent::tearDown(); + } + + /** + * asserts that a prefetch yields an expected string + * + * @param string|null $expected The exepcted result of the prefetch + * @param int $page The page number to prefetch the text for + * @param int $revision The revision number to prefetch the text for + */ + private function assertPrefetchEquals( $expected, $page, $revision ) { + $this->assertEquals( $expected, $this->dump->prefetch( $page, $revision ), + "Prefetch of page $page revision $revision" ); + } + + function testSequential() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizeRevisionMissToRevision() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 2, 3 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 ); + } + + function testSynchronizeRevisionMissToPage() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 2, 40 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizePageMiss() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 3, 40 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testPageMissAtEnd() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 6, 40 ); + } + + function testRevisionMissAtEnd() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 4, 40 ); + } + + function testSynchronizePageMissAtStart() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( null, 0, 2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + } + + function testSynchronizeRevisionMissAtStart() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( null, 1, -2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + } + + function testSequentialAcrossFiles() { + $fname1 = $this->setUpPrefetch( [ 1 ] ); + $fname2 = $this->setUpPrefetch( [ 2, 4 ] ); + $this->dump = new BaseDump( $fname1 . ";" . $fname2 ); + + $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizeSkipAcrossFile() { + $fname1 = $this->setUpPrefetch( [ 1 ] ); + $fname2 = $this->setUpPrefetch( [ 2 ] ); + $fname3 = $this->setUpPrefetch( [ 4 ] ); + $this->dump = new BaseDump( $fname1 . ";" . $fname2 . ";" . $fname3 ); + + $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizeMissInWholeFirstFile() { + $fname1 = $this->setUpPrefetch( [ 1 ] ); + $fname2 = $this->setUpPrefetch( [ 2 ] ); + $this->dump = new BaseDump( $fname1 . ";" . $fname2 ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + } + + /** + * Constructs a temporary file that can be used for prefetching + * + * The temporary file is removed by DumpBackup upon tearDown. + * + * @param array $requested_pages The indices of the page parts that should + * go into the prefetch file. 1,2,4 are available. + * @return string The file name of the created temporary file + */ + private function setUpPrefetch( $requested_pages = [ 1, 2, 4 ] ) { + // The file name, where we store the prepared prefetch file + $fname = $this->getNewTempFile(); + + // The header of every prefetch file + // phpcs:ignore Generic.Files.LineLength + $header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.7/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.7/ http://www.mediawiki.org/xml/export-0.7.xsd" version="0.7" xml:lang="en"> + <siteinfo> + <sitename>wikisvn</sitename> + <base>http://localhost/wiki-svn/index.php/Main_Page</base> + <generator>MediaWiki 1.21alpha</generator> + <case>first-letter</case> + <namespaces> + <namespace key="-2" case="first-letter">Media</namespace> + <namespace key="-1" case="first-letter">Special</namespace> + <namespace key="0" case="first-letter" /> + <namespace key="1" case="first-letter">Talk</namespace> + <namespace key="2" case="first-letter">User</namespace> + <namespace key="3" case="first-letter">User talk</namespace> + <namespace key="4" case="first-letter">Wikisvn</namespace> + <namespace key="5" case="first-letter">Wikisvn talk</namespace> + <namespace key="6" case="first-letter">File</namespace> + <namespace key="7" case="first-letter">File talk</namespace> + <namespace key="8" case="first-letter">MediaWiki</namespace> + <namespace key="9" case="first-letter">MediaWiki talk</namespace> + <namespace key="10" case="first-letter">Template</namespace> + <namespace key="11" case="first-letter">Template talk</namespace> + <namespace key="12" case="first-letter">Help</namespace> + <namespace key="13" case="first-letter">Help talk</namespace> + <namespace key="14" case="first-letter">Category</namespace> + <namespace key="15" case="first-letter">Category talk</namespace> + </namespaces> + </siteinfo> +'; + + // An array holding the pages that are available for prefetch + $available_pages = []; + + // Simple plain page + $available_pages[1] = ' <page> + <title>BackupDumperTestP1</title> + <ns>0</ns> + <id>1</id> + <revision> + <id>1</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP1Summary1</comment> + <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1> + <text xml:space="preserve">BackupDumperTestP1Text1</text> + <model name="wikitext">1</model> + <format mime="text/x-wiki">1</format> + </revision> + </page> +'; + // Page with more than one revisions. Hole in rev ids. + $available_pages[2] = ' <page> + <title>BackupDumperTestP2</title> + <ns>0</ns> + <id>2</id> + <revision> + <id>2</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary1</comment> + <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1> + <text xml:space="preserve">BackupDumperTestP2Text1</text> + <model name="wikitext">1</model> + <format mime="text/x-wiki">1</format> + </revision> + <revision> + <id>5</id> + <parentid>2</parentid> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary4 extra</comment> + <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1> + <text xml:space="preserve">BackupDumperTestP2Text4 some additional Text</text> + <model name="wikitext">1</model> + <format mime="text/x-wiki">1</format> + </revision> + </page> +'; + // Page with id higher than previous id + 1 + $available_pages[4] = ' <page> + <title>Talk:BackupDumperTestP1</title> + <ns>1</ns> + <id>4</id> + <revision> + <id>8</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>Talk BackupDumperTestP1 Summary1</comment> + <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1> + <model name="wikitext">1</model> + <format mime="text/x-wiki">1</format> + <text xml:space="preserve">Talk about BackupDumperTestP1 Text1</text> + </revision> + </page> +'; + + // The common ending for all files + $tail = '</mediawiki> +'; + + // Putting together the content of the prefetch files + $content = $header; + foreach ( $requested_pages as $i ) { + $this->assertTrue( array_key_exists( $i, $available_pages ), + "Check for availability of requested page " . $i ); + $content .= $available_pages[$i]; + } + $content .= $tail; + + $this->assertEquals( strlen( $content ), file_put_contents( + $fname, $content ), "Length of prepared prefetch" ); + + return $fname; + } +} diff --git a/www/wiki/tests/phpunit/maintenance/backupTextPassTest.php b/www/wiki/tests/phpunit/maintenance/backupTextPassTest.php new file mode 100644 index 00000000..ad9bf3ea --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/backupTextPassTest.php @@ -0,0 +1,701 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use MediaWikiLangTestCase; +use TextContentHandler; +use TextPassDumper; +use Title; +use WikiExporter; +use WikiPage; + +require_once __DIR__ . "/../../../maintenance/dumpTextPass.php"; + +/** + * Tests for TextPassDumper that rely on the database + * + * Some of these tests use the old constuctor for TextPassDumper + * and the dump() function, while others use the new loadWithArgv( $args ) + * function and execute(). This is to ensure both the old and new methods + * work properly. + * + * @group Database + * @group Dump + * @covers TextPassDumper + */ +class TextPassDumperDatabaseTest extends DumpTestCase { + + // We'll add several pages, revision and texts. The following variables hold the + // corresponding ids. + private $pageId1, $pageId2, $pageId3, $pageId4; + private static $numOfPages = 4; + private $revId1_1, $textId1_1; + private $revId2_1, $textId2_1, $revId2_2, $textId2_2; + private $revId2_3, $textId2_3, $revId2_4, $textId2_4; + private $revId3_1, $textId3_1, $revId3_2, $textId3_2; + private $revId4_1, $textId4_1; + private static $numOfRevs = 8; + + function addDBData() { + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'ip_changes'; + $this->tablesUsed[] = 'text'; + + $this->mergeMwGlobalArrayValue( 'wgContentHandlers', [ + "BackupTextPassTestModel" => BackupTextPassTestModelHandler::class, + ] ); + + $ns = $this->getDefaultWikitextNS(); + + try { + // Simple page + $title = Title::newFromText( 'BackupDumperTestP1', $ns ); + $page = WikiPage::factory( $title ); + list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page, + "BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" ); + $this->pageId1 = $page->getId(); + + // Page with more than one revision + $title = Title::newFromText( 'BackupDumperTestP2', $ns ); + $page = WikiPage::factory( $title ); + list( $this->revId2_1, $this->textId2_1 ) = $this->addRevision( $page, + "BackupDumperTestP2Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId2_2, $this->textId2_2 ) = $this->addRevision( $page, + "BackupDumperTestP2Text2", "BackupDumperTestP2Summary2" ); + list( $this->revId2_3, $this->textId2_3 ) = $this->addRevision( $page, + "BackupDumperTestP2Text3", "BackupDumperTestP2Summary3" ); + list( $this->revId2_4, $this->textId2_4 ) = $this->addRevision( $page, + "BackupDumperTestP2Text4 some additional Text ", + "BackupDumperTestP2Summary4 extra " ); + $this->pageId2 = $page->getId(); + + // Deleted page. + $title = Title::newFromText( 'BackupDumperTestP3', $ns ); + $page = WikiPage::factory( $title ); + list( $this->revId3_1, $this->textId3_1 ) = $this->addRevision( $page, + "BackupDumperTestP3Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId3_2, $this->textId3_2 ) = $this->addRevision( $page, + "BackupDumperTestP3Text2", "BackupDumperTestP2Summary2" ); + $this->pageId3 = $page->getId(); + $page->doDeleteArticle( "Testing ;)" ); + + // Page from non-default namespace and model. + // ExportTransform applies. + + if ( $ns === NS_TALK ) { + // @todo work around this. + throw new MWException( "The default wikitext namespace is the talk namespace. " + . " We can't currently deal with that." ); + } + + $title = Title::newFromText( 'BackupDumperTestP1', NS_TALK ); + $page = WikiPage::factory( $title ); + list( $this->revId4_1, $this->textId4_1 ) = $this->addRevision( $page, + "Talk about BackupDumperTestP1 Text1", + "Talk BackupDumperTestP1 Summary1", + "BackupTextPassTestModel" ); + $this->pageId4 = $page->getId(); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData in + // DumpTestCase + $this->exceptionFromAddDBData = $e; + } + } + + protected function setUp() { + parent::setUp(); + + // Since we will restrict dumping by page ranges (to allow + // working tests, even if the db gets prepopulated by a base + // class), we have to assert, that the page id are consecutively + // increasing + $this->assertEquals( + [ $this->pageId2, $this->pageId3, $this->pageId4 ], + [ $this->pageId1 + 1, $this->pageId1 + 2, $this->pageId1 + 3 ], + "Page ids increasing without holes" ); + } + + function testPlain() { + // Setting up the dump + $nameStub = $this->setUpStub(); + $nameFull = $this->getNewTempFile(); + $dumper = new TextPassDumper( [ "--stub=file:" . $nameStub, + "--output=file:" . $nameFull ] ); + $dumper->reporting = false; + $dumper->setDB( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + + // Checking for correctness of the dumped data + $this->assertDumpStart( $nameFull ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "TALK ABOUT BACKUPDUMPERTESTP1 TEXT1", + false, + "BackupTextPassTestModel", + "text/plain" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testPrefetchPlain() { + // The mapping between ids and text, for the hits of the prefetch mock + $prefetchMap = [ + [ $this->pageId1, $this->revId1_1, "Prefetch_________1Text1" ], + [ $this->pageId2, $this->revId2_3, "Prefetch_________2Text3" ] + ]; + + // The mock itself + $prefetchMock = $this->getMockBuilder( BaseDump::class ) + ->setMethods( [ 'prefetch' ] ) + ->disableOriginalConstructor() + ->getMock(); + $prefetchMock->expects( $this->exactly( 6 ) ) + ->method( 'prefetch' ) + ->will( $this->returnValueMap( $prefetchMap ) ); + + // Setting up of the dump + $nameStub = $this->setUpStub(); + $nameFull = $this->getNewTempFile(); + + $dumper = new TextPassDumper( [ "--stub=file:" . $nameStub, + "--output=file:" . $nameFull ] ); + + $dumper->prefetch = $prefetchMock; + $dumper->reporting = false; + $dumper->setDB( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + + // Checking for correctness of the dumped data + $this->assertDumpStart( $nameFull ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + // Prefetch kicks in. This is still the SHA-1 of the original text, + // But the actual text (with different SHA-1) comes from prefetch. + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "Prefetch_________1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 ); + // Prefetch kicks in. This is still the SHA-1 of the original text, + // But the actual text (with different SHA-1) comes from prefetch. + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "Prefetch_________2Text3", $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "TALK ABOUT BACKUPDUMPERTESTP1 TEXT1", + false, + "BackupTextPassTestModel", + "text/plain" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + /** + * Ensures that checkpoint dumps are used and written, by successively increasing the + * stub size and dumping until the duration crosses a threshold. + * + * @param string $checkpointFormat Either "file" for plain text or "gzip" for gzipped + * checkpoint files. + */ + private function checkpointHelper( $checkpointFormat = "file" ) { + // Getting temporary names + $nameStub = $this->getNewTempFile(); + $nameOutputDir = $this->getNewTempDirectory(); + + $stderr = fopen( 'php://output', 'a' ); + if ( $stderr === false ) { + $this->fail( "Could not open stream for stderr" ); + } + + $iterations = 32; // We'll start with that many iterations of revisions + // in stub. Make sure that the generated volume is above the buffer size + // set below. Otherwise, the checkpointing does not trigger. + $lastDuration = 0; + $minDuration = 2; // We want the dump to take at least this many seconds + $checkpointAfter = 0.5; // Generate checkpoint after this many seconds + + // Until a dump takes at least $minDuration seconds, perform a dump and check + // duration. If the dump did not take long enough increase the iteration + // count, to generate a bigger stub file next time. + while ( $lastDuration < $minDuration ) { + // Setting up the dump + wfRecursiveRemoveDir( $nameOutputDir ); + $this->assertTrue( wfMkdirParents( $nameOutputDir ), + "Creating temporary output directory " ); + $this->setUpStub( $nameStub, $iterations ); + $dumper = new TextPassDumper(); + $dumper->loadWithArgv( [ "--stub=file:" . $nameStub, + "--output=" . $checkpointFormat . ":" . $nameOutputDir . "/full", + "--maxtime=1" /*This is in minutes. Fixup is below*/, + "--buffersize=32768", // The default of 32 iterations fill up 32KB about twice + "--checkpointfile=checkpoint-%s-%s.xml.gz" ] ); + $dumper->setDB( $this->db ); + $dumper->maxTimeAllowed = $checkpointAfter; // Patching maxTime from 1 minute + $dumper->stderr = $stderr; + + // The actual dump and taking time + $ts_before = microtime( true ); + $dumper->execute(); + $ts_after = microtime( true ); + $lastDuration = $ts_after - $ts_before; + + // Handling increasing the iteration count for the stubs + if ( $lastDuration < $minDuration ) { + $old_iterations = $iterations; + if ( $lastDuration > 0.2 ) { + // lastDuration is big enough, to allow an educated guess + $factor = ( $minDuration + 0.5 ) / $lastDuration; + if ( ( $factor > 1.1 ) && ( $factor < 100 ) ) { + // educated guess is reasonable + $iterations = (int)( $iterations * $factor ); + } + } + + if ( $old_iterations == $iterations ) { + // Heuristics were not applied, so we just *2. + $iterations *= 2; + } + + $this->assertLessThan( 50000, $iterations, + "Emergency stop against infinitely increasing iteration " + . "count ( last duration: $lastDuration )" ); + } + } + + // The dump (hopefully) did take long enough to produce more than one + // checkpoint file. + // We now check all the checkpoint files for validity. + + $files = scandir( $nameOutputDir ); + $this->assertTrue( asort( $files ), "Sorting files in temporary directory" ); + $fileOpened = false; + $lookingForPage = 1; + $checkpointFiles = 0; + + // Each run of the following loop body tries to handle exactly 1 /page/ (not + // iteration of stub content). $i is only increased after having treated page 4. + for ( $i = 0; $i < $iterations; ) { + // 1. Assuring a file is opened and ready. Skipping across header if + // necessary. + if ( !$fileOpened ) { + $this->assertNotEmpty( $files, "No more existing dump files, " + . "but not yet all pages found" ); + $fname = array_shift( $files ); + while ( $fname == "." || $fname == ".." ) { + $this->assertNotEmpty( $files, "No more existing dump" + . " files, but not yet all pages found" ); + $fname = array_shift( $files ); + } + if ( $checkpointFormat == "gzip" ) { + $this->gunzip( $nameOutputDir . "/" . $fname ); + } + $this->assertDumpStart( $nameOutputDir . "/" . $fname ); + $fileOpened = true; + $checkpointFiles++; + } + + // 2. Performing a single page check + switch ( $lookingForPage ) { + case 1: + // Page 1 + $this->assertPageStart( $this->pageId1 + $i * self::$numOfPages, NS_MAIN, + "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1 + $i * self::$numOfRevs, "BackupDumperTestP1Summary1", + $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + $lookingForPage = 2; + break; + + case 2: + // Page 2 + $this->assertPageStart( $this->pageId2 + $i * self::$numOfPages, NS_MAIN, + "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1 + $i * self::$numOfRevs, "BackupDumperTestP2Summary1", + $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2 + $i * self::$numOfRevs, "BackupDumperTestP2Summary2", + $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 + $i * self::$numOfRevs ); + $this->assertRevision( $this->revId2_3 + $i * self::$numOfRevs, "BackupDumperTestP2Summary3", + $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", $this->revId2_2 + $i * self::$numOfRevs ); + $this->assertRevision( $this->revId2_4 + $i * self::$numOfRevs, + "BackupDumperTestP2Summary4 extra", + $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", + $this->revId2_3 + $i * self::$numOfRevs ); + $this->assertPageEnd(); + + $lookingForPage = 4; + break; + + case 4: + // Page 4 + $this->assertPageStart( $this->pageId4 + $i * self::$numOfPages, NS_TALK, + "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1 + $i * self::$numOfRevs, + "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "TALK ABOUT BACKUPDUMPERTESTP1 TEXT1", + false, + "BackupTextPassTestModel", + "text/plain" ); + $this->assertPageEnd(); + + $lookingForPage = 1; + + // We dealt with the whole iteration. + $i++; + break; + + default: + $this->fail( "Bad setting for lookingForPage ($lookingForPage)" ); + } + + // 3. Checking for the end of the current checkpoint file + if ( $this->xml->nodeType == XMLReader::END_ELEMENT + && $this->xml->name == "mediawiki" + ) { + $this->assertDumpEnd(); + $fileOpened = false; + } + } + + // Assuring we completely read all files ... + $this->assertFalse( $fileOpened, "Currently read file still open?" ); + $this->assertEmpty( $files, "Remaining unchecked files" ); + + // ... and have dealt with more than one checkpoint file + $this->assertGreaterThan( + 1, + $checkpointFiles, + "expected more than 1 checkpoint to have been created. " + . "Checkpoint interval is $checkpointAfter seconds, maybe your computer is too fast?" + ); + + $this->expectETAOutput(); + } + + /** + * Broken per T70653. + * + * @group large + * @group Broken + */ + function testCheckpointPlain() { + $this->checkpointHelper(); + } + + /** + * tests for working checkpoint generation in gzip format work. + * + * We keep this test in addition to the simpler self::testCheckpointPlain, as there + * were once problems when the used sinks were DumpPipeOutputs. + * + * xmldumps-backup typically uses bzip2 instead of gzip. However, as bzip2 requires + * PHP extensions, we go for gzip instead, which triggers the same relevant code + * paths while still being testable on more systems. + * + * Broken per T70653. + * + * @group large + * @group Broken + */ + function testCheckpointGzip() { + $this->checkHasGzip(); + $this->checkpointHelper( "gzip" ); + } + + /** + * Creates a stub file that is used for testing the text pass of dumps + * + * @param string $fname (Optional) Absolute name of the file to write + * the stub into. If this parameter is null, a new temporary + * file is generated that is automatically removed upon tearDown. + * @param int $iterations (Optional) specifies how often the block + * of 3 pages should go into the stub file. The page and + * revision id increase further and further, while the text + * id of the first iteration is reused. The pages and revision + * of iteration > 1 have no corresponding representation in the database. + * @return string Absolute filename of the stub + */ + private function setUpStub( $fname = null, $iterations = 1 ) { + if ( $fname === null ) { + $fname = $this->getNewTempFile(); + } + $header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.10/" ' + . 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + . 'xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.10/ ' + . 'http://www.mediawiki.org/xml/export-0.10.xsd" version="0.10" xml:lang="en"> + <siteinfo> + <sitename>wikisvn</sitename> + <base>http://localhost/wiki-svn/index.php/Main_Page</base> + <generator>MediaWiki 1.21alpha</generator> + <case>first-letter</case> + <namespaces> + <namespace key="-2" case="first-letter">Media</namespace> + <namespace key="-1" case="first-letter">Special</namespace> + <namespace key="0" case="first-letter" /> + <namespace key="1" case="first-letter">Talk</namespace> + <namespace key="2" case="first-letter">User</namespace> + <namespace key="3" case="first-letter">User talk</namespace> + <namespace key="4" case="first-letter">Wikisvn</namespace> + <namespace key="5" case="first-letter">Wikisvn talk</namespace> + <namespace key="6" case="first-letter">File</namespace> + <namespace key="7" case="first-letter">File talk</namespace> + <namespace key="8" case="first-letter">MediaWiki</namespace> + <namespace key="9" case="first-letter">MediaWiki talk</namespace> + <namespace key="10" case="first-letter">Template</namespace> + <namespace key="11" case="first-letter">Template talk</namespace> + <namespace key="12" case="first-letter">Help</namespace> + <namespace key="13" case="first-letter">Help talk</namespace> + <namespace key="14" case="first-letter">Category</namespace> + <namespace key="15" case="first-letter">Category talk</namespace> + </namespaces> + </siteinfo> +'; + $tail = '</mediawiki> +'; + + $content = $header; + $iterations = intval( $iterations ); + for ( $i = 0; $i < $iterations; $i++ ) { + $page1 = ' <page> + <title>BackupDumperTestP1</title> + <ns>0</ns> + <id>' . ( $this->pageId1 + $i * self::$numOfPages ) . '</id> + <revision> + <id>' . ( $this->revId1_1 + $i * self::$numOfRevs ) . '</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP1Summary1</comment> + <model>wikitext</model> + <format>text/x-wiki</format> + <text id="' . $this->textId1_1 . '" bytes="23" /> + <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1> + </revision> + </page> +'; + $page2 = ' <page> + <title>BackupDumperTestP2</title> + <ns>0</ns> + <id>' . ( $this->pageId2 + $i * self::$numOfPages ) . '</id> + <revision> + <id>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary1</comment> + <model>wikitext</model> + <format>text/x-wiki</format> + <text id="' . $this->textId2_1 . '" bytes="23" /> + <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1> + </revision> + <revision> + <id>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</id> + <parentid>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</parentid> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary2</comment> + <model>wikitext</model> + <format>text/x-wiki</format> + <text id="' . $this->textId2_2 . '" bytes="23" /> + <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1> + </revision> + <revision> + <id>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</id> + <parentid>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</parentid> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary3</comment> + <model>wikitext</model> + <format>text/x-wiki</format> + <text id="' . $this->textId2_3 . '" bytes="23" /> + <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1> + </revision> + <revision> + <id>' . ( $this->revId2_4 + $i * self::$numOfRevs ) . '</id> + <parentid>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</parentid> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary4 extra</comment> + <model>wikitext</model> + <format>text/x-wiki</format> + <text id="' . $this->textId2_4 . '" bytes="44" /> + <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1> + </revision> + </page> +'; + // page 3 not in stub + + $page4 = ' <page> + <title>Talk:BackupDumperTestP1</title> + <ns>1</ns> + <id>' . ( $this->pageId4 + $i * self::$numOfPages ) . '</id> + <revision> + <id>' . ( $this->revId4_1 + $i * self::$numOfRevs ) . '</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>Talk BackupDumperTestP1 Summary1</comment> + <model>BackupTextPassTestModel</model> + <format>text/plain</format> + <text id="' . $this->textId4_1 . '" bytes="35" /> + <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1> + </revision> + </page> +'; + $content .= $page1 . $page2 . $page4; + } + $content .= $tail; + $this->assertEquals( strlen( $content ), file_put_contents( + $fname, $content ), "Length of prepared stub" ); + + return $fname; + } +} + +class BackupTextPassTestModelHandler extends TextContentHandler { + + public function __construct() { + parent::__construct( 'BackupTextPassTestModel' ); + } + + public function exportTransform( $text, $format = null ) { + return strtoupper( $text ); + } + +} + +/** + * Tests for TextPassDumper that do not rely on the database + * + * (As the Database group is only detected at class level (not method level), we + * cannot bring this test case's tests into the above main test case.) + * + * @group Dump + * @covers TextPassDumper + */ +class TextPassDumperDatabaselessTest extends MediaWikiLangTestCase { + /** + * Ensures that setting the buffer size is effective. + * + * @dataProvider bufferSizeProvider + */ + function testBufferSizeSetting( $expected, $size, $msg ) { + $dumper = new TextPassDumperAccessor(); + $dumper->loadWithArgv( [ "--buffersize=" . $size ] ); + $dumper->execute(); + $this->assertEquals( $expected, $dumper->getBufferSize(), $msg ); + } + + /** + * Ensures that setting the buffer size is effective. + * + * @dataProvider bufferSizeProvider + */ + function bufferSizeProvider() { + // expected, bufferSize to initialize with, message + return [ + [ 512 * 1024, 512 * 1024, "Setting 512KB is not effective" ], + [ 8192, 8192, "Setting 8KB is not effective" ], + [ 4096, 2048, "Could set buffer size below lower bound" ] + ]; + } +} + +/** + * Accessor for internal state of TextPassDumper + * + * Do not warrentless add getters here. + */ +class TextPassDumperAccessor extends TextPassDumper { + /** + * Gets the bufferSize. + * + * If bufferSize setting does not work correctly, testCheckpoint... tests + * fail and point in the wrong direction. To aid in troubleshooting when + * testCheckpoint... tests break at some point in the future, we test the + * bufferSize setting, hence need this accessor. + * + * (Yes, bufferSize is internal state of the TextPassDumper, but aiding + * debugging of testCheckpoint... in the future seems to be worth testing + * against it nonetheless.) + */ + public function getBufferSize() { + return $this->bufferSize; + } + + function dump( $history, $text = null ) { + return true; + } +} diff --git a/www/wiki/tests/phpunit/maintenance/backup_LogTest.php b/www/wiki/tests/phpunit/maintenance/backup_LogTest.php new file mode 100644 index 00000000..c215b997 --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/backup_LogTest.php @@ -0,0 +1,241 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use DumpBackup; +use ManualLogEntry; +use Title; +use User; +use WikiExporter; + +/** + * Tests for log dumps of BackupDumper + * + * Some of these tests use the old constuctor for TextPassDumper + * and the dump() function, while others use the new loadWithArgv( $args ) + * function and execute(). This is to ensure both the old and new methods + * work properly. + * + * @group Database + * @group Dump + * @covers BackupDumper + */ +class BackupDumperLoggerTest extends DumpTestCase { + + // We'll add several log entries and users for this test. The following + // variables hold the corresponding ids. + private $userId1, $userId2; + private $logId1, $logId2, $logId3; + + /** + * adds a log entry to the database. + * + * @param string $type Type of the log entry + * @param string $subtype Subtype of the log entry + * @param User $user User that performs the logged operation + * @param int $ns Number of the namespace for the entry's target's title + * @param string $title Title of the entry's target + * @param string $comment Comment of the log entry + * @param array $parameters (optional) accompanying data that is attached to the entry + * + * @return int Id of the added log entry + */ + private function addLogEntry( $type, $subtype, User $user, $ns, $title, + $comment = null, $parameters = null + ) { + $logEntry = new ManualLogEntry( $type, $subtype ); + $logEntry->setPerformer( $user ); + $logEntry->setTarget( Title::newFromText( $title, $ns ) ); + if ( $comment !== null ) { + $logEntry->setComment( $comment ); + } + if ( $parameters !== null ) { + $logEntry->setParameters( $parameters ); + } + + return $logEntry->insert(); + } + + function addDBData() { + $this->tablesUsed[] = 'logging'; + $this->tablesUsed[] = 'user'; + + try { + $user1 = User::newFromName( 'BackupDumperLogUserA' ); + $this->userId1 = $user1->getId(); + if ( $this->userId1 === 0 ) { + $user1->addToDatabase(); + $this->userId1 = $user1->getId(); + } + $this->assertGreaterThan( 0, $this->userId1 ); + + $user2 = User::newFromName( 'BackupDumperLogUserB' ); + $this->userId2 = $user2->getId(); + if ( $this->userId2 === 0 ) { + $user2->addToDatabase(); + $this->userId2 = $user2->getId(); + } + $this->assertGreaterThan( 0, $this->userId2 ); + + $this->logId1 = $this->addLogEntry( 'type', 'subtype', + $user1, NS_MAIN, "PageA" ); + $this->assertGreaterThan( 0, $this->logId1 ); + + $this->logId2 = $this->addLogEntry( 'supress', 'delete', + $user2, NS_TALK, "PageB", "SomeComment" ); + $this->assertGreaterThan( 0, $this->logId2 ); + + $this->logId3 = $this->addLogEntry( 'move', 'delete', + $user2, NS_MAIN, "PageA", "SomeOtherComment", + [ 'key1' => 1, 3 => 'value3' ] ); + $this->assertGreaterThan( 0, $this->logId3 ); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData in + // DumpTestCase + $this->exceptionFromAddDBData = $e; + } + } + + /** + * asserts that the xml reader is at the beginning of a log entry and skips over + * it while analyzing it. + * + * @param int $id Id of the log entry + * @param string $user_name User name of the log entry's performer + * @param int $user_id User id of the log entry 's performer + * @param string|null $comment Comment of the log entry. If null, the comment text is ignored. + * @param string $type Type of the log entry + * @param string $subtype Subtype of the log entry + * @param string $title Title of the log entry's target + * @param array $parameters (optional) unserialized data accompanying the log entry + */ + private function assertLogItem( $id, $user_name, $user_id, $comment, $type, + $subtype, $title, $parameters = [] + ) { + $this->assertNodeStart( "logitem" ); + $this->skipWhitespace(); + + $this->assertTextNode( "id", $id ); + $this->assertTextNode( "timestamp", false ); + + $this->assertNodeStart( "contributor" ); + $this->skipWhitespace(); + $this->assertTextNode( "username", $user_name ); + $this->assertTextNode( "id", $user_id ); + $this->assertNodeEnd( "contributor" ); + $this->skipWhitespace(); + + if ( $comment !== null ) { + $this->assertTextNode( "comment", $comment ); + } + $this->assertTextNode( "type", $type ); + $this->assertTextNode( "action", $subtype ); + $this->assertTextNode( "logtitle", $title ); + + $this->assertNodeStart( "params" ); + $parameters_xml = unserialize( $this->xml->value ); + $this->assertEquals( $parameters, $parameters_xml ); + $this->assertTrue( $this->xml->read(), "Skipping past processed text of params" ); + $this->assertNodeEnd( "params" ); + $this->skipWhitespace(); + + $this->assertNodeEnd( "logitem" ); + $this->skipWhitespace(); + } + + function testPlain() { + global $wgContLang; + + // Preparing the dump + $fname = $this->getNewTempFile(); + + $dumper = new DumpBackup( [ '--output=file:' . $fname ] ); + $dumper->startId = $this->logId1; + $dumper->endId = $this->logId3 + 1; + $dumper->reporting = false; + $dumper->setDB( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::LOGS, WikiExporter::TEXT ); + + // Analyzing the dumped data + $this->assertDumpStart( $fname ); + + $this->assertLogItem( $this->logId1, "BackupDumperLogUserA", + $this->userId1, null, "type", "subtype", "PageA" ); + + $this->assertNotNull( $wgContLang, "Content language object validation" ); + $namespace = $wgContLang->getNsText( NS_TALK ); + $this->assertInternalType( 'string', $namespace ); + $this->assertGreaterThan( 0, strlen( $namespace ) ); + $this->assertLogItem( $this->logId2, "BackupDumperLogUserB", + $this->userId2, "SomeComment", "supress", "delete", + $namespace . ":PageB" ); + + $this->assertLogItem( $this->logId3, "BackupDumperLogUserB", + $this->userId2, "SomeOtherComment", "move", "delete", + "PageA", [ 'key1' => 1, 3 => 'value3' ] ); + + $this->assertDumpEnd(); + } + + function testXmlDumpsBackupUseCaseLogging() { + global $wgContLang; + + $this->checkHasGzip(); + + // Preparing the dump + $fname = $this->getNewTempFile(); + + $dumper = new DumpBackup(); + $dumper->loadWithArgv( [ '--logs', '--output=gzip:' . $fname, + '--reporting=2' ] ); + $dumper->startId = $this->logId1; + $dumper->endId = $this->logId3 + 1; + $dumper->setDB( $this->db ); + + // xmldumps-backup demands reporting, although this is currently not + // implemented in BackupDumper, when dumping logging data. We + // nevertheless capture the output of the dump process already now, + // to be able to alert (once dumping produces reports) that this test + // needs updates. + $dumper->stderr = fopen( 'php://output', 'a' ); + if ( $dumper->stderr === false ) { + $this->fail( "Could not open stream for stderr" ); + } + + // Performing the dump + $dumper->execute(); + + $this->assertTrue( fclose( $dumper->stderr ), "Closing stderr handle" ); + + // Analyzing the dumped data + $this->gunzip( $fname ); + + $this->assertDumpStart( $fname ); + + $this->assertLogItem( $this->logId1, "BackupDumperLogUserA", + $this->userId1, null, "type", "subtype", "PageA" ); + + $this->assertNotNull( $wgContLang, "Content language object validation" ); + $namespace = $wgContLang->getNsText( NS_TALK ); + $this->assertInternalType( 'string', $namespace ); + $this->assertGreaterThan( 0, strlen( $namespace ) ); + $this->assertLogItem( $this->logId2, "BackupDumperLogUserB", + $this->userId2, "SomeComment", "supress", "delete", + $namespace . ":PageB" ); + + $this->assertLogItem( $this->logId3, "BackupDumperLogUserB", + $this->userId2, "SomeOtherComment", "move", "delete", + "PageA", [ 'key1' => 1, 3 => 'value3' ] ); + + $this->assertDumpEnd(); + + // Currently, no reporting is implemented. Alert via failure, once + // this changes. + // If reporting for log dumps has been implemented, please update + // the following statement to catch good output + $this->expectOutputString( '' ); + } +} diff --git a/www/wiki/tests/phpunit/maintenance/backup_PageTest.php b/www/wiki/tests/phpunit/maintenance/backup_PageTest.php new file mode 100644 index 00000000..51a1ed69 --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/backup_PageTest.php @@ -0,0 +1,443 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use DumpBackup; +use Language; +use Title; +use WikiExporter; +use WikiPage; + +/** + * Tests for page dumps of BackupDumper + * + * @group Database + * @group Dump + * @covers BackupDumper + */ +class BackupDumperPageTest extends DumpTestCase { + + // We'll add several pages, revision and texts. The following variables hold the + // corresponding ids. + private $pageId1, $pageId2, $pageId3, $pageId4; + private $pageTitle1, $pageTitle2, $pageTitle3, $pageTitle4; + private $revId1_1, $textId1_1; + private $revId2_1, $textId2_1, $revId2_2, $textId2_2; + private $revId2_3, $textId2_3, $revId2_4, $textId2_4; + private $revId3_1, $textId3_1, $revId3_2, $textId3_2; + private $revId4_1, $textId4_1; + private $namespace, $talk_namespace; + + function addDBData() { + // be sure, titles created here using english namespace names + $this->setMwGlobals( [ + 'wgLanguageCode' => 'en', + 'wgContLang' => Language::factory( 'en' ), + ] ); + + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'ip_changes'; + $this->tablesUsed[] = 'text'; + + try { + $this->namespace = $this->getDefaultWikitextNS(); + $this->talk_namespace = NS_TALK; + + if ( $this->namespace === $this->talk_namespace ) { + // @todo work around this. + throw new MWException( "The default wikitext namespace is the talk namespace. " + . " We can't currently deal with that." ); + } + + $this->pageTitle1 = Title::newFromText( 'BackupDumperTestP1', $this->namespace ); + $page = WikiPage::factory( $this->pageTitle1 ); + list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page, + "BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" ); + $this->pageId1 = $page->getId(); + + $this->pageTitle2 = Title::newFromText( 'BackupDumperTestP2', $this->namespace ); + $page = WikiPage::factory( $this->pageTitle2 ); + list( $this->revId2_1, $this->textId2_1 ) = $this->addRevision( $page, + "BackupDumperTestP2Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId2_2, $this->textId2_2 ) = $this->addRevision( $page, + "BackupDumperTestP2Text2", "BackupDumperTestP2Summary2" ); + list( $this->revId2_3, $this->textId2_3 ) = $this->addRevision( $page, + "BackupDumperTestP2Text3", "BackupDumperTestP2Summary3" ); + list( $this->revId2_4, $this->textId2_4 ) = $this->addRevision( $page, + "BackupDumperTestP2Text4 some additional Text ", + "BackupDumperTestP2Summary4 extra " ); + $this->pageId2 = $page->getId(); + + $this->pageTitle3 = Title::newFromText( 'BackupDumperTestP3', $this->namespace ); + $page = WikiPage::factory( $this->pageTitle3 ); + list( $this->revId3_1, $this->textId3_1 ) = $this->addRevision( $page, + "BackupDumperTestP3Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId3_2, $this->textId3_2 ) = $this->addRevision( $page, + "BackupDumperTestP3Text2", "BackupDumperTestP2Summary2" ); + $this->pageId3 = $page->getId(); + $page->doDeleteArticle( "Testing ;)" ); + + $this->pageTitle4 = Title::newFromText( 'BackupDumperTestP1', $this->talk_namespace ); + $page = WikiPage::factory( $this->pageTitle4 ); + list( $this->revId4_1, $this->textId4_1 ) = $this->addRevision( $page, + "Talk about BackupDumperTestP1 Text1", + "Talk BackupDumperTestP1 Summary1" ); + $this->pageId4 = $page->getId(); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData in + // DumpTestCase + $this->exceptionFromAddDBData = $e; + } + } + + protected function setUp() { + parent::setUp(); + + // Since we will restrict dumping by page ranges (to allow + // working tests, even if the db gets prepopulated by a base + // class), we have to assert, that the page id are consecutively + // increasing + $this->assertEquals( + [ $this->pageId2, $this->pageId3, $this->pageId4 ], + [ $this->pageId1 + 1, $this->pageId2 + 1, $this->pageId3 + 1 ], + "Page ids increasing without holes" ); + } + + function testFullTextPlain() { + // Preparing the dump + $fname = $this->getNewTempFile(); + + $dumper = new DumpBackup(); + $dumper->loadWithArgv( [ '--full', '--quiet', '--output', 'file:' . $fname ] ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->setDB( $this->db ); + + // Performing the dump + $dumper->execute(); + + // Checking the dumped data + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( + $this->pageId4, + $this->talk_namespace, + $this->pageTitle4->getPrefixedText() + ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testFullStubPlain() { + // Preparing the dump + $fname = $this->getNewTempFile(); + + $dumper = new DumpBackup(); + $dumper->loadWithArgv( [ '--full', '--quiet', '--output', 'file:' . $fname, '--stub' ] ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->setDB( $this->db ); + + // Performing the dump + $dumper->execute(); + + // Checking the dumped data + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( + $this->pageId4, + $this->talk_namespace, + $this->pageTitle4->getPrefixedText() + ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testCurrentStubPlain() { + // Preparing the dump + $fname = $this->getNewTempFile(); + + $dumper = new DumpBackup( [ '--output', 'file:' . $fname ] ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDB( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB ); + + // Checking the dumped data + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( + $this->pageId4, + $this->talk_namespace, + $this->pageTitle4->getPrefixedText() + ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testCurrentStubGzip() { + $this->checkHasGzip(); + + // Preparing the dump + $fname = $this->getNewTempFile(); + + $dumper = new DumpBackup( [ '--output', 'gzip:' . $fname ] ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDB( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB ); + + // Checking the dumped data + $this->gunzip( $fname ); + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( + $this->pageId4, + $this->talk_namespace, + $this->pageTitle4->getPrefixedText() + ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + /** + * xmldumps-backup typically performs a single dump that that writes + * out three files + * - gzipped stubs of everything (meta-history) + * - gzipped stubs of latest revisions of all pages (meta-current) + * - gzipped stubs of latest revisions of all pages of namespage 0 + * (articles) + * + * We reproduce such a setup with our mini fixture, although we omit + * chunks, and all the other gimmicks of xmldumps-backup. + */ + function testXmlDumpsBackupUseCase() { + $this->checkHasGzip(); + + $fnameMetaHistory = $this->getNewTempFile(); + $fnameMetaCurrent = $this->getNewTempFile(); + $fnameArticles = $this->getNewTempFile(); + + $dumper = new DumpBackup( [ "--full", "--stub", "--output=gzip:" . $fnameMetaHistory, + "--output=gzip:" . $fnameMetaCurrent, "--filter=latest", + "--output=gzip:" . $fnameArticles, "--filter=latest", + "--filter=notalk", "--filter=namespace:!NS_USER", + "--reporting=1000" ] ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->setDB( $this->db ); + + // xmldumps-backup uses reporting. We will not check the exact reported + // message, as they are dependent on the processing power of the used + // computer. We only check that reporting does not crash the dumping + // and that something is reported + $dumper->stderr = fopen( 'php://output', 'a' ); + if ( $dumper->stderr === false ) { + $this->fail( "Could not open stream for stderr" ); + } + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::STUB ); + + $this->assertTrue( fclose( $dumper->stderr ), "Closing stderr handle" ); + + // Checking meta-history ------------------------------------------------- + + $this->gunzip( $fnameMetaHistory ); + $this->assertDumpStart( $fnameMetaHistory ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( + $this->pageId4, + $this->talk_namespace, + $this->pageTitle4->getPrefixedText() + ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + + // Checking meta-current ------------------------------------------------- + + $this->gunzip( $fnameMetaCurrent ); + $this->assertDumpStart( $fnameMetaCurrent ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( + $this->pageId4, + $this->talk_namespace, + $this->pageTitle4->getPrefixedText() + ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + + // Checking articles ------------------------------------------------- + + $this->gunzip( $fnameArticles ); + $this->assertDumpStart( $fnameArticles ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + // -> Page is not in $this->namespace. Hence not visible + + $this->assertDumpEnd(); + + $this->expectETAOutput(); + } +} diff --git a/www/wiki/tests/phpunit/maintenance/categoriesRdfTest.php b/www/wiki/tests/phpunit/maintenance/categoriesRdfTest.php new file mode 100644 index 00000000..5068e701 --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/categoriesRdfTest.php @@ -0,0 +1,102 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use DumpCategoriesAsRdf; +use MediaWikiLangTestCase; + +/** + * @covers CategoriesRdf + * @covers DumpCategoriesAsRdf + */ +class CategoriesRdfTest extends MediaWikiLangTestCase { + public function getCategoryIterator() { + return [ + // batch 1 + [ + (object)[ + 'page_title' => 'Category One', + 'page_id' => 1, + 'pp_propname' => null, + 'cat_pages' => '20', + 'cat_subcats' => '10', + 'cat_files' => '3' + ], + (object)[ + 'page_title' => '2 Category Two', + 'page_id' => 2, + 'pp_propname' => 'hiddencat', + 'cat_pages' => 20, + 'cat_subcats' => 0, + 'cat_files' => 3 + ], + ], + // batch 2 + [ + (object)[ + 'page_title' => 'Третья категория', + 'page_id' => 3, + 'pp_propname' => null, + 'cat_pages' => '0', + 'cat_subcats' => '0', + 'cat_files' => '0' + ], + ] + ]; + } + + public function getCategoryLinksIterator( $dbr, array $ids ) { + $res = []; + foreach ( $ids as $pageid ) { + $res[] = (object)[ 'cl_from' => $pageid, 'cl_to' => "Parent of $pageid" ]; + } + return $res; + } + + public function testCategoriesDump() { + $this->setMwGlobals( [ + 'wgServer' => 'http://acme.test', + 'wgCanonicalServer' => 'http://acme.test', + 'wgArticlePath' => '/wiki/$1', + 'wgRightsUrl' => '//creativecommons.org/licenses/by-sa/3.0/', + ] ); + + $dumpScript = + $this->getMockBuilder( DumpCategoriesAsRdf::class ) + ->setMethods( [ 'getCategoryIterator', 'getCategoryLinksIterator' ] ) + ->getMock(); + + $dumpScript->expects( $this->once() ) + ->method( 'getCategoryIterator' ) + ->willReturn( $this->getCategoryIterator() ); + + $dumpScript->expects( $this->any() ) + ->method( 'getCategoryLinksIterator' ) + ->willReturnCallback( [ $this, 'getCategoryLinksIterator' ] ); + + /** @var DumpCategoriesAsRdf $dumpScript */ + $logFileName = tempnam( sys_get_temp_dir(), "Categories-DumpRdfTest" ); + $outFileName = tempnam( sys_get_temp_dir(), "Categories-DumpRdfTest" ); + + $dumpScript->loadParamsAndArgs( + null, + [ + 'log' => $logFileName, + 'output' => $outFileName, + 'format' => 'nt', + ] + ); + + $dumpScript->execute(); + $actualOut = file_get_contents( $outFileName ); + $actualOut = preg_replace( + '|<http://acme.test/wiki/Special:CategoryDump> <http://schema.org/dateModified> "[^"]+?"|', + '<http://acme.test/wiki/Special:CategoryDump> <http://schema.org/dateModified> "{DATE}"', + $actualOut + ); + + $outFile = __DIR__ . '/../data/categoriesrdf/categoriesRdf-out.nt'; + $this->assertFileContains( $outFile, $actualOut ); + } + +} diff --git a/www/wiki/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php b/www/wiki/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php new file mode 100644 index 00000000..c1418174 --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php @@ -0,0 +1,252 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use DeleteAutoPatrolLogs; + +/** + * @group Database + * @covers DeleteAutoPatrolLogs + */ +class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { + + public function getMaintenanceClass() { + return DeleteAutoPatrolLogs::class; + } + + public function setUp() { + parent::setUp(); + $this->tablesUsed = [ 'logging' ]; + + $this->cleanLoggingTable(); + $this->insertLoggingData(); + } + + private function cleanLoggingTable() { + wfGetDB( DB_MASTER )->delete( 'logging', '*' ); + } + + private function insertLoggingData() { + $logs = []; + + // Manual patrolling + $logs[] = [ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => 7251, + 'log_params' => '', + 'log_timestamp' => 20041223210426 + ]; + + // Autopatrol #1 + $logs[] = [ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + 'log_user' => 7252, + 'log_params' => '', + 'log_timestamp' => 20051223210426 + ]; + + // Block + $logs[] = [ + 'log_type' => 'block', + 'log_action' => 'block', + 'log_user' => 7253, + 'log_params' => '', + 'log_timestamp' => 20061223210426 + ]; + + // Very old/ invalid patrol + $logs[] = [ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => 7253, + 'log_params' => 'nanana', + 'log_timestamp' => 20061223210426 + ]; + + // Autopatrol #2 + $logs[] = [ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + 'log_user' => 7254, + 'log_params' => '', + 'log_timestamp' => 20071223210426 + ]; + + // Autopatrol #3 old way + $logs[] = [ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => 7255, + 'log_params' => serialize( [ '6::auto' => true ] ), + 'log_timestamp' => 20081223210426 + ]; + + // Manual patrol #2 old way + $logs[] = [ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => 7256, + 'log_params' => serialize( [ '6::auto' => false ] ), + 'log_timestamp' => 20091223210426 + ]; + + wfGetDB( DB_MASTER )->insert( 'logging', $logs ); + } + + public function runProvider() { + $allRows = [ + (object)[ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => '7251', + ], + (object)[ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + 'log_user' => '7252', + ], + (object)[ + 'log_type' => 'block', + 'log_action' => 'block', + 'log_user' => '7253', + ], + (object)[ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => '7253', + ], + (object)[ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + 'log_user' => '7254', + ], + (object)[ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => '7255', + ], + (object)[ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => '7256', + ], + ]; + + $cases = [ + 'dry run' => [ + $allRows, + [ '--sleep', '0', '--dry-run', '-q' ] + ], + 'basic run' => [ + [ + $allRows[0], + $allRows[2], + $allRows[3], + $allRows[5], + $allRows[6], + ], + [ '--sleep', '0', '-q' ] + ], + 'run with before' => [ + [ + $allRows[0], + $allRows[2], + $allRows[3], + $allRows[4], + $allRows[5], + $allRows[6], + ], + [ '--sleep', '0', '--before', '20060123210426', '-q' ] + ], + 'run with check-old' => [ + [ + $allRows[0], + $allRows[1], + $allRows[2], + $allRows[3], + $allRows[4], + $allRows[6], + ], + [ '--sleep', '0', '--check-old', '-q' ] + ], + ]; + + foreach ( $cases as $key => $case ) { + yield $key . '-batch-size-1' => [ + $case[0], + array_merge( $case[1], [ '--batch-size', '1' ] ) + ]; + yield $key . '-batch-size-5' => [ + $case[0], + array_merge( $case[1], [ '--batch-size', '5' ] ) + ]; + yield $key . '-batch-size-1000' => [ + $case[0], + array_merge( $case[1], [ '--batch-size', '1000' ] ) + ]; + } + } + + /** + * @dataProvider runProvider + */ + public function testRun( $expected, $args ) { + $this->maintenance->loadWithArgv( $args ); + + $this->maintenance->execute(); + + $remainingLogs = wfGetDB( DB_REPLICA )->select( + [ 'logging' ], + [ 'log_type', 'log_action', 'log_user' ], + [], + __METHOD__, + [ 'ORDER BY' => 'log_id' ] + ); + + $this->assertEquals( $expected, iterator_to_array( $remainingLogs, false ) ); + } + + public function testFromId() { + $fromId = wfGetDB( DB_REPLICA )->selectField( + 'logging', + 'log_id', + [ 'log_params' => 'nanana' ] + ); + + $this->maintenance->loadWithArgv( [ '--sleep', '0', '--from-id', strval( $fromId ), '-q' ] ); + + $this->maintenance->execute(); + + $remainingLogs = wfGetDB( DB_REPLICA )->select( + [ 'logging' ], + [ 'log_type', 'log_action', 'log_user' ], + [], + __METHOD__, + [ 'ORDER BY' => 'log_id' ] + ); + + $deleted = [ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + 'log_user' => '7254', + ]; + $notDeleted = [ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + 'log_user' => '7252', + ]; + + $remainingLogs = array_map( + function ( $val ) { + return (array)$val; + }, + iterator_to_array( $remainingLogs, false ) + ); + + $this->assertNotContains( $deleted, $remainingLogs ); + $this->assertContains( $notDeleted, $remainingLogs ); + } + +} diff --git a/www/wiki/tests/phpunit/maintenance/fetchTextTest.php b/www/wiki/tests/phpunit/maintenance/fetchTextTest.php new file mode 100644 index 00000000..97e0c88f --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/fetchTextTest.php @@ -0,0 +1,272 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use ContentHandler; +use FetchText; +use MediaWikiTestCase; +use MWException; +use Title; +use PHPUnit_Framework_ExpectationFailedException; +use WikiPage; + +require_once __DIR__ . "/../../../maintenance/fetchText.php"; + +/** + * Mock for the input/output of FetchText + * + * FetchText internally tries to access stdin and stdout. We mock those aspects + * for testing. + */ +class SemiMockedFetchText extends FetchText { + + /** + * @var string|null Text to pass as stdin + */ + private $mockStdinText = null; + + /** + * @var bool Whether or not a text for stdin has been provided + */ + private $mockSetUp = false; + + /** + * @var array Invocation counters for the mocked aspects + */ + private $mockInvocations = [ 'getStdin' => 0 ]; + + /** + * Data for the fake stdin + * + * @param string $stdin The string to be used instead of stdin + */ + function mockStdin( $stdin ) { + $this->mockStdinText = $stdin; + $this->mockSetUp = true; + } + + /** + * Gets invocation counters for mocked methods. + * + * @return array An array, whose keys are function names. The corresponding values + * denote the number of times the function has been invoked. + */ + function mockGetInvocations() { + return $this->mockInvocations; + } + + // ----------------------------------------------------------------- + // Mocked functions from FetchText follow. + + function getStdin( $len = null ) { + $this->mockInvocations['getStdin']++; + if ( $len !== null ) { + throw new PHPUnit_Framework_ExpectationFailedException( + "Tried to get stdin with non null parameter" ); + } + + if ( !$this->mockSetUp ) { + throw new PHPUnit_Framework_ExpectationFailedException( + "Tried to get stdin before setting up rerouting" ); + } + + return fopen( 'data://text/plain,' . $this->mockStdinText, 'r' ); + } +} + +/** + * TestCase for FetchText + * + * @group Database + * @group Dump + * @covers FetchText + */ +class FetchTextTest extends MediaWikiTestCase { + + // We add 5 Revisions for this test. Their corresponding text id's + // are stored in the following 5 variables. + protected static $textId1; + protected static $textId2; + protected static $textId3; + protected static $textId4; + protected static $textId5; + + /** + * @var Exception|null As the current MediaWikiTestCase::run is not + * robust enough to recover from thrown exceptions directly, we cannot + * throw frow within addDBData, although it would be appropriate. Hence, + * we catch the exception and store it until we are in setUp and may + * finally rethrow the exception without crashing the test suite. + */ + protected static $exceptionFromAddDBDataOnce; + + /** + * @var FetchText The (mocked) FetchText that is to test + */ + private $fetchText; + + /** + * Adds a revision to a page, while returning the resuting text's id + * + * @param WikiPage $page The page to add the revision to + * @param string $text The revisions text + * @param string $summary The revisions summare + * @return int + * @throws MWException + */ + private function addRevision( $page, $text, $summary ) { + $status = $page->doEditContent( + ContentHandler::makeContent( $text, $page->getTitle() ), + $summary + ); + + if ( $status->isGood() ) { + $value = $status->getValue(); + $revision = $value['revision']; + $id = $revision->getTextId(); + + if ( $id > 0 ) { + return $id; + } + } + + throw new MWException( "Could not determine text id" ); + } + + function addDBDataOnce() { + $wikitextNamespace = $this->getDefaultWikitextNS(); + + try { + $title = Title::newFromText( 'FetchTextTestPage1', $wikitextNamespace ); + $page = WikiPage::factory( $title ); + self::$textId1 = $this->addRevision( + $page, + "FetchTextTestPage1Text1", + "FetchTextTestPage1Summary1" + ); + + $title = Title::newFromText( 'FetchTextTestPage2', $wikitextNamespace ); + $page = WikiPage::factory( $title ); + self::$textId2 = $this->addRevision( + $page, + "FetchTextTestPage2Text1", + "FetchTextTestPage2Summary1" + ); + self::$textId3 = $this->addRevision( + $page, + "FetchTextTestPage2Text2", + "FetchTextTestPage2Summary2" + ); + self::$textId4 = $this->addRevision( + $page, + "FetchTextTestPage2Text3", + "FetchTextTestPage2Summary3" + ); + self::$textId5 = $this->addRevision( + $page, + "FetchTextTestPage2Text4 some additional Text ", + "FetchTextTestPage2Summary4 extra " + ); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBDataOnce + self::$exceptionFromAddDBDataOnce = $e; + } + } + + protected function setUp() { + parent::setUp(); + + // Check if any Exception is stored for rethrowing from addDBData + if ( self::$exceptionFromAddDBDataOnce !== null ) { + throw self::$exceptionFromAddDBDataOnce; + } + + $this->fetchText = new SemiMockedFetchText(); + } + + /** + * Helper to relate FetchText's input and output + * @param string $input + * @param string $expectedOutput + */ + private function assertFilter( $input, $expectedOutput ) { + $this->fetchText->mockStdin( $input ); + $this->fetchText->execute(); + $invocations = $this->fetchText->mockGetInvocations(); + $this->assertEquals( 1, $invocations['getStdin'], + "getStdin invocation counter" ); + $this->expectOutputString( $expectedOutput ); + } + + // Instead of the following functions, a data provider would be great. + // However, as data providers are evaluated /before/ addDBData, a data + // provider would not know the required ids. + + function testExistingSimple() { + $this->assertFilter( self::$textId2, + self::$textId2 . "\n23\nFetchTextTestPage2Text1" ); + } + + function testExistingSimpleWithNewline() { + $this->assertFilter( self::$textId2 . "\n", + self::$textId2 . "\n23\nFetchTextTestPage2Text1" ); + } + + function testExistingSeveral() { + $this->assertFilter( + implode( "\n", [ + self::$textId1, + self::$textId5, + self::$textId3, + self::$textId3, + ] ), + implode( '', [ + self::$textId1 . "\n23\nFetchTextTestPage1Text1", + self::$textId5 . "\n44\nFetchTextTestPage2Text4 " + . "some additional Text", + self::$textId3 . "\n23\nFetchTextTestPage2Text2", + self::$textId3 . "\n23\nFetchTextTestPage2Text2" + ] ) ); + } + + function testEmpty() { + $this->assertFilter( "", null ); + } + + function testNonExisting() { + $this->assertFilter( self::$textId5 + 10, ( self::$textId5 + 10 ) . "\n-1\n" ); + } + + function testNegativeInteger() { + $this->assertFilter( "-42", "-42\n-1\n" ); + } + + function testFloatingPointNumberExisting() { + // float -> int -> revision + $this->assertFilter( self::$textId3 + 0.14159, + self::$textId3 . "\n23\nFetchTextTestPage2Text2" ); + } + + function testFloatingPointNumberNonExisting() { + $this->assertFilter( self::$textId5 + 3.14159, + ( self::$textId5 + 3 ) . "\n-1\n" ); + } + + function testCharacters() { + $this->assertFilter( "abc", "0\n-1\n" ); + } + + function testMix() { + $this->assertFilter( "ab\n" . self::$textId4 . ".5cd\n\nefg\n" . self::$textId2 + . "\n" . self::$textId3, + implode( "", [ + "0\n-1\n", + self::$textId4 . "\n23\nFetchTextTestPage2Text3", + "0\n-1\n", + "0\n-1\n", + self::$textId2 . "\n23\nFetchTextTestPage2Text1", + self::$textId3 . "\n23\nFetchTextTestPage2Text2" + ] ) ); + } +} |