~ubuntu-branches/ubuntu/trusty/mediawiki/trusty-proposed

« back to all changes in this revision

Viewing changes to tests/phpunit/includes/parser/NewParserTest.php

  • Committer: Package Import Robot
  • Author(s): Thorsten Glaser
  • Date: 2014-03-28 09:56:29 UTC
  • mfrom: (1.3.14)
  • Revision ID: package-import@ubuntu.com-20140328095629-1526y9tchdd507id
Tags: 1:1.19.14+dfsg-1
* New upstream security fix release (Closes: #742857):
  - (bug 62497) SECURITY: Add CSRF token on Special:ChangePassword
  - (bug 62467) Set a title for the context during import on the cli
* Use upstream-provided signing key bundle

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
 
 
3
/**
 
4
 * Although marked as a stub, can work independently.
 
5
 *
 
6
 * @group Database
 
7
 * @group Parser
 
8
 * @group Stub
 
9
 */
 
10
class NewParserTest extends MediaWikiTestCase {
 
11
        static protected $articles = array();   // Array of test articles defined by the tests
 
12
        /* The dataProvider is run on a different instance than the test, so it must be static
 
13
         * When running tests from several files, all tests will see all articles.
 
14
         */
 
15
        static protected $backendToUse;
 
16
 
 
17
        public $keepUploads = false;
 
18
        public $runDisabled = false;
 
19
        public $regex = '';
 
20
        public $showProgress = true;
 
21
        public $savedInitialGlobals = array();
 
22
        public $savedWeirdGlobals = array();
 
23
        public $savedGlobals = array();
 
24
        public $hooks = array();
 
25
        public $functionHooks = array();
 
26
 
 
27
        //Fuzz test
 
28
        public $maxFuzzTestLength = 300;
 
29
        public $fuzzSeed = 0;
 
30
        public $memoryLimit = 50;
 
31
 
 
32
        protected $file = false;
 
33
 
 
34
        function setUp() {
 
35
                global $wgContLang, $wgNamespaceProtection, $wgNamespaceAliases;
 
36
                global $wgHooks, $IP;
 
37
                $wgContLang = Language::factory( 'en' );
 
38
 
 
39
                //Setup CLI arguments
 
40
                if ( $this->getCliArg( 'regex=' ) ) {
 
41
                        $this->regex = $this->getCliArg( 'regex=' );
 
42
                } else {
 
43
                        # Matches anything
 
44
                        $this->regex = '';
 
45
                }
 
46
 
 
47
                $this->keepUploads = $this->getCliArg( 'keep-uploads' );
 
48
 
 
49
                $tmpGlobals = array();
 
50
 
 
51
                $tmpGlobals['wgScript'] = '/index.php';
 
52
                $tmpGlobals['wgScriptPath'] = '/';
 
53
                $tmpGlobals['wgArticlePath'] = '/wiki/$1';
 
54
                $tmpGlobals['wgStyleSheetPath'] = '/skins';
 
55
                $tmpGlobals['wgStylePath'] = '/skins';
 
56
                $tmpGlobals['wgThumbnailScriptPath'] = false;
 
57
                $tmpGlobals['wgLocalFileRepo'] = array(
 
58
                        'class'           => 'LocalRepo',
 
59
                        'name'            => 'local',
 
60
                        'url'             => 'http://example.com/images',
 
61
                        'hashLevels'      => 2,
 
62
                        'transformVia404' => false,
 
63
                        'backend'         => 'local-backend'
 
64
                );
 
65
                $tmpGlobals['wgForeignFileRepos'] = array();
 
66
                $tmpGlobals['wgEnableParserCache'] = false;
 
67
                $tmpGlobals['wgHooks'] = $wgHooks;
 
68
                $tmpGlobals['wgDeferredUpdateList'] = array();
 
69
                $tmpGlobals['wgMemc'] = wfGetMainCache();
 
70
                $tmpGlobals['messageMemc'] = wfGetMessageCacheStorage();
 
71
                $tmpGlobals['parserMemc'] = wfGetParserCacheStorage();
 
72
 
 
73
                // $tmpGlobals['wgContLang'] = new StubContLang;
 
74
                $tmpGlobals['wgUser'] = new User;
 
75
                $context = new RequestContext();
 
76
                $tmpGlobals['wgLang'] = $context->getLanguage();
 
77
                $tmpGlobals['wgOut'] = $context->getOutput();
 
78
                $tmpGlobals['wgParser'] = new StubObject( 'wgParser', $GLOBALS['wgParserConf']['class'], array( $GLOBALS['wgParserConf'] ) );
 
79
                $tmpGlobals['wgRequest'] = $context->getRequest();
 
80
 
 
81
                if ( $GLOBALS['wgStyleDirectory'] === false ) {
 
82
                        $tmpGlobals['wgStyleDirectory'] = "$IP/skins";
 
83
                }
 
84
 
 
85
 
 
86
                foreach ( $tmpGlobals as $var => $val ) {
 
87
                        if ( array_key_exists( $var, $GLOBALS ) ) {
 
88
                                $this->savedInitialGlobals[$var] = $GLOBALS[$var];
 
89
                        }
 
90
 
 
91
                        $GLOBALS[$var] = $val;
 
92
                }
 
93
 
 
94
                $this->savedWeirdGlobals['mw_namespace_protection'] = $wgNamespaceProtection[NS_MEDIAWIKI];
 
95
                $this->savedWeirdGlobals['image_alias'] = $wgNamespaceAliases['Image'];
 
96
                $this->savedWeirdGlobals['image_talk_alias'] = $wgNamespaceAliases['Image_talk'];
 
97
 
 
98
                $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
 
99
                $wgNamespaceAliases['Image'] = NS_FILE;
 
100
                $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
 
101
        }
 
102
 
 
103
        public function tearDown() {
 
104
                foreach ( $this->savedInitialGlobals as $var => $val ) {
 
105
                        $GLOBALS[$var] = $val;
 
106
                }
 
107
 
 
108
                global $wgNamespaceProtection, $wgNamespaceAliases;
 
109
 
 
110
                $wgNamespaceProtection[NS_MEDIAWIKI] = $this->savedWeirdGlobals['mw_namespace_protection'];
 
111
                $wgNamespaceAliases['Image'] = $this->savedWeirdGlobals['image_alias'];
 
112
                $wgNamespaceAliases['Image_talk'] = $this->savedWeirdGlobals['image_talk_alias'];
 
113
 
 
114
                // Restore backends
 
115
                RepoGroup::destroySingleton();
 
116
                FileBackendGroup::destroySingleton();
 
117
        }
 
118
 
 
119
        function addDBData() {
 
120
                $this->tablesUsed[] = 'site_stats';
 
121
                $this->tablesUsed[] = 'interwiki';
 
122
                # disabled for performance
 
123
                #$this->tablesUsed[] = 'image';
 
124
 
 
125
                # Hack: insert a few Wikipedia in-project interwiki prefixes,
 
126
                # for testing inter-language links
 
127
                $this->db->insert( 'interwiki', array(
 
128
                        array( 'iw_prefix' => 'wikipedia',
 
129
                                   'iw_url'    => 'http://en.wikipedia.org/wiki/$1',
 
130
                                   'iw_api'    => '',
 
131
                                   'iw_wikiid' => '',
 
132
                                   'iw_local'  => 0 ),
 
133
                        array( 'iw_prefix' => 'meatball',
 
134
                                   'iw_url'    => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
 
135
                                   'iw_api'    => '',
 
136
                                   'iw_wikiid' => '',
 
137
                                   'iw_local'  => 0 ),
 
138
                        array( 'iw_prefix' => 'zh',
 
139
                                   'iw_url'    => 'http://zh.wikipedia.org/wiki/$1',
 
140
                                   'iw_api'    => '',
 
141
                                   'iw_wikiid' => '',
 
142
                                   'iw_local'  => 1 ),
 
143
                        array( 'iw_prefix' => 'es',
 
144
                                   'iw_url'    => 'http://es.wikipedia.org/wiki/$1',
 
145
                                   'iw_api'    => '',
 
146
                                   'iw_wikiid' => '',
 
147
                                   'iw_local'  => 1 ),
 
148
                        array( 'iw_prefix' => 'fr',
 
149
                                   'iw_url'    => 'http://fr.wikipedia.org/wiki/$1',
 
150
                                   'iw_api'    => '',
 
151
                                   'iw_wikiid' => '',
 
152
                                   'iw_local'  => 1 ),
 
153
                        array( 'iw_prefix' => 'ru',
 
154
                                   'iw_url'    => 'http://ru.wikipedia.org/wiki/$1',
 
155
                                   'iw_api'    => '',
 
156
                                   'iw_wikiid' => '',
 
157
                                   'iw_local'  => 1 ),
 
158
                        /**
 
159
                         * @todo Fixme! Why are we inserting duplicate data here? Shouldn't
 
160
                         * need this IGNORE or shouldn't need the insert at all.
 
161
                         */
 
162
                        ), __METHOD__, array( 'IGNORE' )
 
163
                );
 
164
 
 
165
 
 
166
                # Update certain things in site_stats
 
167
                $this->db->insert( 'site_stats',
 
168
                        array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ),
 
169
                        __METHOD__
 
170
                );
 
171
 
 
172
                # Reinitialise the LocalisationCache to match the database state
 
173
                Language::getLocalisationCache()->unloadAll();
 
174
 
 
175
                # Clear the message cache
 
176
                MessageCache::singleton()->clear();
 
177
 
 
178
                $user = User::newFromId( 0 );
 
179
                LinkCache::singleton()->clear(); # Avoids the odd failure at creating the nullRevision
 
180
 
 
181
                # Upload DB table entries for files.
 
182
                # We will upload the actual files later. Note that if anything causes LocalFile::load()
 
183
                # to be triggered before then, it will break via maybeUpgrade() setting the fileExists
 
184
                # member to false and storing it in cache.
 
185
                $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) );
 
