<?php

require_once __DIR__ . "/../../../maintenance/dumpTextPass.php";

/**
 * Tests for TextPassDumper that rely on the database
 *
 * Some of these tests use the old constuctor for TextPassDumper
 * and the dump() function, while others use the new loadWithArgv( $args )
 * function and execute(). This is to ensure both the old and new methods
 * work properly.
 *
 * @group Database
 * @group Dump
 * @covers TextPassDumper
 */
class TextPassDumperDatabaseTest extends DumpTestCase {

	// We'll add several pages, revision and texts. The following variables hold the
	// corresponding ids.
	private $pageId1, $pageId2, $pageId3, $pageId4;
	private static $numOfPages = 4;
	private $revId1_1, $textId1_1;
	private $revId2_1, $textId2_1, $revId2_2, $textId2_2;
	private $revId2_3, $textId2_3, $revId2_4, $textId2_4;
	private $revId3_1, $textId3_1, $revId3_2, $textId3_2;
	private $revId4_1, $textId4_1;
	private static $numOfRevs = 8;

	function addDBData() {
		$this->tablesUsed[] = 'page';
		$this->tablesUsed[] = 'revision';
		$this->tablesUsed[] = 'text';

		$this->mergeMwGlobalArrayValue( 'wgContentHandlers', [
			"BackupTextPassTestModel" => "BackupTextPassTestModelHandler"
		] );

		$ns = $this->getDefaultWikitextNS();

		try {
			// Simple page
			$title = Title::newFromText( 'BackupDumperTestP1', $ns );
			$page = WikiPage::factory( $title );
			list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page,
				"BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" );
			$this->pageId1 = $page->getId();

			// Page with more than one revision
			$title = Title::newFromText( 'BackupDumperTestP2', $ns );
			$page = WikiPage::factory( $title );
			list( $this->revId2_1, $this->textId2_1 ) = $this->addRevision( $page,
				"BackupDumperTestP2Text1", "BackupDumperTestP2Summary1" );
			list( $this->revId2_2, $this->textId2_2 ) = $this->addRevision( $page,
				"BackupDumperTestP2Text2", "BackupDumperTestP2Summary2" );
			list( $this->revId2_3, $this->textId2_3 ) = $this->addRevision( $page,
				"BackupDumperTestP2Text3", "BackupDumperTestP2Summary3" );
			list( $this->revId2_4, $this->textId2_4 ) = $this->addRevision( $page,
				"BackupDumperTestP2Text4 some additional Text  ",
				"BackupDumperTestP2Summary4 extra " );
			$this->pageId2 = $page->getId();

			// Deleted page.
			$title = Title::newFromText( 'BackupDumperTestP3', $ns );
			$page = WikiPage::factory( $title );
			list( $this->revId3_1, $this->textId3_1 ) = $this->addRevision( $page,
				"BackupDumperTestP3Text1", "BackupDumperTestP2Summary1" );
			list( $this->revId3_2, $this->textId3_2 ) = $this->addRevision( $page,
				"BackupDumperTestP3Text2", "BackupDumperTestP2Summary2" );
			$this->pageId3 = $page->getId();
			$page->doDeleteArticle( "Testing ;)" );

			// Page from non-default namespace and model.
			// ExportTransform applies.

			if ( $ns === NS_TALK ) {
				// @todo work around this.
				throw new MWException( "The default wikitext namespace is the talk namespace. "
					. " We can't currently deal with that." );
			}

			$title = Title::newFromText( 'BackupDumperTestP1', NS_TALK );
			$page = WikiPage::factory( $title );
			list( $this->revId4_1, $this->textId4_1 ) = $this->addRevision( $page,
				"Talk about BackupDumperTestP1 Text1",
				"Talk BackupDumperTestP1 Summary1",
				"BackupTextPassTestModel" );
			$this->pageId4 = $page->getId();
		} catch ( Exception $e ) {
			// We'd love to pass $e directly. However, ... see
			// documentation of exceptionFromAddDBData in
			// DumpTestCase
			$this->exceptionFromAddDBData = $e;
		}
	}

	protected function setUp() {
		parent::setUp();

		// Since we will restrict dumping by page ranges (to allow
		// working tests, even if the db gets prepopulated by a base
		// class), we have to assert, that the page id are consecutively
		// increasing
		$this->assertEquals(
			[ $this->pageId2, $this->pageId3, $this->pageId4 ],
			[ $this->pageId1 + 1, $this->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( [ "--stub=file:" . $nameStub,
			"--output=file:" . $nameFull ] );
		$dumper->reporting = false;
		$dumper->setDB( $this->db );

		// Performing the dump
		$dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );

		// Checking for correctness of the dumped data
		$this->assertDumpStart( $nameFull );

		// Page 1
		$this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
		$this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
			$this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
			"BackupDumperTestP1Text1" );
		$this->assertPageEnd();

		// Page 2
		$this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
		$this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
			$this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
			"BackupDumperTestP2Text1" );
		$this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
			$this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
			"BackupDumperTestP2Text2", $this->revId2_1 );
		$this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
			$this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
			"BackupDumperTestP2Text3", $this->revId2_2 );
		$this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
			$this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
			"BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
		$this->assertPageEnd();

		// Page 3
		// -> Page is marked deleted. Hence not visible

		// Page 4
		$this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
		$this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
			$this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
			"TALK ABOUT BACKUPDUMPERTESTP1 TEXT1",
			false,
			"BackupTextPassTestModel",
			"text/plain" );
		$this->assertPageEnd();

		$this->assertDumpEnd();
	}

	function testPrefetchPlain() {
		// The mapping between ids and text, for the hits of the prefetch mock
		$prefetchMap = [
			[ $this->pageId1, $this->revId1_1, "Prefetch_________1Text1" ],
			[ $this->pageId2, $this->revId2_3, "Prefetch_________2Text3" ]
		];

		// The mock itself
		$prefetchMock = $this->getMockBuilder( 'BaseDump' )
			->setMethods( [ 'prefetch' ] )
			->disableOriginalConstructor()
			->getMock();
		$prefetchMock->expects( $this->exactly( 6 ) )
			->method( 'prefetch' )
			->will( $this->returnValueMap( $prefetchMap ) );

		// Setting up of the dump
		$nameStub = $this->setUpStub();
		$nameFull = $this->getNewTempFile();

		$dumper = new TextPassDumper( [ "--stub=file:" . $nameStub,
			"--output=file:" . $nameFull ] );

		$dumper->prefetch = $prefetchMock;
		$dumper->reporting = false;
		$dumper->setDB( $this->db );

		// Performing the dump
		$dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );

		// Checking for correctness of the dumped data
		$this->assertDumpStart( $nameFull );

		// Page 1
		$this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
		// Prefetch kicks in. This is still the SHA-1 of the original text,
		// But the actual text (with different SHA-1) comes from prefetch.
		$this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
			$this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
			"Prefetch_________1Text1" );
		$this->assertPageEnd();

		// Page 2
		$this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
		$this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
			$this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
			"BackupDumperTestP2Text1" );
		$this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
			$this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
			"BackupDumperTestP2Text2", $this->revId2_1 );
		// Prefetch kicks in. This is still the SHA-1 of the original text,
		// But the actual text (with different SHA-1) comes from prefetch.
		$this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
			$this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
			"Prefetch_________2Text3", $this->revId2_2 );
		$this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
			$this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
			"BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
		$this->assertPageEnd();

		// Page 3
		// -> Page is marked deleted. Hence not visible

		// Page 4
		$this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
		$this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
			$this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
			"TALK ABOUT BACKUPDUMPERTESTP1 TEXT1",
			false,
			"BackupTextPassTestModel",
			"text/plain" );
		$this->assertPageEnd();

		$this->assertDumpEnd();
	}

	/**
	 * Ensures that checkpoint dumps are used and written, by successively increasing the
	 * stub size and dumping until the duration crosses a threshold.
	 *
	 * @param string $checkpointFormat Either "file" for plain text or "gzip" for gzipped
	 *   checkpoint files.
	 */
	private function checkpointHelper( $checkpointFormat = "file" ) {
		// Getting temporary names
		$nameStub = $this->getNewTempFile();
		$nameOutputDir = $this->getNewTempDirectory();

		$stderr = fopen( 'php://output', 'a' );
		if ( $stderr === false ) {
			$this->fail( "Could not open stream for stderr" );
		}

		$iterations = 32; // We'll start with that many iterations of revisions
		// in stub. Make sure that the generated volume is above the buffer size
		// set below. Otherwise, the checkpointing does not trigger.
		$lastDuration = 0;
		$minDuration = 2; // We want the dump to take at least this many seconds
		$checkpointAfter = 0.5; // Generate checkpoint after this many seconds

		// Until a dump takes at least $minDuration seconds, perform a dump and check
		// duration. If the dump did not take long enough increase the iteration
		// count, to generate a bigger stub file next time.
		while ( $lastDuration < $minDuration ) {

			// Setting up the dump
			wfRecursiveRemoveDir( $nameOutputDir );
			$this->assertTrue( wfMkdirParents( $nameOutputDir ),
				"Creating temporary output directory " );
			$this->setUpStub( $nameStub, $iterations );
			$dumper = new TextPassDumper();
			$dumper->loadWithArgv( [ "--stub=file:" . $nameStub,
				"--output=" . $checkpointFormat . ":" . $nameOutputDir . "/full",
				"--maxtime=1" /*This is in minutes. Fixup is below*/,
				"--buffersize=32768", // The default of 32 iterations fill up 32KB about twice
				"--checkpointfile=checkpoint-%s-%s.xml.gz" ] );
			$dumper->setDB( $this->db );
			$dumper->maxTimeAllowed = $checkpointAfter; // Patching maxTime from 1 minute
			$dumper->stderr = $stderr;

			// The actual dump and taking time
			$ts_before = microtime( true );
			$dumper->execute();
			$ts_after = microtime( true );
			$lastDuration = $ts_after - $ts_before;

			// Handling increasing the iteration count for the stubs
			if ( $lastDuration < $minDuration ) {
				$old_iterations = $iterations;
				if ( $lastDuration > 0.2 ) {
					// lastDuration is big enough, to allow an educated guess
					$factor = ( $minDuration + 0.5 ) / $lastDuration;
					if ( ( $factor > 1.1 ) && ( $factor < 100 ) ) {
						// educated guess is reasonable
						$iterations = (int)( $iterations * $factor );
					}
				}

				if ( $old_iterations == $iterations ) {
					// Heuristics were not applied, so we just *2.
					$iterations *= 2;
				}

				$this->assertLessThan( 50000, $iterations,
					"Emergency stop against infinitely increasing iteration "
						. "count ( last duration: $lastDuration )" );
			}
		}

		// The dump (hopefully) did take long enough to produce more than one
		// checkpoint file.
		// We now check all the checkpoint files for validity.

		$files = scandir( $nameOutputDir );
		$this->assertTrue( asort( $files ), "Sorting files in temporary directory" );
		$fileOpened = false;
		$lookingForPage = 1;
		$checkpointFiles = 0;

		// Each run of the following loop body tries to handle exactly 1 /page/ (not
		// iteration of stub content). $i is only increased after having treated page 4.
		for ( $i = 0; $i < $iterations; ) {

			// 1. Assuring a file is opened and ready. Skipping across header if
			//    necessary.
			if ( !$fileOpened ) {
				$this->assertNotEmpty( $files, "No more existing dump files, "
					. "but not yet all pages found" );
				$fname = array_shift( $files );
				while ( $fname == "." || $fname == ".." ) {
					$this->assertNotEmpty( $files, "No more existing dump"
						. " files, but not yet all pages found" );
					$fname = array_shift( $files );
				}
				if ( $checkpointFormat == "gzip" ) {
					$this->gunzip( $nameOutputDir . "/" . $fname );
				}
				$this->assertDumpStart( $nameOutputDir . "/" . $fname );
				$fileOpened = true;
				$checkpointFiles++;
			}

			// 2. Performing a single page check
			switch ( $lookingForPage ) {
				case 1:
					// Page 1
					$this->assertPageStart( $this->pageId1 + $i * self::$numOfPages, NS_MAIN,
						"BackupDumperTestP1" );
					$this->assertRevision( $this->revId1_1 + $i * self::$numOfRevs, "BackupDumperTestP1Summary1",
						$this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
						"BackupDumperTestP1Text1" );
					$this->assertPageEnd();

					$lookingForPage = 2;
					break;

				case 2:
					// Page 2
					$this->assertPageStart( $this->pageId2 + $i * self::$numOfPages, NS_MAIN,
						"BackupDumperTestP2" );
					$this->assertRevision( $this->revId2_1 + $i * self::$numOfRevs, "BackupDumperTestP2Summary1",
						$this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
						"BackupDumperTestP2Text1" );
					$this->assertRevision( $this->revId2_2 + $i * self::$numOfRevs, "BackupDumperTestP2Summary2",
						$this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
						"BackupDumperTestP2Text2", $this->revId2_1 + $i * self::$numOfRevs );
					$this->assertRevision( $this->revId2_3 + $i * self::$numOfRevs, "BackupDumperTestP2Summary3",
						$this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
						"BackupDumperTestP2Text3", $this->revId2_2 + $i * self::$numOfRevs );
					$this->assertRevision( $this->revId2_4 + $i * self::$numOfRevs,
						"BackupDumperTestP2Summary4 extra",
						$this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
						"BackupDumperTestP2Text4 some additional Text",
						$this->revId2_3 + $i * self::$numOfRevs );
					$this->assertPageEnd();

					$lookingForPage = 4;
					break;

				case 4:
					// Page 4
					$this->assertPageStart( $this->pageId4 + $i * self::$numOfPages, NS_TALK,
						"Talk:BackupDumperTestP1" );
					$this->assertRevision( $this->revId4_1 + $i * self::$numOfRevs,
						"Talk BackupDumperTestP1 Summary1",
						$this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
						"TALK ABOUT BACKUPDUMPERTESTP1 TEXT1",
						false,
						"BackupTextPassTestModel",
						"text/plain" );
					$this->assertPageEnd();

					$lookingForPage = 1;

					// We dealt with the whole iteration.
					$i++;
					break;

				default:
					$this->fail( "Bad setting for lookingForPage ($lookingForPage)" );
			}

			// 3. Checking for the end of the current checkpoint file
			if ( $this->xml->nodeType == XMLReader::END_ELEMENT
				&& $this->xml->name == "mediawiki"
			) {
				$this->assertDumpEnd();
				$fileOpened = false;
			}
		}

		// Assuring we completely read all files ...
		$this->assertFalse( $fileOpened, "Currently read file still open?" );
		$this->assertEmpty( $files, "Remaining unchecked files" );

		// ... and have dealt with more than one checkpoint file
		$this->assertGreaterThan(
			1,
			$checkpointFiles,
			"expected more than 1 checkpoint to have been created. "
				. "Checkpoint interval is $checkpointAfter seconds, maybe your computer is too fast?"
		);

		$this->expectETAOutput();
	}

	/**
	 * Broken per T70653.
	 *
	 * @group large
	 * @group Broken
	 */
	function testCheckpointPlain() {
		$this->checkpointHelper();
	}

	/**
	 * tests for working checkpoint generation in gzip format work.
	 *
	 * We keep this test in addition to the simpler self::testCheckpointPlain, as there
	 * were once problems when the used sinks were DumpPipeOutputs.
	 *
	 * xmldumps-backup typically uses bzip2 instead of gzip. However, as bzip2 requires
	 * PHP extensions, we go for gzip instead, which triggers the same relevant code
	 * paths while still being testable on more systems.
	 *
	 * Broken per T70653.
	 *
	 * @group large
	 * @group Broken
	 */
	function testCheckpointGzip() {
		$this->checkHasGzip();
		$this->checkpointHelper( "gzip" );
	}

	/**
	 * Creates a stub file that is used for testing the text pass of dumps
	 *
	 * @param string $fname (Optional) Absolute name of the file to write
	 *   the stub into. If this parameter is null, a new temporary
	 *   file is generated that is automatically removed upon tearDown.
	 * @param int $iterations (Optional) specifies how often the block
	 *   of 3 pages should go into the stub file. The page and
	 *   revision id increase further and further, while the text
	 *   id of the first iteration is reused. The pages and revision
	 *   of iteration > 1 have no corresponding representation in the database.
	 * @return string Absolute filename of the stub
	 */
	private function setUpStub( $fname = null, $iterations = 1 ) {
		if ( $fname === null ) {
			$fname = $this->getNewTempFile();
		}
		$header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.10/" '
			. 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
			. 'xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.10/ '
			. 'http://www.mediawiki.org/xml/export-0.10.xsd" version="0.10" xml:lang="en">
  <siteinfo>
    <sitename>wikisvn</sitename>
    <base>http://localhost/wiki-svn/index.php/Main_Page</base>
    <generator>MediaWiki 1.21alpha</generator>
    <case>first-letter</case>
    <namespaces>
      <namespace key="-2" case="first-letter">Media</namespace>
      <namespace key="-1" case="first-letter">Special</namespace>
      <namespace key="0" case="first-letter" />
      <namespace key="1" case="first-letter">Talk</namespace>
      <namespace key="2" case="first-letter">User</namespace>
      <namespace key="3" case="first-letter">User talk</namespace>
      <namespace key="4" case="first-letter">Wikisvn</namespace>
      <namespace key="5" case="first-letter">Wikisvn talk</namespace>
      <namespace key="6" case="first-letter">File</namespace>
      <namespace key="7" case="first-letter">File talk</namespace>
      <namespace key="8" case="first-letter">MediaWiki</namespace>
      <namespace key="9" case="first-letter">MediaWiki talk</namespace>
      <namespace key="10" case="first-letter">Template</namespace>
      <namespace key="11" case="first-letter">Template talk</namespace>
      <namespace key="12" case="first-letter">Help</namespace>
      <namespace key="13" case="first-letter">Help talk</namespace>
      <namespace key="14" case="first-letter">Category</namespace>
      <namespace key="15" case="first-letter">Category talk</namespace>
    </namespaces>
  </siteinfo>
';
		$tail = '</mediawiki>
';

		$content = $header;
		$iterations = intval( $iterations );
		for ( $i = 0; $i < $iterations; $i++ ) {

			$page1 = '  <page>
    <title>BackupDumperTestP1</title>
    <ns>0</ns>
    <id>' . ( $this->pageId1 + $i * self::$numOfPages ) . '</id>
    <revision>
      <id>' . ( $this->revId1_1 + $i * self::$numOfRevs ) . '</id>
      <timestamp>2012-04-01T16:46:05Z</timestamp>
      <contributor>
        <ip>127.0.0.1</ip>
      </contributor>
      <comment>BackupDumperTestP1Summary1</comment>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text id="' . $this->textId1_1 . '" bytes="23" />
      <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
    </revision>
  </page>
';
			$page2 = '  <page>
    <title>BackupDumperTestP2</title>
    <ns>0</ns>
    <id>' . ( $this->pageId2 + $i * self::$numOfPages ) . '</id>
    <revision>
      <id>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</id>
      <timestamp>2012-04-01T16:46:05Z</timestamp>
      <contributor>
        <ip>127.0.0.1</ip>
      </contributor>
      <comment>BackupDumperTestP2Summary1</comment>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text id="' . $this->textId2_1 . '" bytes="23" />
      <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
    </revision>
    <revision>
      <id>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</id>
      <parentid>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</parentid>
      <timestamp>2012-04-01T16:46:05Z</timestamp>
      <contributor>
        <ip>127.0.0.1</ip>
      </contributor>
      <comment>BackupDumperTestP2Summary2</comment>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text id="' . $this->textId2_2 . '" bytes="23" />
      <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1>
    </revision>
    <revision>
      <id>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</id>
      <parentid>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</parentid>
      <timestamp>2012-04-01T16:46:05Z</timestamp>
      <contributor>
        <ip>127.0.0.1</ip>
      </contributor>
      <comment>BackupDumperTestP2Summary3</comment>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text id="' . $this->textId2_3 . '" bytes="23" />
      <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1>
    </revision>
    <revision>
      <id>' . ( $this->revId2_4 + $i * self::$numOfRevs ) . '</id>
      <parentid>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</parentid>
      <timestamp>2012-04-01T16:46:05Z</timestamp>
      <contributor>
        <ip>127.0.0.1</ip>
      </contributor>
      <comment>BackupDumperTestP2Summary4 extra</comment>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text id="' . $this->textId2_4 . '" bytes="44" />
      <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
    </revision>
  </page>
';
			// page 3 not in stub

			$page4 = '  <page>
    <title>Talk:BackupDumperTestP1</title>
    <ns>1</ns>
    <id>' . ( $this->pageId4 + $i * self::$numOfPages ) . '</id>
    <revision>
      <id>' . ( $this->revId4_1 + $i * self::$numOfRevs ) . '</id>
      <timestamp>2012-04-01T16:46:05Z</timestamp>
      <contributor>
        <ip>127.0.0.1</ip>
      </contributor>
      <comment>Talk BackupDumperTestP1 Summary1</comment>
      <model>BackupTextPassTestModel</model>
      <format>text/plain</format>
      <text id="' . $this->textId4_1 . '" bytes="35" />
      <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
    </revision>
  </page>
';
			$content .= $page1 . $page2 . $page4;
		}
		$content .= $tail;
		$this->assertEquals( strlen( $content ), file_put_contents(
			$fname, $content ), "Length of prepared stub" );

		return $fname;
	}
}

