diff options
Diffstat (limited to 'tests/phpunit/maintenance')
-rw-r--r-- | tests/phpunit/maintenance/DumpTestCase.php | 386 | ||||
-rw-r--r-- | tests/phpunit/maintenance/MaintenanceTest.php | 830 | ||||
-rw-r--r-- | tests/phpunit/maintenance/backupPrefetchTest.php | 277 | ||||
-rw-r--r-- | tests/phpunit/maintenance/backupTextPassTest.php | 584 | ||||
-rw-r--r-- | tests/phpunit/maintenance/backup_LogTest.php | 225 | ||||
-rw-r--r-- | tests/phpunit/maintenance/backup_PageTest.php | 428 | ||||
-rw-r--r-- | tests/phpunit/maintenance/fetchTextTest.php | 261 |
7 files changed, 2991 insertions, 0 deletions
diff --git a/tests/phpunit/maintenance/DumpTestCase.php b/tests/phpunit/maintenance/DumpTestCase.php new file mode 100644 index 00000000..8b6aef53 --- /dev/null +++ b/tests/phpunit/maintenance/DumpTestCase.php @@ -0,0 +1,386 @@ +<?php + +/** + * 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; + + /** + * 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 summare + * @return array + * @throws MWException + */ + protected function addRevision( Page $page, $text, $summary ) { + $status = $page->doEditContent( + ContentHandler::makeContent( $text, $page->getTitle() ), + $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 array( $revision_id, $text_id ); + } + } + + throw new MWException( "Could not determine revision id (" . $status->getWikiText() . ")" ); + } + + /** + * 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(); + + 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 ); + + $this->assertTextNode( "model", $model ); + $this->skipWhitespace(); + + $this->assertTextNode( "format", $format ); + $this->skipWhitespace(); + + 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/tests/phpunit/maintenance/MaintenanceTest.php b/tests/phpunit/maintenance/MaintenanceTest.php new file mode 100644 index 00000000..e2fc8247 --- /dev/null +++ b/tests/phpunit/maintenance/MaintenanceTest.php @@ -0,0 +1,830 @@ +<?php + +// It would be great if we were able to use PHPUnit's getMockForAbstractClass +// instead of the MaintenanceFixup hack below. However, we cannot do +// without changing the visibility and without working around hacks in +// Maintenance.php +// +// For the same reason, we cannot just use FakeMaintenance. + +/** + * makes parts of the API of Maintenance that is hidden by protected visibily + * visible for testing, and makes up for a stream closing hack in Maintenance.php. + * + * This class is solely used for being able to test Maintenance right now + * without having to apply major refactorings to fix some design issues in + * Maintenance.php. Before adding more functions here, please consider whether + * this approach is correct, or a refactoring Maintenance to separate concers + * is more appropriate. + * + * Upon refactoring, keep in mind that besides the maintenance scrits themselves + * and tests right here, also at least Extension:Maintenance make use of + * Maintenance. + * + * Due to a hack in Maintenance.php using register_shutdown_function, be sure to + * finally call simulateShutdown on MaintenanceFixup instance before a test + * ends. + * + */ +class MaintenanceFixup extends Maintenance { + + // --- Making up for the register_shutdown_function hack in Maintenance.php + + /** + * The test case that generated this instance. + * + * This member is motivated by allowing the destructor to check whether or not + * the test failed, in order to avoid unnecessary nags about omitted shutdown + * simulation. + * But as it is already available, we also usi it to flagging tests as failed + * + * @var MediaWikiTestCase + */ + private $testCase; + + /** + * shutdownSimulated === true if simulateShutdown has done it's work + * + * @var bool + */ + private $shutdownSimulated = false; + + /** + * Simulates what Maintenance wants to happen at script's end. + */ + public function simulateShutdown() { + + if ( $this->shutdownSimulated ) { + $this->testCase->fail( __METHOD__ . " called more than once" ); + } + + // The cleanup action. + $this->outputChanneled( false ); + + // Bookkeeping that we simulated the clean up. + $this->shutdownSimulated = true; + } + + // Note that the "public" here does not change visibility + public function outputChanneled( $msg, $channel = null ) { + if ( $this->shutdownSimulated ) { + if ( $msg !== false ) { + $this->testCase->fail( "Already past simulated shutdown, but msg is " + . "not false. Did the hack in Maintenance.php change? Please " + . "adapt the test case or Maintenance.php" ); + } + + // The current call is the one registered via register_shutdown_function. + // We can safely ignore it, as we simulated this one via simulateShutdown + // before (if we did not, the destructor of this instance will warn about + // it) + return; + } + + call_user_func_array( array( "parent", __FUNCTION__ ), func_get_args() ); + } + + /** + * Safety net around register_shutdown_function of Maintenance.php + */ + public function __destruct() { + if ( !$this->shutdownSimulated ) { + // Someone generated a MaintenanceFixup instance without calling + // simulateShutdown. We'd have to raise a PHPUnit exception to correctly + // flag this illegal usage. However, we are already in a destruktor, which + // would trigger undefined behavior. Hence, we can only report to the + // error output :( Hopefully people read the PHPUnit output. + $name = $this->testCase->getName(); + fwrite( STDERR, "ERROR! Instance of " . __CLASS__ . " for test $name " + . "destructed without calling simulateShutdown method. Call " + . "simulateShutdown on the instance before it gets destructed." ); + } + + // The following guard is required, as PHP does not offer default destructors :( + if ( is_callable( "parent::__destruct" ) ) { + parent::__destruct(); + } + } + + public function __construct( MediaWikiTestCase $testCase ) { + parent::__construct(); + $this->testCase = $testCase; + } + + // --- Making protected functions visible for test + + public function output( $out, $channel = null ) { + // Just to make PHP not nag about signature mismatches, we copied + // Maintenance::output signature. However, we do not use (or rely on) + // those variables. Instead we pass to Maintenance::output whatever we + // receive at runtime. + return call_user_func_array( array( "parent", __FUNCTION__ ), func_get_args() ); + } + + // --- Requirements for getting instance of abstract class + + public function execute() { + $this->testCase->fail( __METHOD__ . " called unexpectedly" ); + } +} + +/** + * @covers Maintenance + */ +class MaintenanceTest extends MediaWikiTestCase { + + /** + * The main Maintenance instance that is used for testing. + * + * @var MaintenanceFixup + */ + private $m; + + protected function setUp() { + parent::setUp(); + $this->m = new MaintenanceFixup( $this ); + } + + protected function tearDown() { + if ( $this->m ) { + $this->m->simulateShutdown(); + $this->m = null; + } + parent::tearDown(); + } + + /** + * asserts the output before and after simulating shutdown + * + * This function simulates shutdown of self::m. + * + * @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. + */ + private function assertOutputPrePostShutdown( $preShutdownOutput, $expectNLAppending ) { + + $this->assertEquals( $preShutdownOutput, $this->getActualOutput(), + "Output before shutdown simulation" ); + + $this->m->simulateShutdown(); + $this->m = null; + + $postShutdownOutput = $preShutdownOutput . ( $expectNLAppending ? "\n" : "" ); + $this->expectOutputString( $postShutdownOutput ); + } + + // 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. + + function testOutputEmpty() { + $this->m->output( "" ); + $this->assertOutputPrePostShutdown( "", false ); + } + + function testOutputString() { + $this->m->output( "foo" ); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testOutputStringString() { + $this->m->output( "foo" ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputStringNL() { + $this->m->output( "foo\n" ); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testOutputStringNLNL() { + $this->m->output( "foo\n\n" ); + $this->assertOutputPrePostShutdown( "foo\n\n", false ); + } + + function testOutputStringNLString() { + $this->m->output( "foo\nbar" ); + $this->assertOutputPrePostShutdown( "foo\nbar", false ); + } + + function testOutputStringNLStringNL() { + $this->m->output( "foo\nbar\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputStringNLStringNLLinewise() { + $this->m->output( "foo\n" ); + $this->m->output( "bar\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputStringNLStringNLArbitrary() { + $this->m->output( "" ); + $this->m->output( "foo" ); + $this->m->output( "" ); + $this->m->output( "\n" ); + $this->m->output( "ba" ); + $this->m->output( "" ); + $this->m->output( "r\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputStringNLStringNLArbitraryAgain() { + $this->m->output( "" ); + $this->m->output( "foo" ); + $this->m->output( "" ); + $this->m->output( "\nb" ); + $this->m->output( "a" ); + $this->m->output( "" ); + $this->m->output( "r\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWNullChannelEmpty() { + $this->m->output( "", null ); + $this->assertOutputPrePostShutdown( "", false ); + } + + function testOutputWNullChannelString() { + $this->m->output( "foo", null ); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testOutputWNullChannelStringString() { + $this->m->output( "foo", null ); + $this->m->output( "bar", null ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputWNullChannelStringNL() { + $this->m->output( "foo\n", null ); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testOutputWNullChannelStringNLNL() { + $this->m->output( "foo\n\n", null ); + $this->assertOutputPrePostShutdown( "foo\n\n", false ); + } + + function testOutputWNullChannelStringNLString() { + $this->m->output( "foo\nbar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar", false ); + } + + function testOutputWNullChannelStringNLStringNL() { + $this->m->output( "foo\nbar\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWNullChannelStringNLStringNLLinewise() { + $this->m->output( "foo\n", null ); + $this->m->output( "bar\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWNullChannelStringNLStringNLArbitrary() { + $this->m->output( "", null ); + $this->m->output( "foo", null ); + $this->m->output( "", null ); + $this->m->output( "\n", null ); + $this->m->output( "ba", null ); + $this->m->output( "", null ); + $this->m->output( "r\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWNullChannelStringNLStringNLArbitraryAgain() { + $this->m->output( "", null ); + $this->m->output( "foo", null ); + $this->m->output( "", null ); + $this->m->output( "\nb", null ); + $this->m->output( "a", null ); + $this->m->output( "", null ); + $this->m->output( "r\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWChannelString() { + $this->m->output( "foo", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo", true ); + } + + function testOutputWChannelStringNL() { + $this->m->output( "foo\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo", true ); + } + + function testOutputWChannelStringNLNL() { + // 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) + $this->m->output( "foo\n\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\n", true ); + } + + function testOutputWChannelStringNLString() { + $this->m->output( "foo\nbar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputWChannelStringNLStringNL() { + $this->m->output( "foo\nbar\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputWChannelStringNLStringNLLinewise() { + $this->m->output( "foo\n", "bazChannel" ); + $this->m->output( "bar\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputWChannelStringNLStringNLArbitrary() { + $this->m->output( "", "bazChannel" ); + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "\n", "bazChannel" ); + $this->m->output( "ba", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "r\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputWChannelStringNLStringNLArbitraryAgain() { + $this->m->output( "", "bazChannel" ); + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "\nb", "bazChannel" ); + $this->m->output( "a", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "r\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputWMultipleChannelsChannelChange() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "bar", "bazChannel" ); + $this->m->output( "qux", "quuxChannel" ); + $this->m->output( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", true ); + } + + function testOutputWMultipleChannelsChannelChangeNL() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "bar\n", "bazChannel" ); + $this->m->output( "qux\n", "quuxChannel" ); + $this->m->output( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", true ); + } + + function testOutputWAndWOChannelStringStartWO() { + $this->m->output( "foo" ); + $this->m->output( "bar", "bazChannel" ); + $this->m->output( "qux" ); + $this->m->output( "quux", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nquxquux", true ); + } + + function testOutputWAndWOChannelStringStartW() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "bar" ); + $this->m->output( "qux", "bazChannel" ); + $this->m->output( "quux" ); + $this->assertOutputPrePostShutdown( "foo\nbarqux\nquux", false ); + } + + function testOutputWChannelTypeSwitch() { + $this->m->output( "foo", 1 ); + $this->m->output( "bar", 1.0 ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputIntermittentEmpty() { + $this->m->output( "foo" ); + $this->m->output( "" ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputIntermittentFalse() { + $this->m->output( "foo" ); + $this->m->output( false ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputIntermittentFalseAfterOtherChannel() { + $this->m->output( "qux", "quuxChannel" ); + $this->m->output( "foo" ); + $this->m->output( false ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "qux\nfoobar", false ); + } + + function testOutputWNullChannelIntermittentEmpty() { + $this->m->output( "foo", null ); + $this->m->output( "", null ); + $this->m->output( "bar", null ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputWNullChannelIntermittentFalse() { + $this->m->output( "foo", null ); + $this->m->output( false, null ); + $this->m->output( "bar", null ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputWChannelIntermittentEmpty() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputWChannelIntermittentFalse() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( false, "bazChannel" ); + $this->m->output( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + // Note that (per documentation) outputChanneled does take strings that end + // in \n, hence we do not test such strings. + + function testOutputChanneledEmpty() { + $this->m->outputChanneled( "" ); + $this->assertOutputPrePostShutdown( "\n", false ); + } + + function testOutputChanneledString() { + $this->m->outputChanneled( "foo" ); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testOutputChanneledStringString() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "bar" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledStringNLString() { + $this->m->outputChanneled( "foo\nbar" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledStringNLStringNLArbitraryAgain() { + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "\nb" ); + $this->m->outputChanneled( "a" ); + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "r" ); + $this->assertOutputPrePostShutdown( "\nfoo\n\n\nb\na\n\nr\n", false ); + } + + function testOutputChanneledWNullChannelEmpty() { + $this->m->outputChanneled( "", null ); + $this->assertOutputPrePostShutdown( "\n", false ); + } + + function testOutputChanneledWNullChannelString() { + $this->m->outputChanneled( "foo", null ); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testOutputChanneledWNullChannelStringString() { + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( "bar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledWNullChannelStringNLString() { + $this->m->outputChanneled( "foo\nbar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledWNullChannelStringNLStringNLArbitraryAgain() { + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "\nb", null ); + $this->m->outputChanneled( "a", null ); + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "r", null ); + $this->assertOutputPrePostShutdown( "\nfoo\n\n\nb\na\n\nr\n", false ); + } + + function testOutputChanneledWChannelString() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo", true ); + } + + function testOutputChanneledWChannelStringNLString() { + $this->m->outputChanneled( "foo\nbar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputChanneledWChannelStringString() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputChanneledWChannelStringNLStringNLArbitraryAgain() { + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "\nb", "bazChannel" ); + $this->m->outputChanneled( "a", "bazChannel" ); + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "r", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputChanneledWMultipleChannelsChannelChange() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->m->outputChanneled( "qux", "quuxChannel" ); + $this->m->outputChanneled( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", true ); + } + + function testOutputChanneledWMultipleChannelsChannelChangeEnclosedNull() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", null ); + $this->m->outputChanneled( "qux", null ); + $this->m->outputChanneled( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\ncorge", true ); + } + + function testOutputChanneledWMultipleChannelsChannelAfterNullChange() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", null ); + $this->m->outputChanneled( "qux", null ); + $this->m->outputChanneled( "corge", "quuxChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\ncorge", true ); + } + + function testOutputChanneledWAndWOChannelStringStartWO() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->m->outputChanneled( "qux" ); + $this->m->outputChanneled( "quux", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\nquux", true ); + } + + function testOutputChanneledWAndWOChannelStringStartW() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar" ); + $this->m->outputChanneled( "qux", "bazChannel" ); + $this->m->outputChanneled( "quux" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\nquux\n", false ); + } + + function testOutputChanneledWChannelTypeSwitch() { + $this->m->outputChanneled( "foo", 1 ); + $this->m->outputChanneled( "bar", 1.0 ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputChanneledWOChannelIntermittentEmpty() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "bar" ); + $this->assertOutputPrePostShutdown( "foo\n\nbar\n", false ); + } + + function testOutputChanneledWOChannelIntermittentFalse() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( false ); + $this->m->outputChanneled( "bar" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledWNullChannelIntermittentEmpty() { + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "bar", null ); + $this->assertOutputPrePostShutdown( "foo\n\nbar\n", false ); + } + + function testOutputChanneledWNullChannelIntermittentFalse() { + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( false, null ); + $this->m->outputChanneled( "bar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledWChannelIntermittentEmpty() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputChanneledWChannelIntermittentFalse() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( false, "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testCleanupChanneledClean() { + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "", false ); + } + + function testCleanupChanneledAfterOutput() { + $this->m->output( "foo" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testCleanupChanneledAfterOutputWNullChannel() { + $this->m->output( "foo", null ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testCleanupChanneledAfterOutputWChannel() { + $this->m->output( "foo", "bazChannel" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterNLOutput() { + $this->m->output( "foo\n" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterNLOutputWNullChannel() { + $this->m->output( "foo\n", null ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterNLOutputWChannel() { + $this->m->output( "foo\n", "bazChannel" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterOutputChanneledWOChannel() { + $this->m->outputChanneled( "foo" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterOutputChanneledWNullChannel() { + $this->m->outputChanneled( "foo", null ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterOutputChanneledWChannel() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutput() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo" ); + $m2->output( "bar" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWNullChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo", null ); + $m2->output( "bar", null ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo", "bazChannel" ); + $m2->output( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n", true ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWNullChannelNL() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo\n", null ); + $m2->output( "bar\n", null ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWChannelNL() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo\n", "bazChannel" ); + $m2->output( "bar\n", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n", true ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneled() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo" ); + $m2->outputChanneled( "bar" ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneledWNullChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo", null ); + $m2->outputChanneled( "bar", null ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneledWChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo", "bazChannel" ); + $m2->outputChanneled( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n", true ); + } + + function testMultipleMaintenanceObjectsInteractionCleanupChanneledWChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo", "bazChannel" ); + $m2->outputChanneled( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before first cleanup" ); + $this->m->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->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n\n", false ); + } + + /** + * @covers Maintenance::getConfig + */ + public function testGetConfig() { + $this->assertInstanceOf( 'Config', $this->m->getConfig() ); + $this->assertSame( ConfigFactory::getDefaultInstance()->makeConfig( 'main' ), $this->m->getConfig() ); + } + + /** + * @covers Maintenance::setConfig + */ + public function testSetConfig() { + $conf = $this->getMock( 'Config' ); + $this->m->setConfig( $conf ); + $this->assertSame( $conf, $this->m->getConfig() ); + } +} diff --git a/tests/phpunit/maintenance/backupPrefetchTest.php b/tests/phpunit/maintenance/backupPrefetchTest.php new file mode 100644 index 00000000..5e0fe89d --- /dev/null +++ b/tests/phpunit/maintenance/backupPrefetchTest.php @@ -0,0 +1,277 @@ +<?php + +require_once __DIR__ . "/../../../maintenance/backupPrefetch.inc"; + +/** + * 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(); + } + + // Bug 37458, 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( array( 1 ) ); + $fname2 = $this->setUpPrefetch( array( 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( array( 1 ) ); + $fname2 = $this->setUpPrefetch( array( 2 ) ); + $fname3 = $this->setUpPrefetch( array( 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( array( 1 ) ); + $fname2 = $this->setUpPrefetch( array( 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 = array( 1, 2, 4 ) ) { + // The file name, where we store the prepared prefetch file + $fname = $this->getNewTempFile(); + + // The header of every prefetch file + // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + $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> +'; + // @codingStandardsIgnoreEnd + + // An array holding the pages that are available for prefetch + $available_pages = array(); + + // 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/tests/phpunit/maintenance/backupTextPassTest.php b/tests/phpunit/maintenance/backupTextPassTest.php new file mode 100644 index 00000000..a37a97c7 --- /dev/null +++ b/tests/phpunit/maintenance/backupTextPassTest.php @@ -0,0 +1,584 @@ +<?php + +require_once __DIR__ . "/../../../maintenance/backupTextPass.inc"; + +/** + * Tests for page dumps of BackupDumper + * + * @group Database + * @group Dump + * @covers TextPassDumper + */ +class TextPassDumperTest 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[] = 'text'; + + $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 + + 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" ); + $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( + array( $this->pageId2, $this->pageId3, $this->pageId4 ), + array( $this->pageId1 + 1, $this->pageId2 + 1, $this->pageId3 + 1 ), + "Page ids increasing without holes" ); + } + + function testPlain() { + // Setting up the dump + $nameStub = $this->setUpStub(); + $nameFull = $this->getNewTempFile(); + $dumper = new TextPassDumper( array( "--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" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testPrefetchPlain() { + // The mapping between ids and text, for the hits of the prefetch mock + $prefetchMap = array( + array( $this->pageId1, $this->revId1_1, "Prefetch_________1Text1" ), + array( $this->pageId2, $this->revId2_3, "Prefetch_________2Text3" ) + ); + + // The mock itself + $prefetchMock = $this->getMock( 'BaseDump', array( 'prefetch' ), array(), '', false ); + $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( array( "--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" ); + $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 + $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( array( "--stub=file:" . $nameStub, + "--output=" . $checkpointFormat . ":" . $nameOutputDir . "/full", + "--maxtime=1" /*This is in minutes. Fixup is below*/, + "--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->dump( WikiExporter::FULL, WikiExporter::TEXT ); + $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" ); + $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(); + } + + /** + * @group large + */ + 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. + * + * @group large + */ + 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.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> +'; + $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> + <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1> + <model>wikitext</model> + <format>text/x-wiki</format> + <text id="' . $this->textId1_1 . '" bytes="23" /> + </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> + <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1> + <model>wikitext</model> + <format>text/x-wiki</format> + <text id="' . $this->textId2_1 . '" bytes="23" /> + </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> + <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1> + <model>wikitext</model> + <format>text/x-wiki</format> + <text id="' . $this->textId2_2 . '" bytes="23" /> + </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> + <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1> + <model>wikitext</model> + <format>text/x-wiki</format> + <text id="' . $this->textId2_3 . '" bytes="23" /> + </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> + <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1> + <model>wikitext</model> + <format>text/x-wiki</format> + <text id="' . $this->textId2_4 . '" bytes="44" /> + </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> + <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1> + <model>wikitext</model> + <format>text/x-wiki</format> + <text id="' . $this->textId4_1 . '" bytes="35" /> + </revision> + </page> +'; + $content .= $page1 . $page2 . $page4; + } + $content .= $tail; + $this->assertEquals( strlen( $content ), file_put_contents( + $fname, $content ), "Length of prepared stub" ); + + return $fname; + } +} diff --git a/tests/phpunit/maintenance/backup_LogTest.php b/tests/phpunit/maintenance/backup_LogTest.php new file mode 100644 index 00000000..7ca45960 --- /dev/null +++ b/tests/phpunit/maintenance/backup_LogTest.php @@ -0,0 +1,225 @@ +<?php +/** + * Tests for log dumps of BackupDumper + * + * @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", + array( '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 = array() + ) { + + $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 BackupDumper( array( "--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", array( 'key1' => 1, 3 => 'value3' ) ); + + $this->assertDumpEnd(); + } + + function testXmlDumpsBackupUseCaseLogging() { + global $wgContLang; + + $this->checkHasGzip(); + + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--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->dump( WikiExporter::LOGS, WikiExporter::TEXT ); + + $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", array( '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/tests/phpunit/maintenance/backup_PageTest.php b/tests/phpunit/maintenance/backup_PageTest.php new file mode 100644 index 00000000..0cb0cdb6 --- /dev/null +++ b/tests/phpunit/maintenance/backup_PageTest.php @@ -0,0 +1,428 @@ +<?php +/** + * 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, $pageId5; + private $pageTitle1, $pageTitle2, $pageTitle3, $pageTitle4, $pageTitle5; + 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( array( + 'wgLanguageCode' => 'en', + 'wgContLang' => Language::factory( 'en' ), + ) ); + + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $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( + array( $this->pageId2, $this->pageId3, $this->pageId4 ), + array( $this->pageId1 + 1, $this->pageId2 + 1, $this->pageId3 + 1 ), + "Page ids increasing without holes" ); + } + + function testFullTextPlain() { + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--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::FULL, WikiExporter::TEXT ); + + // 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 BackupDumper( array( "--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::FULL, 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_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 BackupDumper( array( "--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 BackupDumper( array( "--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(); + } + + function testXmlDumpsBackupUseCase() { + // 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. + // + $this->checkHasGzip(); + + $fnameMetaHistory = $this->getNewTempFile(); + $fnameMetaCurrent = $this->getNewTempFile(); + $fnameArticles = $this->getNewTempFile(); + + $dumper = new BackupDumper( array( "--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/tests/phpunit/maintenance/fetchTextTest.php b/tests/phpunit/maintenance/fetchTextTest.php new file mode 100644 index 00000000..4e38418a --- /dev/null +++ b/tests/phpunit/maintenance/fetchTextTest.php @@ -0,0 +1,261 @@ +<?php + +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 = array( '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. + private $textId1; + private $textId2; + private $textId3; + private $textId4; + private $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. + */ + private $exceptionFromAddDBData; + + /** + * @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 addDBData() { + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'text'; + + $wikitextNamespace = $this->getDefaultWikitextNS(); + + try { + $title = Title::newFromText( 'FetchTextTestPage1', $wikitextNamespace ); + $page = WikiPage::factory( $title ); + $this->textId1 = $this->addRevision( + $page, + "FetchTextTestPage1Text1", + "FetchTextTestPage1Summary1" + ); + + $title = Title::newFromText( 'FetchTextTestPage2', $wikitextNamespace ); + $page = WikiPage::factory( $title ); + $this->textId2 = $this->addRevision( + $page, + "FetchTextTestPage2Text1", + "FetchTextTestPage2Summary1" + ); + $this->textId3 = $this->addRevision( + $page, + "FetchTextTestPage2Text2", + "FetchTextTestPage2Summary2" + ); + $this->textId4 = $this->addRevision( + $page, + "FetchTextTestPage2Text3", + "FetchTextTestPage2Summary3" + ); + $this->textId5 = $this->addRevision( + $page, + "FetchTextTestPage2Text4 some additional Text ", + "FetchTextTestPage2Summary4 extra " + ); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData + $this->exceptionFromAddDBData = $e; + } + } + + protected function setUp() { + parent::setUp(); + + // Check if any Exception is stored for rethrowing from addDBData + if ( $this->exceptionFromAddDBData !== null ) { + throw $this->exceptionFromAddDBData; + } + + $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( $this->textId2, + $this->textId2 . "\n23\nFetchTextTestPage2Text1" ); + } + + function testExistingSimpleWithNewline() { + $this->assertFilter( $this->textId2 . "\n", + $this->textId2 . "\n23\nFetchTextTestPage2Text1" ); + } + + function testExistingSeveral() { + $this->assertFilter( "$this->textId1\n$this->textId5\n" + . "$this->textId3\n$this->textId3", + implode( "", array( + $this->textId1 . "\n23\nFetchTextTestPage1Text1", + $this->textId5 . "\n44\nFetchTextTestPage2Text4 " + . "some additional Text", + $this->textId3 . "\n23\nFetchTextTestPage2Text2", + $this->textId3 . "\n23\nFetchTextTestPage2Text2" + ) ) ); + } + + function testEmpty() { + $this->assertFilter( "", null ); + } + + function testNonExisting() { + $this->assertFilter( $this->textId5 + 10, ( $this->textId5 + 10 ) . "\n-1\n" ); + } + + function testNegativeInteger() { + $this->assertFilter( "-42", "-42\n-1\n" ); + } + + function testFloatingPointNumberExisting() { + // float -> int -> revision + $this->assertFilter( $this->textId3 + 0.14159, + $this->textId3 . "\n23\nFetchTextTestPage2Text2" ); + } + + function testFloatingPointNumberNonExisting() { + $this->assertFilter( $this->textId5 + 3.14159, + ( $this->textId5 + 3 ) . "\n-1\n" ); + } + + function testCharacters() { + $this->assertFilter( "abc", "0\n-1\n" ); + } + + function testMix() { + $this->assertFilter( "ab\n" . $this->textId4 . ".5cd\n\nefg\n" . $this->textId2 + . "\n" . $this->textId3, + implode( "", array( + "0\n-1\n", + $this->textId4 . "\n23\nFetchTextTestPage2Text3", + "0\n-1\n", + "0\n-1\n", + $this->textId2 . "\n23\nFetchTextTestPage2Text1", + $this->textId3 . "\n23\nFetchTextTestPage2Text2" + ) ) ); + } +} |