186
                if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
 
187
                        $image->recordUpload2(
 
188
                                '', // archive name
 
189
                                'Upload of some lame file', 
 
190
                                'Some lame file',
 
191
                                array(
 
192
                                        'size'        => 12345,
 
193
                                        'width'       => 1941,
 
194
                                        'height'      => 220,
 
195
                                        'bits'        => 24,
 
196
                                        'media_type'  => MEDIATYPE_BITMAP,
 
197
                                        'mime'        => 'image/jpeg',
 
198
                                        'metadata'    => serialize( array() ),
 
199
                                        'sha1'        => wfBaseConvert( '', 16, 36, 31 ),
 
200
                                        'fileExists'  => true ), 
 
201
                                $this->db->timestamp( '20010115123500' ), $user
 
202
                        );
 
203
                }
 
204
 
 
205
                # This image will be blacklisted in [[MediaWiki:Bad image list]]
 
206
                $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) );
 
207
                if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
 
208
                        $image->recordUpload2(
 
209
                                '', // archive name
 
210
                                'zomgnotcensored', 
 
211
                                'Borderline image', 
 
212
                                array(
 
213
                                        'size'        => 12345,
 
214
                                        'width'       => 320,
 
215
                                        'height'      => 240,
 
216
                                        'bits'        => 24,
 
217
                                        'media_type'  => MEDIATYPE_BITMAP,
 
218
                                        'mime'        => 'image/jpeg',
 
219
                                        'metadata'    => serialize( array() ),
 
220
                                        'sha1'        => wfBaseConvert( '', 16, 36, 31 ),
 
221
                                        'fileExists'  => true ), 
 
222
                                $this->db->timestamp( '20010115123500' ), $user
 
223
                        );
 