class BackupTextPassTestModelHandler extends TextContentHandler {

	public function __construct() {
		parent::__construct( 'BackupTextPassTestModel' );
	}

	public function exportTransform( $text, $format = null ) {
		return strtoupper( $text );
	}

}

/**
 * Tests for TextPassDumper that do not rely on the database
 *
 * (As the Database group is only detected at class level (not method level), we
 * cannot bring this test case's tests into the above main test case.)
 *
 * @group Dump
 * @covers TextPassDumper
 */
class TextPassDumperDatabaselessTest extends MediaWikiLangTestCase {
	/**
	 * Ensures that setting the buffer size is effective.
	 *
	 * @dataProvider bufferSizeProvider
	 */
	function testBufferSizeSetting( $expected, $size, $msg ) {
		$dumper = new TextPassDumperAccessor();
		$dumper->loadWithArgv( [ "--buffersize=" . $size ] );
		$dumper->execute();
		$this->assertEquals( $expected, $dumper->getBufferSize(), $msg );
	}

	/**
	 * Ensures that setting the buffer size is effective.
	 *
	 * @dataProvider bufferSizeProvider
	 */
	function bufferSizeProvider() {
		// expected, bufferSize to initialize with, message
		return [
			[ 512 * 1024, 512 * 1024, "Setting 512KB is not effective" ],
			[ 8192, 8192, "Setting 8KB is not effective" ],
			[ 4096, 2048, "Could set buffer size below lower bound" ]
		];
	}
}

/**
 * Accessor for internal state of TextPassDumper
 *
 * Do not warrentless add getters here.
 */
class TextPassDumperAccessor extends TextPassDumper {
	/**
	 * Gets the bufferSize.
	 *
	 * If bufferSize setting does not work correctly, testCheckpoint... tests
	 * fail and point in the wrong direction. To aid in troubleshooting when
	 * testCheckpoint... tests break at some point in the future, we test the
	 * bufferSize setting, hence need this accessor.
	 *
	 * (Yes, bufferSize is internal state of the TextPassDumper, but aiding
	 * debugging of testCheckpoint... in the future seems to be worth testing
	 * against it nonetheless.)
	 */
	public function getBufferSize() {
		return $this->bufferSize;
	}

	function dump( $history, $text = null ) {
		return true;
	}
}
