Merge "Choose parentids in tests as they are in real dumps"
[lhc/web/wiklou.git] / tests / phpunit / maintenance / backupTextPassTest.php
1 <?php
2
3 require_once dirname( __FILE__ ) . "/../../../maintenance/backupTextPass.inc";
4
5 /**
6 * Tests for page dumps of BackupDumper
7 *
8 * @group Database
9 * @group Dump
10 */
11 class TextPassDumperTest extends DumpTestCase {
12
13 // We'll add several pages, revision and texts. The following variables hold the
14 // corresponding ids.
15 private $pageId1, $pageId2, $pageId3, $pageId4;
16 private static $numOfPages = 4;
17 private $revId1_1, $textId1_1;
18 private $revId2_1, $textId2_1, $revId2_2, $textId2_2;
19 private $revId2_3, $textId2_3, $revId2_4, $textId2_4;
20 private $revId3_1, $textId3_1, $revId3_2, $textId3_2;
21 private $revId4_1, $textId4_1;
22 private static $numOfRevs = 8;
23
24 function addDBData() {
25 $this->tablesUsed[] = 'page';
26 $this->tablesUsed[] = 'revision';
27 $this->tablesUsed[] = 'text';
28
29 try {
30 // Simple page
31 $title = Title::newFromText( 'BackupDumperTestP1' );
32 $page = WikiPage::factory( $title );
33 list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page,
34 "BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" );
35 $this->pageId1 = $page->getId();
36
37 // Page with more than one revision
38 $title = Title::newFromText( 'BackupDumperTestP2' );
39 $page = WikiPage::factory( $title );
40 list( $this->revId2_1, $this->textId2_1 ) = $this->addRevision( $page,
41 "BackupDumperTestP2Text1", "BackupDumperTestP2Summary1" );
42 list( $this->revId2_2, $this->textId2_2 ) = $this->addRevision( $page,
43 "BackupDumperTestP2Text2", "BackupDumperTestP2Summary2" );
44 list( $this->revId2_3, $this->textId2_3 ) = $this->addRevision( $page,
45 "BackupDumperTestP2Text3", "BackupDumperTestP2Summary3" );
46 list( $this->revId2_4, $this->textId2_4 ) = $this->addRevision( $page,
47 "BackupDumperTestP2Text4 some additional Text ",
48 "BackupDumperTestP2Summary4 extra " );
49 $this->pageId2 = $page->getId();
50
51 // Deleted page.
52 $title = Title::newFromText( 'BackupDumperTestP3' );
53 $page = WikiPage::factory( $title );
54 list( $this->revId3_1, $this->textId3_1 ) = $this->addRevision( $page,
55 "BackupDumperTestP3Text1", "BackupDumperTestP2Summary1" );
56 list( $this->revId3_2, $this->textId3_2 ) = $this->addRevision( $page,
57 "BackupDumperTestP3Text2", "BackupDumperTestP2Summary2" );
58 $this->pageId3 = $page->getId();
59 $page->doDeleteArticle( "Testing ;)" );
60
61 // Page from non-default namespace
62 $title = Title::newFromText( 'BackupDumperTestP1', NS_TALK );
63 $page = WikiPage::factory( $title );
64 list( $this->revId4_1, $this->textId4_1 ) = $this->addRevision( $page,
65 "Talk about BackupDumperTestP1 Text1",
66 "Talk BackupDumperTestP1 Summary1" );
67 $this->pageId4 = $page->getId();
68 } catch ( Exception $e ) {
69 // We'd love to pass $e directly. However, ... see
70 // documentation of exceptionFromAddDBData in
71 // DumpTestCase
72 $this->exceptionFromAddDBData = $e;
73 }
74
75 }
76
77 public function setUp() {
78 parent::setUp();
79
80 // Since we will restrict dumping by page ranges (to allow
81 // working tests, even if the db gets prepopulated by a base
82 // class), we have to assert, that the page id are consecutively
83 // increasing
84 $this->assertEquals(
85 array( $this->pageId2, $this->pageId3, $this->pageId4 ),
86 array( $this->pageId1 + 1, $this->pageId2 + 1, $this->pageId3 + 1 ),
87 "Page ids increasing without holes" );
88
89 }
90
91 function testPlain() {
92 // Setting up the dump
93 $nameStub = $this->setUpStub();
94 $nameFull = $this->getNewTempFile();
95 $dumper = new TextPassDumper( array ( "--stub=file:" . $nameStub,
96 "--output=file:" . $nameFull ) );
97 $dumper->reporting = false;
98 $dumper->setDb( $this->db );
99
100 // Performing the dump
101 $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );
102
103 // Checking for correctness of the dumped data
104 $this->assertDumpStart( $nameFull );
105
106 // Page 1
107 $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
108 $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
109 $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
110 "BackupDumperTestP1Text1" );
111 $this->assertPageEnd();
112
113 // Page 2
114 $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
115 $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
116 $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
117 "BackupDumperTestP2Text1" );
118 $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
119 $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
120 "BackupDumperTestP2Text2", $this->revId2_1 );
121 $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
122 $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
123 "BackupDumperTestP2Text3", $this->revId2_2 );
124 $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
125 $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
126 "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
127 $this->assertPageEnd();
128
129 // Page 3
130 // -> Page is marked deleted. Hence not visible
131
132 // Page 4
133 $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
134 $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
135 $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
136 "Talk about BackupDumperTestP1 Text1" );
137 $this->assertPageEnd();
138
139 $this->assertDumpEnd();
140 }
141
142 function testPrefetchPlain() {
143 // The mapping between ids and text, for the hits of the prefetch mock
144 $prefetchMap = array(
145 array( $this->pageId1, $this->revId1_1, "Prefetch_________1Text1" ),
146 array( $this->pageId2, $this->revId2_3, "Prefetch_________2Text3" )
147 );
148
149 // The mock itself
150 $prefetchMock = $this->getMock( 'BaseDump', array( 'prefetch' ), array(), '', FALSE );
151 $prefetchMock->expects( $this->exactly( 6 ) )
152 ->method( 'prefetch' )
153 ->will( $this->returnValueMap( $prefetchMap ) );
154
155 // Setting up of the dump
156 $nameStub = $this->setUpStub();
157 $nameFull = $this->getNewTempFile();
158 $dumper = new TextPassDumper( array ( "--stub=file:"
159 . $nameStub, "--output=file:" . $nameFull ) );
160 $dumper->prefetch = $prefetchMock;
161 $dumper->reporting = false;
162 $dumper->setDb( $this->db );
163
164 // Performing the dump
165 $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );
166
167 // Checking for correctness of the dumped data
168 $this->assertDumpStart( $nameFull );
169
170 // Page 1
171 $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
172 // Prefetch kicks in. This is still the SHA-1 of the original text,
173 // But the actual text (with different SHA-1) comes from prefetch.
174 $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
175 $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
176 "Prefetch_________1Text1" );
177 $this->assertPageEnd();
178
179 // Page 2
180 $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
181 $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
182 $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
183 "BackupDumperTestP2Text1" );
184 $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
185 $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
186 "BackupDumperTestP2Text2", $this->revId2_1 );
187 // Prefetch kicks in. This is still the SHA-1 of the original text,
188 // But the actual text (with different SHA-1) comes from prefetch.
189 $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
190 $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
191 "Prefetch_________2Text3", $this->revId2_2 );
192 $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
193 $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
194 "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
195 $this->assertPageEnd();
196
197 // Page 3
198 // -> Page is marked deleted. Hence not visible
199
200 // Page 4
201 $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
202 $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
203 $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
204 "Talk about BackupDumperTestP1 Text1" );
205 $this->assertPageEnd();
206
207 $this->assertDumpEnd();
208
209 }
210
211 /**
212 * Ensures that checkpoint dumps are used and written, by successively increasing the
213 * stub size and dumping until the duration crosses a threshold.
214 *
215 * @param $checkpointFormat string: Either "file" for plain text or "gzip" for gzipped
216 * checkpoint files.
217 */
218 private function checkpointHelper( $checkpointFormat = "file" ) {
219 // Getting temporary names
220 $nameStub = $this->getNewTempFile();
221 $nameOutputDir = $this->getNewTempDirectory();
222
223 $stderr = fopen( 'php://output', 'a' );
224 if ( $stderr === FALSE ) {
225 $this->fail( "Could not open stream for stderr" );
226 }
227
228 $iterations = 32; // We'll start with that many iterations of revisions in stub
229 $lastDuration = 0;
230 $minDuration = 2; // We want the dump to take at least this many seconds
231 $checkpointAfter = 0.5; // Generate checkpoint after this many seconds
232
233
234 // Until a dump takes at least $minDuration seconds, perform a dump and check
235 // duration. If the dump did not take long enough increase the iteration
236 // count, to generate a bigger stub file next time.
237 while ( $lastDuration < $minDuration ) {
238
239 // Setting up the dump
240 wfRecursiveRemoveDir( $nameOutputDir );
241 $this->assertTrue( wfMkdirParents( $nameOutputDir ),
242 "Creating temporary output directory " );
243 $this->setUpStub( $nameStub, $iterations );
244 $dumper = new TextPassDumper( array ( "--stub=file:" . $nameStub,
245 "--output=" . $checkpointFormat . ":" . $nameOutputDir . "/full",
246 "--maxtime=1" /*This is in minutes. Fixup is below*/,
247 "--checkpointfile=checkpoint-%s-%s.xml.gz" ) );
248 $dumper->setDb( $this->db );
249 $dumper->maxTimeAllowed = $checkpointAfter; // Patching maxTime from 1 minute
250 $dumper->stderr = $stderr;
251
252 // The actual dump and taking time
253 $ts_before = wfTime();
254 $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );
255 $ts_after = wfTime();
256 $lastDuration = $ts_after - $ts_before;
257
258 // Handling increasing the iteration count for the stubs
259 if ( $lastDuration < $minDuration ) {
260 $old_iterations = $iterations;
261 if ( $lastDuration > 0.2 ) {
262 // lastDuration is big enough, to allow an educated guess
263 $factor = ( $minDuration + 0.5 ) / $lastDuration;
264 if ( ( $factor > 1.1 ) && ( $factor < 100 ) ) {
265 // educated guess is reasonable
266 $iterations = (int)( $iterations * $factor );
267 }
268 }
269
270 if ( $old_iterations == $iterations ) {
271 // Heuristics were not applied, so we just *2.
272 $iterations *= 2;
273 }
274
275 $this->assertLessThan( 50000, $iterations,
276 "Emergency stop against infinitely increasing iteration "
277 . "count ( last duration: $lastDuration )" );
278 }
279 }
280
281 // The dump (hopefully) did take long enough to produce more than one
282 // checkpoint file.
283 //
284 // We now check all the checkpoint files for validity.
285
286 $files = scandir( $nameOutputDir );
287 $this->assertTrue( asort( $files ), "Sorting files in temporary directory" );
288 $fileOpened = false;
289 $lookingForPage = 1;
290 $checkpointFiles = 0;
291
292 // Each run of the following loop body tries to handle exactly 1 /page/ (not
293 // iteration of stub content). $i is only increased after having treated page 4.
294 for ( $i = 0 ; $i < $iterations ; ) {
295
296 // 1. Assuring a file is opened and ready. Skipping across header if
297 // necessary.
298 if ( ! $fileOpened ) {
299 $this->assertNotEmpty( $files, "No more existing dump files, "
300 . "but not yet all pages found" );
301 $fname = array_shift( $files );
302 while ( $fname == "." || $fname == ".." ) {
303 $this->assertNotEmpty( $files, "No more existing dump"
304 . " files, but not yet all pages found" );
305 $fname = array_shift( $files );
306 }
307 if ( $checkpointFormat == "gzip" ) {
308 $this->gunzip( $nameOutputDir . "/" . $fname );
309 }
310 $this->assertDumpStart( $nameOutputDir . "/" . $fname );
311 $fileOpened = true;
312 $checkpointFiles++;
313 }
314
315 // 2. Performing a single page check
316 switch ( $lookingForPage ) {
317 case 1:
318 // Page 1
319 $this->assertPageStart( $this->pageId1 + $i * self::$numOfPages, NS_MAIN,
320 "BackupDumperTestP1" );
321 $this->assertRevision( $this->revId1_1 + $i * self::$numOfRevs, "BackupDumperTestP1Summary1",
322 $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
323 "BackupDumperTestP1Text1" );
324 $this->assertPageEnd();
325
326 $lookingForPage = 2;
327 break;
328
329 case 2:
330 // Page 2
331 $this->assertPageStart( $this->pageId2 + $i * self::$numOfPages, NS_MAIN,
332 "BackupDumperTestP2" );
333 $this->assertRevision( $this->revId2_1 + $i * self::$numOfRevs, "BackupDumperTestP2Summary1",
334 $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
335 "BackupDumperTestP2Text1" );
336 $this->assertRevision( $this->revId2_2 + $i * self::$numOfRevs, "BackupDumperTestP2Summary2",
337 $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
338 "BackupDumperTestP2Text2", $this->revId2_1 + $i * self::$numOfRevs );
339 $this->assertRevision( $this->revId2_3 + $i * self::$numOfRevs, "BackupDumperTestP2Summary3",
340 $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
341 "BackupDumperTestP2Text3", $this->revId2_2 + $i * self::$numOfRevs );
342 $this->assertRevision( $this->revId2_4 + $i * self::$numOfRevs,
343 "BackupDumperTestP2Summary4 extra",
344 $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
345 "BackupDumperTestP2Text4 some additional Text",
346 $this->revId2_3 + $i * self::$numOfRevs );
347 $this->assertPageEnd();
348
349 $lookingForPage = 4;
350 break;
351
352 case 4:
353 // Page 4
354 $this->assertPageStart( $this->pageId4 + $i * self::$numOfPages, NS_TALK,
355 "Talk:BackupDumperTestP1" );
356 $this->assertRevision( $this->revId4_1 + $i * self::$numOfRevs,
357 "Talk BackupDumperTestP1 Summary1",
358 $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
359 "Talk about BackupDumperTestP1 Text1" );
360 $this->assertPageEnd();
361
362 $lookingForPage = 1;
363
364 // We dealt with the whole iteration.
365 $i++;
366 break;
367
368 default:
369 $this->fail( "Bad setting for lookingForPage ($lookingForPage)" );
370 }
371
372 // 3. Checking for the end of the current checkpoint file
373 if ( $this->xml->nodeType == XMLReader::END_ELEMENT
374 && $this->xml->name == "mediawiki" ) {
375
376 $this->assertDumpEnd();
377 $fileOpened = false;
378 }
379 }
380
381 // Assuring we completely read all files ...
382 $this->assertFalse( $fileOpened, "Currently read file still open?" );
383 $this->assertEmpty( $files, "Remaining unchecked files" );
384
385 // ... and have dealt with more than one checkpoint file
386 $this->assertGreaterThan( 1, $checkpointFiles, "# of checkpoint files" );
387
388 $this->expectETAOutput();
389 }
390
391 /**
392 * @group large
393 */
394 function testCheckpointPlain() {
395 $this->checkpointHelper();
396 }
397
398 /**
399 * tests for working checkpoint generation in gzip format work.
400 *
401 * We keep this test in addition to the simpler self::testCheckpointPlain, as there
402 * were once problems when the used sinks were DumpPipeOutputs.
403 *
404 * xmldumps-backup typically uses bzip2 instead of gzip. However, as bzip2 requires
405 * PHP extensions, we go for gzip instead, which triggers the same relevant code
406 * paths while still being testable on more systems.
407 *
408 * @group large
409 */
410 function testCheckpointGzip() {
411 $this->checkpointHelper( "gzip" );
412 }
413
414
415 /**
416 * Creates a stub file that is used for testing the text pass of dumps
417 *
418 * @param $fname string: (Optional) Absolute name of the file to write
419 * the stub into. If this parameter is null, a new temporary
420 * file is generated that is automatically removed upon
421 * tearDown.
422 * @param $iterations integer: (Optional) specifies how often the block
423 * of 3 pages should go into the stub file. The page id
424 * increase further and further, while the revision and text
425 * ids of the first iteration are reused. The pages of
426 * iteration > 1 have no corresponding representation in the
427 * database.
428 * @return string absolute filename of the stub
429 */
430 private function setUpStub( $fname = null, $iterations = 1 ) {
431 if ( $fname === null ) {
432 $fname = $this->getNewTempFile();
433 }
434 $header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.7/" '
435 . 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
436 . 'xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.7/ '
437 . 'http://www.mediawiki.org/xml/export-0.7.xsd" version="0.7" xml:lang="en">
438 <siteinfo>
439 <sitename>wikisvn</sitename>
440 <base>http://localhost/wiki-svn/index.php/Main_Page</base>
441 <generator>MediaWiki 1.20alpha</generator>
442 <case>first-letter</case>
443 <namespaces>
444 <namespace key="-2" case="first-letter">Media</namespace>
445 <namespace key="-1" case="first-letter">Special</namespace>
446 <namespace key="0" case="first-letter" />
447 <namespace key="1" case="first-letter">Talk</namespace>
448 <namespace key="2" case="first-letter">User</namespace>
449 <namespace key="3" case="first-letter">User talk</namespace>
450 <namespace key="4" case="first-letter">Wikisvn</namespace>
451 <namespace key="5" case="first-letter">Wikisvn talk</namespace>
452 <namespace key="6" case="first-letter">File</namespace>
453 <namespace key="7" case="first-letter">File talk</namespace>
454 <namespace key="8" case="first-letter">MediaWiki</namespace>
455 <namespace key="9" case="first-letter">MediaWiki talk</namespace>
456 <namespace key="10" case="first-letter">Template</namespace>
457 <namespace key="11" case="first-letter">Template talk</namespace>
458 <namespace key="12" case="first-letter">Help</namespace>
459 <namespace key="13" case="first-letter">Help talk</namespace>
460 <namespace key="14" case="first-letter">Category</namespace>
461 <namespace key="15" case="first-letter">Category talk</namespace>
462 </namespaces>
463 </siteinfo>
464 ';
465 $tail = '</mediawiki>
466 ';
467
468 $content = $header;
469 $iterations = intval( $iterations );
470 for ( $i = 0; $i < $iterations; $i++ ) {
471
472 $page1 = ' <page>
473 <title>BackupDumperTestP1</title>
474 <ns>0</ns>
475 <id>' . ( $this->pageId1 + $i * self::$numOfPages ) . '</id>
476 <revision>
477 <id>' . ( $this->revId1_1 + $i * self::$numOfRevs ) . '</id>
478 <timestamp>2012-04-01T16:46:05Z</timestamp>
479 <contributor>
480 <ip>127.0.0.1</ip>
481 </contributor>
482 <comment>BackupDumperTestP1Summary1</comment>
483 <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
484 <text id="' . $this->textId1_1 . '" bytes="23" />
485 </revision>
486 </page>
487 ';
488 $page2 = ' <page>
489 <title>BackupDumperTestP2</title>
490 <ns>0</ns>
491 <id>' . ( $this->pageId2 + $i * self::$numOfPages ) . '</id>
492 <revision>
493 <id>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</id>
494 <timestamp>2012-04-01T16:46:05Z</timestamp>
495 <contributor>
496 <ip>127.0.0.1</ip>
497 </contributor>
498 <comment>BackupDumperTestP2Summary1</comment>
499 <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
500 <text id="' . $this->textId2_1 . '" bytes="23" />
501 </revision>
502 <revision>
503 <id>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</id>
504 <parentid>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</parentid>
505 <timestamp>2012-04-01T16:46:05Z</timestamp>
506 <contributor>
507 <ip>127.0.0.1</ip>
508 </contributor>
509 <comment>BackupDumperTestP2Summary2</comment>
510 <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1>
511 <text id="' . $this->textId2_2 . '" bytes="23" />
512 </revision>
513 <revision>
514 <id>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</id>
515 <parentid>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</parentid>
516 <timestamp>2012-04-01T16:46:05Z</timestamp>
517 <contributor>
518 <ip>127.0.0.1</ip>
519 </contributor>
520 <comment>BackupDumperTestP2Summary3</comment>
521 <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1>
522 <text id="' . $this->textId2_3 . '" bytes="23" />
523 </revision>
524 <revision>
525 <id>' . ( $this->revId2_4 + $i * self::$numOfRevs ) . '</id>
526 <parentid>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</parentid>
527 <timestamp>2012-04-01T16:46:05Z</timestamp>
528 <contributor>
529 <ip>127.0.0.1</ip>
530 </contributor>
531 <comment>BackupDumperTestP2Summary4 extra</comment>
532 <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
533 <text id="' . $this->textId2_4 . '" bytes="44" />
534 </revision>
535 </page>
536 ';
537 // page 3 not in stub
538
539 $page4 = ' <page>
540 <title>Talk:BackupDumperTestP1</title>
541 <ns>1</ns>
542 <id>' . ( $this->pageId4 + $i * self::$numOfPages ) . '</id>
543 <revision>
544 <id>' . ( $this->revId4_1 + $i * self::$numOfRevs ) . '</id>
545 <timestamp>2012-04-01T16:46:05Z</timestamp>
546 <contributor>
547 <ip>127.0.0.1</ip>
548 </contributor>
549 <comment>Talk BackupDumperTestP1 Summary1</comment>
550 <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
551 <text id="' . $this->textId4_1 . '" bytes="35" />
552 </revision>
553 </page>
554 ';
555 $content .= $page1 . $page2 . $page4;
556 }
557 $content .= $tail;
558 $this->assertEquals( strlen( $content ), file_put_contents(
559 $fname, $content ), "Length of prepared stub" );
560 return $fname;
561 }
562
563 }