224
                }
 
225
        }
 
226
 
 
227
 
 
228
 
 
229
 
 
230
        //ParserTest setup/teardown functions
 
231
 
 
232
        /**
 
233
         * Set up the global variables for a consistent environment for each test.
 
234
         * Ideally this should replace the global configuration entirely.
 
235
         */
 
236
        protected function setupGlobals( $opts = '', $config = '' ) {
 
237
                global $wgFileBackends;
 
238
                # Find out values for some special options.
 
239
                $lang =
 
240
                        self::getOptionValue( 'language', $opts, 'en' );
 
241
                $variant =
 
242
                        self::getOptionValue( 'variant', $opts, false );
 
243
                $maxtoclevel =
 
244
                        self::getOptionValue( 'wgMaxTocLevel', $opts, 999 );
 
245
                $linkHolderBatchSize =
 
246
                        self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 );
 
247
 
 
248
                $uploadDir = $this->getUploadDir();
 
249
                if ( $this->getCliArg( 'use-filebackend=' ) ) {
 
250
                        if ( self::$backendToUse ) {
 
251
                                $backend = self::$backendToUse;
 
252
                        } else {
 
253
                                $name = $this->getCliArg( 'use-filebackend=' );
 
254
                                $useConfig = array();
 
255
                                foreach ( $wgFileBackends as $conf ) {
 
256
                                        if ( $conf['name'] == $name ) {
 
257
                                                $useConfig = $conf;
 
258
                                        }
 
259
                                }
 
260
                                $useConfig['name'] = 'local-backend'; // swap name
 
261
                                $class = $conf['class'];
 
262
                                self::$backendToUse = new $class( $useConfig );
 
263
                                $backend = self::$backendToUse;
 
264
                        }
 
265
                } else {
 
266
                        $backend = new FSFileBackend( array(
 
267
                                'name'        => 'local-backend',
 
268
                                'lockManager' => 'nullLockManager',
 
269
                                'containerPaths' => array(
 
270
                                        'local-public' => "$uploadDir",
 
271
                                        'local-thumb'  => "$uploadDir/thumb",
 
272
                                )
 
273
                        ) );
 
274
                }
 
