3 namespace MediaWiki\Tests\Maintenance
;
7 use MediaWiki\CommentStore\CommentStoreComment
;
8 use MediaWiki\Content\Content
;
9 use MediaWiki\MediaWikiServices
;
10 use MediaWiki\Revision\RevisionAccessException
;
11 use MediaWiki\Revision\RevisionRecord
;
12 use MediaWiki\Revision\SlotRecord
;
13 use MediaWikiLangTestCase
;
19 * Base TestCase for dumps
21 abstract class DumpTestCase
extends MediaWikiLangTestCase
{
24 * exception to be rethrown once in sound PHPUnit surrounding
26 * As the current MediaWikiIntegrationTestCase::run is not robust enough to recover
27 * from thrown exceptions directly, we cannot throw frow within
28 * self::addDBData, although it would be appropriate. Hence, we catch the
29 * exception and store it until we are in setUp and may finally rethrow
30 * the exception without crashing the test suite.
32 * @var \Exception|null
34 protected $exceptionFromAddDBData = null;
36 /** @var bool|null Whether the 'gzip' utility is available */
37 protected static $hasGzip = null;
40 * Skip the test if 'gzip' is not in $PATH.
44 protected function checkHasGzip() {
45 if ( self
::$hasGzip === null ) {
46 self
::$hasGzip = ( ExecutableFinder
::findInDefaultPaths( 'gzip' ) !== false );
49 if ( !self
::$hasGzip ) {
50 $this->markTestSkipped( "Skip test, requires the gzip utility in PATH" );
53 return self
::$hasGzip;
57 * Adds a revision to a page, while returning the resuting revision's id text id.
59 * @param WikiPage $page Page to add the revision to
60 * @param string $text Revisions text
61 * @param string $summary Revisions summary
62 * @param string $model The model ID (defaults to wikitext)
65 protected function addRevision(
69 $model = CONTENT_MODEL_WIKITEXT
71 $contentHandler = $this->getServiceContainer()
72 ->getContentHandlerFactory()->getContentHandler( $model );
74 $content = $contentHandler->unserializeContent( $text );
76 $rev = $this->addMultiSlotRevision( $page, [ SlotRecord
::MAIN
=> $content ], $summary );
79 throw new RuntimeException( "Could not create revision" );
82 $text_id = $this->getSlotTextId( $rev->getSlot( SlotRecord
::MAIN
) );
83 return [ $rev->getId(), $text_id, $rev ];
87 * @param SlotRecord $slot
91 protected function getSlotText( SlotRecord
$slot ) {
93 return $slot->getContent()->serialize();
94 } catch ( RevisionAccessException
$ex ) {
100 * @param SlotRecord $slot
104 protected function getSlotTextId( SlotRecord
$slot ) {
105 return (int)preg_replace( '/^tt:/', '', $slot->getAddress() );
109 * @param SlotRecord $slot
113 protected function getSlotFormat( SlotRecord
$slot ) {
114 $contentHandler = $this->getServiceContainer()
115 ->getContentHandlerFactory()->getContentHandler( $slot->getModel() );
117 return $contentHandler->getDefaultFormat();
121 * Adds a revision to a page, while returning the resulting revision's id and text id.
123 * @param WikiPage $page Page to add the revision to
124 * @param Content[] $slots A mapping of slot names to Content objects
125 * @param string $summary Revisions summary
126 * @return RevisionRecord
128 protected function addMultiSlotRevision(
133 $slotRoleRegistry = MediaWikiServices
::getInstance()->getSlotRoleRegistry();
135 $updater = $page->newPageUpdater( $this->getTestUser()->getUser() );
137 foreach ( $slots as $role => $content ) {
138 if ( !$slotRoleRegistry->isDefinedRole( $role ) ) {
139 $slotRoleRegistry->defineRoleWithModel( $role, $content->getModel() );
142 $updater->setContent( $role, $content );
145 $updater->saveRevision( CommentStoreComment
::newUnsavedComment( trim( $summary ) ) );
146 return $updater->getNewRevision();
150 * gunzips the given file and stores the result in the original file name
152 * @param string $fname Filename to read the gzipped data from and stored
153 * the gunzipped data into
155 protected function gunzip( $fname ) {
156 $gzipped_contents = file_get_contents( $fname );
157 if ( $gzipped_contents === false ) {
158 $this->fail( "Could not get contents of $fname" );
161 $contents = gzdecode( $gzipped_contents );
165 file_put_contents( $fname, $contents ),
170 public static function setUpBeforeClass(): void
{
171 parent
::setUpBeforeClass();
173 if ( !function_exists( 'libxml_set_external_entity_loader' ) ) {
177 // The W3C is intentionally slow about returning schema files,
178 // see <https://www.w3.org/Help/Webmaster#slowdtd>.
179 // To work around that, we keep our own copies of the relevant schema files.
180 libxml_set_external_entity_loader(
181 static function ( $public, $system, $context ) {
183 // if more schema files are needed, add them here.
184 case 'http://www.w3.org/2001/xml.xsd':
185 $file = __DIR__
. '/xml.xsd';
188 if ( is_file( $system ) ) {
201 * Default set up function.
203 * Reports errors from addDBData to PHPUnit
205 protected function setUp(): void
{
208 // Check if any Exception is stored for rethrowing from addDBData
209 // @see self::exceptionFromAddDBData
210 if ( $this->exceptionFromAddDBData
!== null ) {
211 throw $this->exceptionFromAddDBData
;
216 * Returns the path to the XML schema file for the given schema version.
218 * @param string|null $schemaVersion
222 protected function getXmlSchemaPath( $schemaVersion = null ) {
223 global $IP, $wgXmlDumpSchemaVersion;
225 $schemaVersion = $schemaVersion ?
: $wgXmlDumpSchemaVersion;
227 return "$IP/docs/export-$schemaVersion.xsd";
231 * Checks for test output consisting only of lines containing ETA announcements
233 protected function expectETAOutput() {
234 // Newer PHPUnits require assertion about the output using PHPUnit's own
235 // expectOutput[...] functions. However, the PHPUnit shipped prediactes
236 // do not allow to check /each/ line of the output using /readable/ REs.
239 // 1. ... add a dummy output checking to make PHPUnit not complain
240 // about unchecked test output
241 $this->expectOutputRegex( '//' );
243 // 2. Do the real output checking on our own.
244 $lines = explode( "\n", $this->getActualOutput() );
245 $this->assertGreaterThan( 1, count( $lines ), "Minimal lines of produced output" );
246 $this->assertSame( '', array_pop( $lines ), "Output ends in LF" );
247 $timestamp_re = "[0-9]{4}-[01][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-6][0-9]";
248 foreach ( $lines as $line ) {
249 $this->assertMatchesRegularExpression(
250 "/$timestamp_re: .* \(ID [0-9]+\) [0-9]* pages .*, [0-9]* revs .*, ETA/",
257 * @param null|string $schemaVersion
259 * @return DumpAsserter
261 protected function getDumpAsserter( $schemaVersion = null ) {
262 $schemaVersion = $schemaVersion ?
: WikiExporter
::schemaVersion();
263 return new DumpAsserter( $schemaVersion );
267 * Checks an XML file against an XSD schema.
268 * @param string $fname
269 * @param string $schemaFile
271 protected function assertDumpSchema( $fname, $schemaFile ) {
272 if ( !function_exists( 'libxml_use_internal_errors' ) ) {
273 // Would be nice to leave a warning somehow.
274 // We don't want to skip all of the test case that calls this, though.
275 $this->markAsRisky();
278 $xml = new DOMDocument();
279 $this->assertTrue( $xml->load( $fname ),
280 "Opening temporary file $fname via DOMDocument failed" );
283 $oldLibXmlInternalErrors = libxml_use_internal_errors( true );
285 // NOTE: if this reports "Invalid Schema", the schema may be referencing an external
286 // entity (typically, another schema) that needs to be mapped in the
287 // libxml_set_external_entity_loader callback defined in setUpBeforeClass() above!
288 // Or $schemaFile doesn't point to a schema file, or the schema is indeed just broken.
289 if ( !$xml->schemaValidate( $schemaFile ) ) {
292 foreach ( libxml_get_errors() as $error ) {
293 $errorText .= "\nline {$error->line}: {$error->message}";
296 libxml_clear_errors();
299 "Failed asserting that $fname conforms to the schema in $schemaFile:\n$errorText"
303 libxml_use_internal_errors( $oldLibXmlInternalErrors );