275
 
 
276
                $settings = array(
 
277
                        'wgServer' => 'http://Britney-Spears',
 
278
                        'wgScript' => '/index.php',
 
279
                        'wgScriptPath' => '/',
 
280
                        'wgArticlePath' => '/wiki/$1',
 
281
                        'wgExtensionAssetsPath' => '/extensions',
 
282
                        'wgActionPaths' => array(),
 
283
                        'wgLocalFileRepo' => array(
 
284
                                'class'           => 'LocalRepo',
 
285
                                'name'            => 'local',
 
286
                                'url'             => 'http://example.com/images',
 
287
                                'hashLevels'      => 2,
 
288
                                'transformVia404' => false,
 
289
                                'backend'         => $backend
 
290
                        ),
 
291
                        'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ),
 
292
                        'wgStylePath' => '/skins',
 
293
                        'wgStyleSheetPath' => '/skins',
 
294
                        'wgSitename' => 'MediaWiki',
 
295
                        'wgLanguageCode' => $lang,
 
296
                        'wgDBprefix' => $this->db->getType() != 'oracle' ? 'unittest_' : 'ut_',
 
297
                        'wgRawHtml' => isset( $opts['rawhtml'] ),
 
298
                        'wgLang' => null,
 
299
                        'wgContLang' => null,
 
300
                        'wgNamespacesWithSubpages' => array( 0 => isset( $opts['subpage'] ) ),
 
301
                        'wgMaxTocLevel' => $maxtoclevel,
 
302
                        'wgCapitalLinks' => true,
 
303
                        'wgNoFollowLinks' => true,
 
304
                        'wgNoFollowDomainExceptions' => array(),
 
305
                        'wgThumbnailScriptPath' => false,
 
306
                        'wgUseImageResize' => false,
 
307
                        'wgUseTeX' => isset( $opts['math'] ),
 
308
                        'wgMathDirectory' => $uploadDir . '/math',
 
309
                        'wgLocaltimezone' => 'UTC',
 
310
                        'wgAllowExternalImages' => true,
 
311
                        'wgUseTidy' => false,
 
312
                        'wgDefaultLanguageVariant' => $variant,
 
313
                        'wgVariantArticlePath' => false,
 
314
                        'wgGroupPermissions' => array( '*' => array(
 
315
                                'createaccount' => true,
 
316
                                'read'          => true,
 
317
                                'edit'          => true,
 
318
                                'createpage'    => true,
 
319
                                'createtalk'    => true,
 
320
                        ) ),
 
321
                        'wgNamespaceProtection' => array( NS_MEDIAWIKI => 'editinterface' ),
 
322
                        'wgDefaultExternalStore' => array(),
 
323
                        'wgForeignFileRepos' => array(),
 
324
                        'wgLinkHolderBatchSize' => $linkHolderBatchSize,
 
325
                        'wgExperimentalHtmlIds' => false,
 
326
                        'wgExternalLinkTarget' => false,
 
327
                        'wgAlwaysUseTidy' => false,
 
328
                        'wgHtml5' => true,
 
329
                        'wgCleanupPresentationalAttributes' => true,
 
330
                        'wgWellFormedXml' => true,
 
331
                        'wgAllowMicrodataAttributes' => true,
 
332
                        'wgAdaptiveMessageCache' => true,
 
333
                        'wgUseDatabaseMessages' => true,
 
334
                );
 
335
 
 
336
                if ( $config ) {
 
337
                        $configLines = explode( "\n", $config );
 
338
 
 
339
                        foreach ( $configLines as $line ) {
 
340
                                list( $var, $value ) = explode( '=', $line, 2 );
 
341
 
 
342
                                $settings[$var] = eval( "return $value;" ); //???
 
343
                        }
 
344
                }
 
345
 
 
346
                $this->savedGlobals = array();
 
347
 
 
348
                foreach ( $settings as $var => $val ) {
 
349
                        if ( array_key_exists( $var, $GLOBALS ) ) {
 
350
                                $this->savedGlobals[$var] = $GLOBALS[$var];
 
351
                        }
 
352
 
 
353
                        $GLOBALS[$var] = $val;
 
354
                }
 
355
 
 
356
                $langObj = Language::factory( $lang );
 
357
                $GLOBALS['wgContLang'] = $langObj;
 
358
                $context = new RequestContext();
 
359
                $GLOBALS['wgLang'] = $context->getLanguage();
 
360
 
 
361
                $GLOBALS['wgMemc'] = new EmptyBagOStuff;
 
362
                $GLOBALS['wgOut'] = $context->getOutput();
 
363
                $GLOBALS['wgUser'] = $context->getUser();
 
364
 
 
365
                global $wgHooks;
 
366
 
 
367
                $wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup';
 
368
                $wgHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp';
 
369
 
 
370
                MagicWord::clearCache();
 
371
                RepoGroup::destroySingleton();
 
372
                FileBackendGroup::destroySingleton();
 
373
 
 
374
                # Create dummy files in storage
 
375
                $this->setupUploads();
 
376
 
 
377
                # Publish the articles after we have the final language set
 
378
                $this->publishTestArticles();
 
379
 
 
380
                # The entries saved into RepoGroup cache with previous globals will be wrong.
 
381
                RepoGroup::destroySingleton();
 
382
                FileBackendGroup::destroySingleton();
 
383
                MessageCache::singleton()->destroyInstance();
 
384
 
 
385
                return $context;
 
386
        }
 
387
 
 
388
        /**
 
389
         * Get an FS upload directory (only applies to FSFileBackend)
 
390
         *
 
391
         * @return String: the directory
 
392
         */
 
393
        protected function getUploadDir() {
 
394
                if ( $this->keepUploads ) {
 
395
                        $dir = wfTempDir() . '/mwParser-images';
 
396
 
 
397
                        if ( is_dir( $dir ) ) {
 
398
                                return $dir;
 
399
                        }
 
400
                } else {
 
401
                        $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images";
 
402
                }
 
403
 
 
404
                // wfDebug( "Creating upload directory $dir\n" );
 
405
                if ( file_exists( $dir ) ) {
 
406
                        wfDebug( "Already exists!\n" );
 
407
                        return $dir;
 
408
                }
 
409
 
 
410
                return $dir;
 
411
        }
 
412
 
 
413
        /**
 
414
         * Create a dummy uploads directory which will contain a couple
 
415
         * of files in order to pass existence tests.
 
416
         *
 
417
         * @return String: the directory
 
418
         */
 
419
        protected function setupUploads() {
 
420
                global $IP;
 
421
 
 
422
                $base = $this->getBaseDir();
 
423
                $backend = RepoGroup::singleton()->getLocalRepo()->getBackend();
 
424
                $backend->prepare( array( 'dir' => "$base/local-public/3/3a" ) );
 
425
                $backend->store( array(
 
426
                        'src' => "$IP/skins/monobook/headbg.jpg", 'dst' => "$base/local-public/3/3a/Foobar.jpg"
 
427
                ) );
 
428
                $backend->prepare( array( 'dir' => "$base/local-public/0/09" ) );
 
429
                $backend->store( array(
 
430
                        'src' => "$IP/skins/monobook/headbg.jpg", 'dst' => "$base/local-public/0/09/Bad.jpg"
 
431
                ) );
 
432
        }
 
433
 
 
434
        /**
 
435
         * Restore default values and perform any necessary clean-up
 
436
         * after each test runs.
 
437
         */
 
438
        protected function teardownGlobals() {
 
439
                $this->teardownUploads();
 
440
 
 
441
                foreach ( $this->savedGlobals as $var => $val ) {
 
442
                        $GLOBALS[$var] = $val;
 
443
                }
 
444
 
 
445
                RepoGroup::destroySingleton();
 
446
                LinkCache::singleton()->clear();
 
447
        }
 
448
 
 
449
        /**
 
450
         * Remove the dummy uploads directory
 
451
         */
 
452
        private function teardownUploads() {
 
453
                if ( $this->keepUploads ) {
 
454
                        return;
 
455
                }
 
456
 
 
457
                $base = $this->getBaseDir();
 
458
                // delete the files first, then the dirs.
 
459
                self::deleteFiles(
 
460
                        array (
 
461
                                "$base/local-public/3/3a/Foobar.jpg",
 
462
                                "$base/local-thumb/3/3a/Foobar.jpg/180px-Foobar.jpg",
 
463
                                "$base/local-thumb/3/3a/Foobar.jpg/200px-Foobar.jpg",
 
464
                                "$base/local-thumb/3/3a/Foobar.jpg/640px-Foobar.jpg",
 
465
                                "$base/local-thumb/3/3a/Foobar.jpg/120px-Foobar.jpg",
 
466
 
 
467
                                "$base/local-public/0/09/Bad.jpg",
 
468
                                "$base/local-thumb/0/09/Bad.jpg",
 
469
 
 
470
                                "$base/local-public/math/f/a/5/fa50b8b616463173474302ca3e63586b.png",
 
471
                        )
 
472
                );
 
473
        }
 
474
 
 
475
        /**
 
476
         * Delete the specified files, if they exist.
 
477
         * @param $files Array: full paths to files to delete.
 
478
         */
 
479
        private static function deleteFiles( $files ) {
 
480
                $backend = RepoGroup::singleton()->getLocalRepo()->getBackend();
 
481
                foreach ( $files as $file ) {
 
482
                        $backend->delete( array( 'src' => $file ), array( 'force' => 1 ) );
 
483
                }
 
484
                foreach ( $files as $file ) {
 
485
                        $tmp = $file;
 
486
                        while ( $tmp = FileBackend::parentStoragePath( $tmp ) ) {
 
487
                                if ( !$backend->clean( array( 'dir' => $tmp ) )->isOK() ) {
 
488
                                        break;
 
489
                                }
 
490
                        }
 
491
                }
 
492
        }
 
493
 
 
494
        protected function getBaseDir() {
 
495
                return 'mwstore://local-backend';
 
496
        }
 
497
 
 
498
        public function parserTestProvider() {
 
499
                if ( $this->file === false ) {
 
500
                        global $wgParserTestFiles;
 
501
                        $this->file = $wgParserTestFiles[0];
 
502
                }
 
503
                return new TestFileIterator( $this->file, $this );
 
504
        }
 
505
 
 
506
        /**
 
507
         * Set the file from whose tests will be run by this instance
 
508
         */
 
509
        public function setParserTestFile( $filename ) {
 
510
                $this->file = $filename;
 
511
        }
 
512
 
 
513
        /**
 
514
         * @group medium
 
515
         * @dataProvider parserTestProvider
 
516
         */
 
517
        public function testParserTest( $desc, $input, $result, $opts, $config ) {
 
518
                if ( $this->regex != '' && !preg_match( '/' . $this->regex . '/', $desc ) ) {
 
519
                        $this->assertTrue( true ); // XXX: don't flood output with "test made no assertions"
 
520
                        //$this->markTestSkipped( 'Filtered out by the user' );
 
521
                        return;
 
522
                }
 
523
 
 
524
                wfDebug( "Running parser test: $desc\n" );
 
525
 
 
526
                $opts = $this->parseOptions( $opts );
 
527
                $context = $this->setupGlobals( $opts, $config );
 
528
 
 
529
                $user = $context->getUser();
 
530
                $options = ParserOptions::newFromContext( $context );
 
531
 
 
532
                if ( isset( $opts['title'] ) ) {
 
533
                        $titleText = $opts['title'];
 
534
                }
 
535
                else {
 
536
                        $titleText = 'Parser test';
 
537
                }
 
538
 
 
539
                $local = isset( $opts['local'] );
 
540
                $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null;
 
541
                $parser = $this->getParser( $preprocessor );
 
542
 
 
543
                $title = Title::newFromText( $titleText );
 
544
 
 
545
                if ( isset( $opts['pst'] ) ) {
 
546
                        $out = $parser->preSaveTransform( $input, $title, $user, $options );
 
547
                } elseif ( isset( $opts['msg'] ) ) {
 
548
                        $out = $parser->transformMsg( $input, $options, $title );
 
549
                } elseif ( isset( $opts['section'] ) ) {
 
550
                        $section = $opts['section'];
 
551
                        $out = $parser->getSection( $input, $section );
 
552
                } elseif ( isset( $opts['replace'] ) ) {
 
553
                        $section = $opts['replace'][0];
 
554
                        $replace = $opts['replace'][1];
 
555
                        $out = $parser->replaceSection( $input, $section, $replace );
 
556
                } elseif ( isset( $opts['comment'] ) ) {
 
557
                        $out = Linker::formatComment( $input, $title, $local );
 
558
                } elseif ( isset( $opts['preload'] ) ) {
 
559
                        $out = $parser->getpreloadText( $input, $title, $options );
 
560
                } else {
 
561
                        $output = $parser->parse( $input, $title, $options, true, true, 1337 );
 
562
                        $out = $output->getText();
 
563
 
 
564
                        if ( isset( $opts['showtitle'] ) ) {
 
565
                                if ( $output->getTitleText() ) {
 
566
                                        $title = $output->getTitleText();
 
567
                                }
 
568
 
 
569
                                $out = "$title\n$out";
 
570
                        }
 
571
 
 
572
                        if ( isset( $opts['ill'] ) ) {
 
573
                                $out = $this->tidy( implode( ' ', $output->getLanguageLinks() ) );
 
574
                        } elseif ( isset( $opts['cat'] ) ) {
 
575
                                $outputPage = $context->getOutput();
 
576
                                $outputPage->addCategoryLinks( $output->getCategories() );
 
577
                                $cats = $outputPage->getCategoryLinks();
 
578
 
 
579
                                if ( isset( $cats['normal'] ) ) {
 
580
                                        $out = $this->tidy( implode( ' ', $cats['normal'] ) );
 
581
                                } else {
 
582
                                        $out = '';
 
583
                                }
 
584
                        }
 
585
                        $parser->mPreprocessor = null;
 
586
 
 
587
                        $result = $this->tidy( $result );
 
588
                }
 
589
 
 
590
                $this->teardownGlobals();
 
591
 
 
592
                $this->assertEquals( $result, $out, $desc );
 
593
        }
 
594
 
 
595
        /**
 
596
         * Run a fuzz test series
 
597
         * Draw input from a set of test files
 
598
         *
 
599
         * @todo @fixme Needs some work to not eat memory until the world explodes
 
600
         *
 
601
         * @group ParserFuzz
 
602
         */
 
603
        function testFuzzTests() {
 
604
                global $wgParserTestFiles;
 
605
 
 
606
                $files = $wgParserTestFiles;
 
607
 
 
608
                if( $this->getCliArg( 'file=' ) ) {
 
609
                        $files = array( $this->getCliArg( 'file=' ) );
 
610
                }
 
611
 
 
612
                $dict = $this->getFuzzInput( $files );
 
613
                $dictSize = strlen( $dict );
 
614
                $logMaxLength = log( $this->maxFuzzTestLength );
 
615
 
 
616
                ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
 
617
 
 
618
                $user = new User;
 
619
                $opts = ParserOptions::newFromUser( $user );
 
620
                $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
 
621
 
 
622
                $id = 1;
 
623
 
 
624
                while ( true ) {
 
625
 
 
626
                        // Generate test input
 
627
                        mt_srand( ++$this->fuzzSeed );
 
628
                        $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
 
629
                        $input = '';
 
630
 
 
631
                        while ( strlen( $input ) < $totalLength ) {
 
632
                                $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
 
633
                                $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
 
634
                                $offset = mt_rand( 0, $dictSize - $hairLength );
 
635
                                $input .= substr( $dict, $offset, $hairLength );
 
636
                        }
 
637
 
 
638
                        $this->setupGlobals();
 
639
                        $parser = $this->getParser();
 
640
 
 
641
                        // Run the test
 
642
                        try {
 
643
                                $parser->parse( $input, $title, $opts );
 
644
                                $this->assertTrue( true, "Test $id, fuzz seed {$this->fuzzSeed}" );
 
645
                        } catch ( Exception $exception ) {
 
646
                                $input_dump = sprintf( "string(%d) \"%s\"\n", strlen( $input ), $input );
 
647
 
 
648
                                $this->assertTrue( false, "Test $id, fuzz seed {$this->fuzzSeed}. \n\nInput: $input_dump\n\nError: {$exception->getMessage()}\n\nBacktrace: {$exception->getTraceAsString()}" );
 
649
                        }
 
650
 
 
651
                        $this->teardownGlobals();
 
652
                        $parser->__destruct();
 
653
 
 
654
                        if ( $id % 100 == 0 ) {
 
655
                                $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
 
656
                                //echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
 
657
                                if ( $usage > 90 ) {
 
658
                                        $ret = "Out of memory:\n";
 
659
                                        $memStats = $this->getMemoryBreakdown();
 
660
 
 
661
                                        foreach ( $memStats as $name => $usage ) {
 
662
                                                $ret .= "$name: $usage\n";
 
663
                                        }
 
664
 
 
665
                                        throw new MWException( $ret );
 
666
                                }
 
667
                        }
 
668
 
 
669
                        $id++;
 
670
 
 
671
                }
 
672
        }
 
673
 
 
674
        //Various getter functions
 
675
 
 
676
        /**
 
677
         * Get an input dictionary from a set of parser test files
 
678
         */
 
679
        function getFuzzInput( $filenames ) {
 
680
                $dict = '';
 
681
 
 
682
                foreach ( $filenames as $filename ) {
 
683
                        $contents = file_get_contents( $filename );
 
684
                        preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
 
685
 
 
686
                        foreach ( $matches[1] as $match ) {
 
687
                                $dict .= $match . "\n";
 
688
                        }
 
689
                }
 
690
 
 
691
                return $dict;
 
692
        }
 
693
 
 
694
        /**
 
695
         * Get a memory usage breakdown
 
696
         */
 
697
        function getMemoryBreakdown() {
 
698
                $memStats = array();
 
699
 
 
700
                foreach ( $GLOBALS as $name => $value ) {
 
701
                        $memStats['$' . $name] = strlen( serialize( $value ) );
 
702
                }
 
703
 
 
704
                $classes = get_declared_classes();
 
705
 
 
706
                foreach ( $classes as $class ) {
 
707
                        $rc = new ReflectionClass( $class );
 
708
                        $props = $rc->getStaticProperties();
 
709
                        $memStats[$class] = strlen( serialize( $props ) );
 
710
                        $methods = $rc->getMethods();
 
711
 
 
712
                        foreach ( $methods as $method ) {
 
713
                                $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
 
714
                        }
 
715
                }
 
716
 
 
717
                $functions = get_defined_functions();
 
718
 
 
719
                foreach ( $functions['user'] as $function ) {
 
720
                        $rf = new ReflectionFunction( $function );
 
721
                        $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
 
722
                }
 
723
 
 
724
                asort( $memStats );
 
725
 
 
726
                return $memStats;
 
727
        }
 
728
 
 
729
        /**
 
730
         * Get a Parser object
 
731
         */
 
732
        function getParser( $preprocessor = null ) {
 
733
                global $wgParserConf;
 
734
 
 
735
                $class = $wgParserConf['class'];
 
736
                $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf );
 
737
 
 
738
                wfRunHooks( 'ParserTestParser', array( &$parser ) );
 
739
 
 
740
                return $parser;
 
741
        }
 
742
 
 
743
        //Various action functions
 
744
 
 
745
        public function addArticle( $name, $text, $line ) {
 
746
                self::$articles[$name] = array( $text, $line );
 
747
        }
 
748
 
 
749
        public function publishTestArticles() {
 
750
                if ( empty( self::$articles ) ) {
 
751
                        return;
 
752
                }
 
753
 
 
754
                foreach ( self::$articles as $name => $info ) {
 
755
                        list( $text, $line ) = $info;
 
756
                        ParserTest::addArticle( $name, $text, $line, 'ignoreduplicate' );
 
757
                }
 
758
        }
 
759
 
 
760
        /**
 
761
         * Steal a callback function from the primary parser, save it for
 
762
         * application to our scary parser. If the hook is not installed,
 
763
         * abort processing of this file.
 
764
         *
 
765
         * @param $name String
 
766
         * @return Bool true if tag hook is present
 
767
         */
 
768
        public function requireHook( $name ) {
 
769
                global $wgParser;
 
770
                $wgParser->firstCallInit( ); // make sure hooks are loaded.
 
771
                return isset( $wgParser->mTagHooks[$name] );
 
772
        }
 
773
 
 
774
        public function requireFunctionHook( $name ) {
 
775
                global $wgParser;
 
776
                $wgParser->firstCallInit( ); // make sure hooks are loaded.
 
777
                return isset( $wgParser->mFunctionHooks[$name] );
 
778
        }
 
779
        //Various "cleanup" functions
 
780
 
 
781
        /**
 
782
         * Run the "tidy" command on text if the $wgUseTidy
 
783
         * global is true
 
784
         *
 
785
         * @param $text String: the text to tidy
 
786
         * @return String
 
787
         */
 
788
        protected function tidy( $text ) {
 
789
                global $wgUseTidy;
 
790
 
 
791
                if ( $wgUseTidy ) {
 
792
                        $text = MWTidy::tidy( $text );
 
793
                }
 
794
 
 
795
                return $text;
 
796
        }
 
797
 
 
798
        /**
 
799
         * Remove last character if it is a newline
 
800
         */
 
801
        public function removeEndingNewline( $s ) {
 
802
                if ( substr( $s, -1 ) === "\n" ) {
 
803
                        return substr( $s, 0, -1 );
 
804
                }
 
805
                else {
 
806
                        return $s;
 
807
                }
 
808
        }
 
809
 
 
810
        //Test options parser functions
 
811
 
 
812
        protected function parseOptions( $instring ) {
 
813
                $opts = array();
 
814
                // foo
 
815
                // foo=bar
 
816
                // foo="bar baz"
 
817
                // foo=[[bar baz]]
 
818
                // foo=bar,"baz quux"
 
819
                $regex = '/\b
 
820
                        ([\w-]+)                                                # Key
 
821
                        \b
 
822
                        (?:\s*
 
823
                                =                                               # First sub-value
 
824
                                \s*
 
825
                                (
 
826
                                        "
 
827
                                                [^"]*                   # Quoted val
 
828
                                        "
 
829
                                |
 
830
                                        \[\[
 
831
                                                [^]]*                   # Link target
 
832
                                        \]\]
 
833
                                |
 
834
                                        [\w-]+                          # Plain word
 
835
                                )
 
836
                                (?:\s*
 
837
                                        ,                                       # Sub-vals 1..N
 
838
                                        \s*
 
839
                                        (
 
840
                                                "[^"]*"                 # Quoted val
 
841
                                        |
 
842
                                                \[\[[^]]*\]\]   # Link target
 
843
                                        |
 
844
                                                [\w-]+                  # Plain word
 
845
                                        )
 
846
                                )*
 
847
                        )?
 
848
                        /x';
 
849
 
 
850
                if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) {
 
851
                        foreach ( $matches as $bits ) {
 
852
                                array_shift( $bits );
 
853
                                $key = strtolower( array_shift( $bits ) );
 
854
                                if ( count( $bits ) == 0 ) {
 
855
                                        $opts[$key] = true;
 
856
                                } elseif ( count( $bits ) == 1 ) {
 
857
                                        $opts[$key] = $this->cleanupOption( array_shift( $bits ) );
 
858
                                } else {
 
859
                                        // Array!
 
860
                                        $opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits );
 
861
                                }
 
862
                        }
 
863
                }
 
864
                return $opts;
 
865
        }
 
866
 
 
867
        protected function cleanupOption( $opt ) {
 
868
                if ( substr( $opt, 0, 1 ) == '"' ) {
 
869
                        return substr( $opt, 1, -1 );
 
870
                }
 
871
 
 
872
                if ( substr( $opt, 0, 2 ) == '[[' ) {
 
873
                        return substr( $opt, 2, -2 );
 
874
                }
 
875
                return $opt;
 
876
        }
 
877
 
 
878
        /**
 
879
         * Use a regex to find out the value of an option
 
880
         * @param $key String: name of option val to retrieve
 
881
         * @param $opts Options array to look in
 
882
         * @param $default Mixed: default value returned if not found
 
883
         */
 
884
        protected static function getOptionValue( $key, $opts, $default ) {
 
885
                $key = strtolower( $key );
 
886
 
 
887
                if ( isset( $opts[$key] ) ) {
 
888
                        return $opts[$key];
 
889
                } else {
 
890
                        return $default;
 
891
                }
 
892
        }
 
893
}